lanscape 1.3.8a1__py3-none-any.whl → 2.4.0a2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of lanscape might be problematic. Click here for more details.
- lanscape/__init__.py +8 -4
- lanscape/{libraries → core}/app_scope.py +21 -3
- lanscape/core/decorators.py +231 -0
- lanscape/{libraries → core}/device_alive.py +83 -16
- lanscape/{libraries → core}/ip_parser.py +2 -26
- lanscape/{libraries → core}/net_tools.py +209 -66
- lanscape/{libraries → core}/runtime_args.py +6 -0
- lanscape/{libraries → core}/scan_config.py +103 -5
- lanscape/core/service_scan.py +222 -0
- lanscape/{libraries → core}/subnet_scan.py +30 -14
- lanscape/{libraries → core}/version_manager.py +15 -17
- lanscape/resources/ports/test_port_list_scan.json +4 -0
- lanscape/resources/services/definitions.jsonc +576 -400
- lanscape/ui/app.py +17 -5
- lanscape/ui/blueprints/__init__.py +1 -1
- lanscape/ui/blueprints/api/port.py +15 -1
- lanscape/ui/blueprints/api/scan.py +1 -1
- lanscape/ui/blueprints/api/tools.py +4 -4
- lanscape/ui/blueprints/web/routes.py +29 -2
- lanscape/ui/main.py +46 -19
- lanscape/ui/shutdown_handler.py +2 -2
- lanscape/ui/static/css/style.css +186 -20
- lanscape/ui/static/js/core.js +14 -0
- lanscape/ui/static/js/main.js +30 -2
- lanscape/ui/static/js/quietReload.js +3 -0
- lanscape/ui/static/js/scan-config.js +56 -6
- lanscape/ui/templates/base.html +6 -8
- lanscape/ui/templates/core/head.html +1 -1
- lanscape/ui/templates/info.html +20 -5
- lanscape/ui/templates/main.html +33 -36
- lanscape/ui/templates/scan/config.html +214 -176
- lanscape/ui/templates/scan/device-detail.html +111 -0
- lanscape/ui/templates/scan/ip-table-row.html +17 -83
- lanscape/ui/templates/scan/ip-table.html +5 -5
- lanscape/ui/ws/__init__.py +31 -0
- lanscape/ui/ws/delta.py +170 -0
- lanscape/ui/ws/handlers/__init__.py +20 -0
- lanscape/ui/ws/handlers/base.py +145 -0
- lanscape/ui/ws/handlers/port.py +184 -0
- lanscape/ui/ws/handlers/scan.py +352 -0
- lanscape/ui/ws/handlers/tools.py +145 -0
- lanscape/ui/ws/protocol.py +86 -0
- lanscape/ui/ws/server.py +375 -0
- {lanscape-1.3.8a1.dist-info → lanscape-2.4.0a2.dist-info}/METADATA +18 -3
- lanscape-2.4.0a2.dist-info/RECORD +85 -0
- {lanscape-1.3.8a1.dist-info → lanscape-2.4.0a2.dist-info}/WHEEL +1 -1
- lanscape-2.4.0a2.dist-info/entry_points.txt +2 -0
- lanscape/libraries/decorators.py +0 -170
- lanscape/libraries/service_scan.py +0 -50
- lanscape/libraries/web_browser.py +0 -210
- lanscape-1.3.8a1.dist-info/RECORD +0 -74
- /lanscape/{libraries → core}/__init__.py +0 -0
- /lanscape/{libraries → core}/errors.py +0 -0
- /lanscape/{libraries → core}/logger.py +0 -0
- /lanscape/{libraries → core}/mac_lookup.py +0 -0
- /lanscape/{libraries → core}/port_manager.py +0 -0
- {lanscape-1.3.8a1.dist-info → lanscape-2.4.0a2.dist-info}/licenses/LICENSE +0 -0
- {lanscape-1.3.8a1.dist-info → lanscape-2.4.0a2.dist-info}/top_level.txt +0 -0
lanscape/ui/app.py
CHANGED
|
@@ -8,12 +8,12 @@ import logging
|
|
|
8
8
|
from flask import Flask, render_template
|
|
9
9
|
from lanscape.ui.blueprints.web import web_bp, routes # pylint: disable=unused-import
|
|
10
10
|
from lanscape.ui.blueprints.api import api_bp, tools, port, scan # pylint: disable=unused-import
|
|
11
|
-
from lanscape.
|
|
12
|
-
from lanscape.
|
|
11
|
+
from lanscape.core.runtime_args import RuntimeArgs, parse_args
|
|
12
|
+
from lanscape.core.version_manager import (
|
|
13
13
|
is_update_available, get_installed_version, lookup_latest_version
|
|
14
14
|
)
|
|
15
|
-
from lanscape.
|
|
16
|
-
from lanscape.
|
|
15
|
+
from lanscape.core.app_scope import is_local_run
|
|
16
|
+
from lanscape.core.net_tools import is_arp_supported
|
|
17
17
|
from lanscape.ui.shutdown_handler import FlaskShutdownHandler
|
|
18
18
|
|
|
19
19
|
app = Flask(
|
|
@@ -51,6 +51,18 @@ app.jinja_env.filters['is_substring_in_values'] = is_substring_in_values
|
|
|
51
51
|
################################
|
|
52
52
|
|
|
53
53
|
|
|
54
|
+
def get_runtime_args_safe():
|
|
55
|
+
"""
|
|
56
|
+
Safely get runtime args, returning empty dict if parsing fails.
|
|
57
|
+
This prevents conflicts when the module is imported during testing.
|
|
58
|
+
"""
|
|
59
|
+
try:
|
|
60
|
+
return vars(parse_args())
|
|
61
|
+
except SystemExit:
|
|
62
|
+
# This happens when pytest tries to import the module
|
|
63
|
+
return {}
|
|
64
|
+
|
|
65
|
+
|
|
54
66
|
def set_global_safe(key: str, value):
|
|
55
67
|
""" Safely set global vars without worrying about an exception """
|
|
56
68
|
app_globals = app.jinja_env.globals
|
|
@@ -73,7 +85,7 @@ def set_global_safe(key: str, value):
|
|
|
73
85
|
set_global_safe('app_version', get_installed_version)
|
|
74
86
|
set_global_safe('update_available', is_update_available)
|
|
75
87
|
set_global_safe('latest_version', lookup_latest_version)
|
|
76
|
-
set_global_safe('runtime_args',
|
|
88
|
+
set_global_safe('runtime_args', get_runtime_args_safe)
|
|
77
89
|
set_global_safe('is_local', is_local_run)
|
|
78
90
|
set_global_safe('is_arp_supported', is_arp_supported)
|
|
79
91
|
|
|
@@ -4,7 +4,7 @@ Provides CRUD operations for managing port lists used in network scans.
|
|
|
4
4
|
"""
|
|
5
5
|
from flask import request, jsonify
|
|
6
6
|
from lanscape.ui.blueprints.api import api_bp
|
|
7
|
-
from lanscape.
|
|
7
|
+
from lanscape.core.port_manager import PortManager
|
|
8
8
|
|
|
9
9
|
# Port Manager API
|
|
10
10
|
############################################
|
|
@@ -21,6 +21,20 @@ def get_port_lists():
|
|
|
21
21
|
return jsonify(PortManager().get_port_lists())
|
|
22
22
|
|
|
23
23
|
|
|
24
|
+
@api_bp.route('/api/port/list/summary', methods=['GET'])
|
|
25
|
+
def get_port_lists_summary():
|
|
26
|
+
"""Get port list names with their port counts."""
|
|
27
|
+
manager = PortManager()
|
|
28
|
+
summaries = []
|
|
29
|
+
for name in manager.get_port_lists():
|
|
30
|
+
ports = manager.get_port_list(name) or {}
|
|
31
|
+
summaries.append({
|
|
32
|
+
'name': name,
|
|
33
|
+
'count': len(ports)
|
|
34
|
+
})
|
|
35
|
+
return jsonify(summaries)
|
|
36
|
+
|
|
37
|
+
|
|
24
38
|
@api_bp.route('/api/port/list/<port_list>', methods=['GET'])
|
|
25
39
|
def get_port_list(port_list):
|
|
26
40
|
"""
|
|
@@ -9,7 +9,7 @@ import traceback
|
|
|
9
9
|
from flask import request, jsonify
|
|
10
10
|
|
|
11
11
|
from lanscape.ui.blueprints.api import api_bp
|
|
12
|
-
from lanscape.
|
|
12
|
+
from lanscape.core.subnet_scan import ScanConfig
|
|
13
13
|
from lanscape.ui.blueprints import scan_manager
|
|
14
14
|
|
|
15
15
|
# Subnet Scanner API
|
|
@@ -5,10 +5,10 @@ API endpoints for subnet testing and listing.
|
|
|
5
5
|
import traceback
|
|
6
6
|
from flask import request, jsonify
|
|
7
7
|
from lanscape.ui.blueprints.api import api_bp
|
|
8
|
-
from lanscape.
|
|
9
|
-
from lanscape.
|
|
10
|
-
from lanscape.
|
|
11
|
-
from lanscape.
|
|
8
|
+
from lanscape.core.net_tools import get_all_network_subnets, is_arp_supported
|
|
9
|
+
from lanscape.core.ip_parser import parse_ip_input
|
|
10
|
+
from lanscape.core.errors import SubnetTooLargeError
|
|
11
|
+
from lanscape.core.scan_config import DEFAULT_CONFIGS
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
@api_bp.route('/api/tools/subnet/test')
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
Web blueprint routes for the LANscape application.
|
|
3
3
|
Handles UI views including the main dashboard, scan results, error display, and exports.
|
|
4
4
|
"""
|
|
5
|
-
from flask import render_template, request, redirect
|
|
5
|
+
from flask import render_template, request, redirect, url_for
|
|
6
6
|
from lanscape.ui.blueprints.web import web_bp
|
|
7
|
-
from lanscape.
|
|
7
|
+
from lanscape.core.net_tools import (
|
|
8
8
|
get_all_network_subnets,
|
|
9
9
|
smart_select_primary_subnet
|
|
10
10
|
)
|
|
@@ -81,6 +81,33 @@ def view_errors(scan_id):
|
|
|
81
81
|
return redirect('/')
|
|
82
82
|
|
|
83
83
|
|
|
84
|
+
@web_bp.route('/device/<scan_id>/<device_ip>')
|
|
85
|
+
def view_device(scan_id, device_ip):
|
|
86
|
+
"""
|
|
87
|
+
Display detailed information about a specific device from a scan.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
scan_id: Unique identifier for the scan
|
|
91
|
+
device_ip: IP address of the device to view
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
Rendered device detail template or redirect to home if scan not found
|
|
95
|
+
"""
|
|
96
|
+
if scanner := scan_manager.get_scan(scan_id):
|
|
97
|
+
devices = scanner.results.devices
|
|
98
|
+
device_info = next(
|
|
99
|
+
(device for device in devices if getattr(
|
|
100
|
+
device, 'ip', None) == device_ip), None)
|
|
101
|
+
|
|
102
|
+
if device_info:
|
|
103
|
+
return render_template('scan/device-detail.html', device=device_info, scan_id=scan_id)
|
|
104
|
+
|
|
105
|
+
log.debug(f'Device {device_ip} not found in scan {scan_id}')
|
|
106
|
+
return redirect(url_for('render_scan', scan_id=scan_id))
|
|
107
|
+
log.debug(f'Redirecting, scan {scan_id} doesnt exist in memory')
|
|
108
|
+
return redirect('/')
|
|
109
|
+
|
|
110
|
+
|
|
84
111
|
@web_bp.route('/export/<scan_id>')
|
|
85
112
|
def export_scan(scan_id):
|
|
86
113
|
"""
|
lanscape/ui/main.py
CHANGED
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
"""Main entry point for the LANscape application when running as a module."""
|
|
2
2
|
import socket
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
import threading
|
|
6
4
|
import time
|
|
7
5
|
import logging
|
|
8
6
|
import traceback
|
|
9
7
|
import os
|
|
8
|
+
from subprocess import Popen
|
|
9
|
+
import webbrowser
|
|
10
10
|
import requests
|
|
11
11
|
|
|
12
|
-
from
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
from lanscape.
|
|
12
|
+
from pwa_launcher import open_pwa, ChromiumNotFoundError
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
from lanscape.core.logger import configure_logging
|
|
16
|
+
from lanscape.core.runtime_args import parse_args
|
|
17
|
+
from lanscape.core.version_manager import get_installed_version, is_update_available
|
|
16
18
|
from lanscape.ui.app import start_webserver_daemon, start_webserver
|
|
19
|
+
from lanscape.ui.ws.server import run_server
|
|
17
20
|
# do this so any logs generated on import are displayed
|
|
18
21
|
args = parse_args()
|
|
19
22
|
configure_logging(args.loglevel, args.logfile, args.flask_logging)
|
|
@@ -46,6 +49,11 @@ def _main():
|
|
|
46
49
|
else:
|
|
47
50
|
log.info('Flask reloaded app.')
|
|
48
51
|
|
|
52
|
+
# Check if WebSocket server mode is requested
|
|
53
|
+
if args.ws_server:
|
|
54
|
+
start_websocket_server()
|
|
55
|
+
return
|
|
56
|
+
|
|
49
57
|
args.port = get_valid_port(args.port)
|
|
50
58
|
|
|
51
59
|
try:
|
|
@@ -70,7 +78,23 @@ def try_check_update():
|
|
|
70
78
|
log.warning('Unable to check for updates.')
|
|
71
79
|
|
|
72
80
|
|
|
73
|
-
def
|
|
81
|
+
def start_websocket_server():
|
|
82
|
+
"""Start the WebSocket server."""
|
|
83
|
+
args.ws_port = get_valid_port(args.ws_port)
|
|
84
|
+
log.info(f'Starting WebSocket server on port {args.ws_port}')
|
|
85
|
+
log.info(f'React UI should connect to ws://localhost:{args.ws_port}')
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
run_server(host='0.0.0.0', port=args.ws_port)
|
|
89
|
+
except KeyboardInterrupt:
|
|
90
|
+
log.info('WebSocket server stopped by user')
|
|
91
|
+
except Exception as e:
|
|
92
|
+
log.critical(f'WebSocket server failed: {e}')
|
|
93
|
+
log.debug(traceback.format_exc())
|
|
94
|
+
raise
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def open_browser(url: str, wait=2) -> Popen | None:
|
|
74
98
|
"""
|
|
75
99
|
Open a browser window to the specified
|
|
76
100
|
url after waiting for the server to start
|
|
@@ -78,12 +102,18 @@ def open_browser(url: str, wait=2) -> bool:
|
|
|
78
102
|
try:
|
|
79
103
|
time.sleep(wait)
|
|
80
104
|
log.info(f'Starting UI - http://127.0.0.1:{args.port}')
|
|
81
|
-
return
|
|
82
|
-
|
|
105
|
+
return open_pwa(url)
|
|
106
|
+
|
|
107
|
+
except ChromiumNotFoundError:
|
|
108
|
+
success = webbrowser.open(url)
|
|
109
|
+
if success:
|
|
110
|
+
log.warning("Chromium browser not found. Falling back to default web browser.")
|
|
111
|
+
else:
|
|
112
|
+
log.warning(f"Cannot find any web browser. LANScape UI running on {url}")
|
|
83
113
|
except BaseException:
|
|
84
114
|
log.debug(traceback.format_exc())
|
|
85
115
|
log.info(f'Unable to open web browser, server running on {url}')
|
|
86
|
-
return
|
|
116
|
+
return None
|
|
87
117
|
|
|
88
118
|
|
|
89
119
|
def start_webserver_ui():
|
|
@@ -97,19 +127,16 @@ def start_webserver_ui():
|
|
|
97
127
|
# if it was, dont open the browser again
|
|
98
128
|
log.info('Opening UI as daemon')
|
|
99
129
|
if not IS_FLASK_RELOAD:
|
|
100
|
-
|
|
101
|
-
target=open_browser,
|
|
102
|
-
args=(uri,),
|
|
103
|
-
daemon=True
|
|
104
|
-
).start()
|
|
130
|
+
open_browser(uri)
|
|
105
131
|
start_webserver(args)
|
|
106
132
|
else:
|
|
107
133
|
flask_thread = start_webserver_daemon(args)
|
|
108
|
-
|
|
134
|
+
proc = open_browser(uri)
|
|
135
|
+
if proc:
|
|
136
|
+
app_closed = proc.wait()
|
|
137
|
+
else:
|
|
138
|
+
app_closed = False
|
|
109
139
|
|
|
110
|
-
# depending on env, open_browser may or
|
|
111
|
-
# may not be coupled with the closure of UI
|
|
112
|
-
# (if in browser tab, it's uncoupled)
|
|
113
140
|
if not app_closed or args.persistent:
|
|
114
141
|
# not doing a direct join so i can still
|
|
115
142
|
# terminate the app with ctrl+c
|
lanscape/ui/shutdown_handler.py
CHANGED
|
@@ -5,10 +5,10 @@ import os
|
|
|
5
5
|
from flask import request
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
from lanscape.
|
|
8
|
+
from lanscape.core.runtime_args import parse_args
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
log = logging.getLogger('
|
|
11
|
+
log = logging.getLogger('ShutdownHandler')
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class FlaskShutdownHandler:
|
lanscape/ui/static/css/style.css
CHANGED
|
@@ -34,6 +34,9 @@
|
|
|
34
34
|
--danger-border-color: #922; /* Bold red for danger borders */
|
|
35
35
|
|
|
36
36
|
--footer-height: 25px;
|
|
37
|
+
|
|
38
|
+
--fa-primary-color: var(--text-accent-color);
|
|
39
|
+
--fa-secondary-color: var(--text-placeholder);
|
|
37
40
|
}
|
|
38
41
|
|
|
39
42
|
body {
|
|
@@ -63,13 +66,16 @@ body:has(.submodule) footer {
|
|
|
63
66
|
|
|
64
67
|
#header {
|
|
65
68
|
background-color: var(--primary-bg);
|
|
66
|
-
padding: 8px
|
|
69
|
+
padding: 8px 2%;
|
|
67
70
|
margin: 0;
|
|
68
71
|
display: block;
|
|
69
72
|
position: relative;
|
|
70
73
|
box-shadow: 0 0 10px var(--box-shadow);
|
|
71
74
|
width: 100vw;
|
|
72
75
|
}
|
|
76
|
+
#header .title {
|
|
77
|
+
font-size: 36px;
|
|
78
|
+
}
|
|
73
79
|
|
|
74
80
|
footer {
|
|
75
81
|
position: sticky;
|
|
@@ -115,6 +121,14 @@ hr {
|
|
|
115
121
|
border-color: var(--border-color);
|
|
116
122
|
margin: 20px 0;
|
|
117
123
|
}
|
|
124
|
+
|
|
125
|
+
.config-sections > .config-section {
|
|
126
|
+
border-top: 1px solid var(--border-color);
|
|
127
|
+
margin-top: 20px;
|
|
128
|
+
padding-top: 20px;
|
|
129
|
+
padding-bottom: 20px;
|
|
130
|
+
}
|
|
131
|
+
|
|
118
132
|
h1.title{
|
|
119
133
|
cursor: pointer;
|
|
120
134
|
margin: 0;
|
|
@@ -152,7 +166,12 @@ details {
|
|
|
152
166
|
|
|
153
167
|
|
|
154
168
|
#scan-form {
|
|
155
|
-
width:
|
|
169
|
+
width: 60vw;
|
|
170
|
+
margin: 0;
|
|
171
|
+
min-width: 300px;
|
|
172
|
+
max-width: 700px;
|
|
173
|
+
}
|
|
174
|
+
#scan-form .form-group {
|
|
156
175
|
margin: 0;
|
|
157
176
|
}
|
|
158
177
|
#scan-form label {
|
|
@@ -236,7 +255,7 @@ details {
|
|
|
236
255
|
transition: all .5s ease-in-out
|
|
237
256
|
}
|
|
238
257
|
|
|
239
|
-
#app-actions a .
|
|
258
|
+
#app-actions a .fa-solid {
|
|
240
259
|
font-size: inherit
|
|
241
260
|
}
|
|
242
261
|
#app-actions {
|
|
@@ -249,9 +268,12 @@ details {
|
|
|
249
268
|
color: var(--text-color);
|
|
250
269
|
text-decoration: none;
|
|
251
270
|
cursor: pointer;
|
|
271
|
+
--fa-primary-color: var(--text-accent-color);
|
|
272
|
+
--fa-secondary-color: var(--text-placeholder);
|
|
252
273
|
}
|
|
253
|
-
#app-actions a:hover {
|
|
254
|
-
color: var(--text-
|
|
274
|
+
#app-actions a i:hover {
|
|
275
|
+
--fa-primary-color: var(--text-color);
|
|
276
|
+
--fa-secondary-color: var(--text-accent-color);
|
|
255
277
|
}
|
|
256
278
|
|
|
257
279
|
#power-button {
|
|
@@ -266,10 +288,10 @@ details {
|
|
|
266
288
|
display: flex;
|
|
267
289
|
justify-content: space-around;
|
|
268
290
|
align-items: center;
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
#scan-actions i {
|
|
294
|
+
font-size: 20px;
|
|
273
295
|
}
|
|
274
296
|
|
|
275
297
|
#advanced-modal {
|
|
@@ -290,6 +312,9 @@ details {
|
|
|
290
312
|
}
|
|
291
313
|
#advanced-modal label {
|
|
292
314
|
font-size: 12px;
|
|
315
|
+
text-overflow: ellipsis;
|
|
316
|
+
overflow: hidden;
|
|
317
|
+
white-space: nowrap;
|
|
293
318
|
}
|
|
294
319
|
#advanced-modal .form-check {
|
|
295
320
|
width: fit-content;
|
|
@@ -433,8 +458,10 @@ button {
|
|
|
433
458
|
|
|
434
459
|
#scan-form #scan-submit {
|
|
435
460
|
border: none;
|
|
436
|
-
padding: 10px
|
|
437
|
-
margin
|
|
461
|
+
padding: 10px 3%;
|
|
462
|
+
margin: 0 15px;
|
|
463
|
+
min-width: 55px;
|
|
464
|
+
height: 42px;
|
|
438
465
|
}
|
|
439
466
|
|
|
440
467
|
/* Button Styling */
|
|
@@ -547,6 +574,41 @@ input[type="range"] {
|
|
|
547
574
|
color: var(--text-color);
|
|
548
575
|
}
|
|
549
576
|
|
|
577
|
+
/* Service Strategy Select Styles */
|
|
578
|
+
.service-strategy-wrapper {
|
|
579
|
+
position: relative;
|
|
580
|
+
display: block;
|
|
581
|
+
width: 100%;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
.service-strategy {
|
|
585
|
+
position: relative;
|
|
586
|
+
background-color: var(--secondary-bg);
|
|
587
|
+
border: 1px solid var(--border-color);
|
|
588
|
+
color: var(--text-color);
|
|
589
|
+
padding: 10px;
|
|
590
|
+
cursor: pointer;
|
|
591
|
+
width: 100%;
|
|
592
|
+
height: 42px;
|
|
593
|
+
user-select: none;
|
|
594
|
+
appearance: none; /* Hide default arrow */
|
|
595
|
+
transition: all .2s ease-in-out;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
.service-strategy:focus {
|
|
599
|
+
border-color: var(--primary-accent-hover);
|
|
600
|
+
outline: none;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
.service-strategy-wrapper::after {
|
|
604
|
+
content: '▼';
|
|
605
|
+
position: absolute;
|
|
606
|
+
top: 10px;
|
|
607
|
+
right: 10px;
|
|
608
|
+
pointer-events: none;
|
|
609
|
+
color: var(--text-color);
|
|
610
|
+
}
|
|
611
|
+
|
|
550
612
|
|
|
551
613
|
|
|
552
614
|
.text-color {
|
|
@@ -604,6 +666,31 @@ input[type="range"] {
|
|
|
604
666
|
background-color: var(--primary-bg-accent);
|
|
605
667
|
}
|
|
606
668
|
|
|
669
|
+
.table tbody tr td:has(.info-icon-container),
|
|
670
|
+
.table thead tr th.detail-col
|
|
671
|
+
{
|
|
672
|
+
width: 30px;
|
|
673
|
+
}
|
|
674
|
+
.table td:has(.info-icon-container) {
|
|
675
|
+
width: 30px;
|
|
676
|
+
text-align: center; /* horizontal center */
|
|
677
|
+
vertical-align: middle; /* vertical center inside the cell */
|
|
678
|
+
padding: 0; /* optional: remove extra padding */
|
|
679
|
+
}
|
|
680
|
+
.table td .info-icon-container {
|
|
681
|
+
display: flex;
|
|
682
|
+
justify-content: center; /* horizontal center */
|
|
683
|
+
align-items: center; /* vertical center */
|
|
684
|
+
height: 100%; /* ensure it takes full cell height */
|
|
685
|
+
}
|
|
686
|
+
.table td .info-icon-container .info-icon {
|
|
687
|
+
font-size: 1.2em;
|
|
688
|
+
color: var(--text-placeholder);
|
|
689
|
+
cursor: pointer;
|
|
690
|
+
}
|
|
691
|
+
.table td .info-icon:hover {
|
|
692
|
+
color: var(--text-color);
|
|
693
|
+
}
|
|
607
694
|
|
|
608
695
|
/* Badge Styles */
|
|
609
696
|
.badge-warning {
|
|
@@ -619,7 +706,7 @@ input[type="range"] {
|
|
|
619
706
|
}
|
|
620
707
|
|
|
621
708
|
.badge-info {
|
|
622
|
-
background-color: var(--
|
|
709
|
+
background-color: var(--primary-accent);
|
|
623
710
|
}
|
|
624
711
|
|
|
625
712
|
.badge-secondary {
|
|
@@ -631,6 +718,12 @@ input[type="range"] {
|
|
|
631
718
|
span.alt {
|
|
632
719
|
color: var(--text-accent-color);
|
|
633
720
|
}
|
|
721
|
+
span.no-wrap {
|
|
722
|
+
white-space: nowrap;
|
|
723
|
+
overflow: hidden;
|
|
724
|
+
text-overflow: ellipsis;
|
|
725
|
+
display: block;
|
|
726
|
+
}
|
|
634
727
|
.colorful-buttons a{
|
|
635
728
|
margin:2px;
|
|
636
729
|
color: var(--text-color);
|
|
@@ -724,13 +817,7 @@ html {
|
|
|
724
817
|
}
|
|
725
818
|
|
|
726
819
|
|
|
727
|
-
|
|
728
|
-
font-variation-settings:
|
|
729
|
-
'FILL' 0,
|
|
730
|
-
'wght' 400,
|
|
731
|
-
'GRAD' 0,
|
|
732
|
-
'opsz' 24
|
|
733
|
-
}
|
|
820
|
+
/* FontAwesome Solid Icon Styling */
|
|
734
821
|
|
|
735
822
|
#shutdown-sub-sub {
|
|
736
823
|
color: var(--text-almost-hidden);
|
|
@@ -827,4 +914,83 @@ html {
|
|
|
827
914
|
|
|
828
915
|
}
|
|
829
916
|
|
|
830
|
-
/* END overview container */
|
|
917
|
+
/* END overview container */
|
|
918
|
+
|
|
919
|
+
/* Device Modal Styles */
|
|
920
|
+
#device-modal {
|
|
921
|
+
--bs-modal-width: 750px;
|
|
922
|
+
}
|
|
923
|
+
#device-modal .modal-content {
|
|
924
|
+
background-color: var(--primary-bg);
|
|
925
|
+
}
|
|
926
|
+
#device-modal h6 {
|
|
927
|
+
color: var(--primary-accent);
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
/* Key/Value grid for overview */
|
|
931
|
+
#device-modal .kv-grid {
|
|
932
|
+
display: grid;
|
|
933
|
+
grid-template-columns: 180px 1fr;
|
|
934
|
+
column-gap: 12px;
|
|
935
|
+
row-gap: 8px;
|
|
936
|
+
align-items: center;
|
|
937
|
+
}
|
|
938
|
+
#device-modal .kv-label {
|
|
939
|
+
color: var(--text-placeholder);
|
|
940
|
+
text-align: right;
|
|
941
|
+
}
|
|
942
|
+
#device-modal .kv-value {
|
|
943
|
+
color: var(--text-color);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
/* Port/service chips */
|
|
947
|
+
#device-modal .chip-group { margin-top: 4px; }
|
|
948
|
+
#device-modal .chip {
|
|
949
|
+
display: inline-flex;
|
|
950
|
+
align-items: center;
|
|
951
|
+
gap: 4px;
|
|
952
|
+
padding: 2px 8px;
|
|
953
|
+
margin: 2px;
|
|
954
|
+
font-size: .85rem;
|
|
955
|
+
background-color: var(--primary-bg-accent);
|
|
956
|
+
border: 1px solid var(--border-color);
|
|
957
|
+
border-radius: 999px;
|
|
958
|
+
color: var(--text-color);
|
|
959
|
+
}
|
|
960
|
+
#device-modal .chip .fa-solid {
|
|
961
|
+
font-size: 16px;
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
/* Services layout */
|
|
965
|
+
#device-modal .service-list { width: 100%; }
|
|
966
|
+
#device-modal .service-row {
|
|
967
|
+
display: flex;
|
|
968
|
+
align-items: flex-start;
|
|
969
|
+
gap: 8px;
|
|
970
|
+
padding: 6px 0;
|
|
971
|
+
border-bottom: 1px dashed var(--border-color);
|
|
972
|
+
}
|
|
973
|
+
#device-modal .service-row:last-child {
|
|
974
|
+
border-bottom: none;
|
|
975
|
+
}
|
|
976
|
+
#device-modal .service-name {
|
|
977
|
+
min-width: 140px;
|
|
978
|
+
color: var(--text-accent-color);
|
|
979
|
+
font-weight: 600;
|
|
980
|
+
}
|
|
981
|
+
#device-modal .service-ports { flex: 1; }
|
|
982
|
+
|
|
983
|
+
/* Errors */
|
|
984
|
+
#device-modal .error-list li {
|
|
985
|
+
margin-bottom: 4px;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
/* Responsive tweaks */
|
|
989
|
+
@media screen and (max-width: 576px) {
|
|
990
|
+
#device-modal .kv-grid {
|
|
991
|
+
grid-template-columns: 130px 1fr;
|
|
992
|
+
}
|
|
993
|
+
#device-modal .service-name {
|
|
994
|
+
min-width: 110px;
|
|
995
|
+
}
|
|
996
|
+
}
|
lanscape/ui/static/js/core.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
$(document).ready(function() {
|
|
2
2
|
rightSizeDocLayout(0,showFooter);
|
|
3
3
|
initTooltips();
|
|
4
|
+
adjustNoWrap();
|
|
4
5
|
})
|
|
5
6
|
|
|
6
7
|
$(window).on('resize', function() {
|
|
7
8
|
rightSizeDocLayout();
|
|
9
|
+
adjustNoWrap();
|
|
8
10
|
});
|
|
9
11
|
|
|
10
12
|
|
|
@@ -36,4 +38,16 @@ function initTooltips() {
|
|
|
36
38
|
tooltipTriggerList.map(function (tooltipTriggerEl) {
|
|
37
39
|
return new bootstrap.Tooltip(tooltipTriggerEl)
|
|
38
40
|
})
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/*
|
|
44
|
+
An imperfect approach to adjusting
|
|
45
|
+
text field width within a table
|
|
46
|
+
*/
|
|
47
|
+
function adjustNoWrap() {
|
|
48
|
+
$('.no-wrap').width(0);
|
|
49
|
+
$('.no-wrap').each(function() {
|
|
50
|
+
var parentWidth = $(this).parent().width();
|
|
51
|
+
$(this).width(parseInt(parentWidth));
|
|
52
|
+
});
|
|
39
53
|
}
|
lanscape/ui/static/js/main.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
1
|
$(document).ready(function() {
|
|
4
2
|
// Load port lists into the dropdown
|
|
5
3
|
const scanId = getActiveScanId();
|
|
@@ -217,6 +215,36 @@ $(window).on('resize', function() {
|
|
|
217
215
|
resizeIframe($('#ip-table-frame')[0]);
|
|
218
216
|
});
|
|
219
217
|
|
|
218
|
+
function openDeviceDetail(deviceIp) {
|
|
219
|
+
try {
|
|
220
|
+
const scanId = getActiveScanId();
|
|
221
|
+
if (!scanId || !deviceIp) return;
|
|
222
|
+
|
|
223
|
+
const safeIp = encodeURIComponent(deviceIp.trim());
|
|
224
|
+
|
|
225
|
+
// Remove any existing modal instance to avoid duplicates
|
|
226
|
+
$('#device-modal').remove();
|
|
227
|
+
|
|
228
|
+
$.get(`/device/${scanId}/${safeIp}`, function(html) {
|
|
229
|
+
// Append modal HTML to the document
|
|
230
|
+
$('body').append(html);
|
|
231
|
+
|
|
232
|
+
// Show the modal
|
|
233
|
+
const $modal = $('#device-modal');
|
|
234
|
+
$modal.modal('show');
|
|
235
|
+
|
|
236
|
+
// Clean up after closing
|
|
237
|
+
$modal.on('hidden.bs.modal', function() {
|
|
238
|
+
$(this).remove();
|
|
239
|
+
});
|
|
240
|
+
}).fail(function() {
|
|
241
|
+
console.error('Failed to load device details');
|
|
242
|
+
});
|
|
243
|
+
} catch (e) {
|
|
244
|
+
console.error('Error opening device detail modal:', e);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
220
248
|
|
|
221
249
|
|
|
222
250
|
|
|
@@ -8,6 +8,9 @@ function quietReload() {
|
|
|
8
8
|
var newDoc = new DOMParser().parseFromString(data, 'text/html');
|
|
9
9
|
// replace current body with the new body content
|
|
10
10
|
$('body').html($(newDoc.body).html());
|
|
11
|
+
if (typeof adjustNoWrap === 'function') {
|
|
12
|
+
adjustNoWrap();
|
|
13
|
+
}
|
|
11
14
|
});
|
|
12
15
|
}
|
|
13
16
|
setTimeout(function() {
|