lanscape 2.1.2__tar.gz → 2.4.0a1__tar.gz

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.

Files changed (103) hide show
  1. {lanscape-2.1.2/lanscape.egg-info → lanscape-2.4.0a1}/PKG-INFO +6 -1
  2. {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/core/net_tools.py +18 -3
  3. {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/core/runtime_args.py +6 -0
  4. {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/core/service_scan.py +38 -3
  5. {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/blueprints/api/port.py +14 -0
  6. {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/main.py +44 -16
  7. {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/static/css/style.css +25 -10
  8. {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/static/js/core.js +14 -0
  9. {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/static/js/quietReload.js +3 -0
  10. {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/static/js/scan-config.js +17 -6
  11. {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/templates/main.html +30 -32
  12. {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/templates/scan/config.html +6 -4
  13. {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/templates/scan/ip-table-row.html +6 -6
  14. {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/templates/scan/ip-table.html +4 -4
  15. lanscape-2.4.0a1/lanscape/ui/ws/__init__.py +31 -0
  16. lanscape-2.4.0a1/lanscape/ui/ws/delta.py +170 -0
  17. lanscape-2.4.0a1/lanscape/ui/ws/handlers/__init__.py +20 -0
  18. lanscape-2.4.0a1/lanscape/ui/ws/handlers/base.py +145 -0
  19. lanscape-2.4.0a1/lanscape/ui/ws/handlers/port.py +184 -0
  20. lanscape-2.4.0a1/lanscape/ui/ws/handlers/scan.py +352 -0
  21. lanscape-2.4.0a1/lanscape/ui/ws/handlers/tools.py +145 -0
  22. lanscape-2.4.0a1/lanscape/ui/ws/protocol.py +86 -0
  23. lanscape-2.4.0a1/lanscape/ui/ws/server.py +375 -0
  24. {lanscape-2.1.2 → lanscape-2.4.0a1/lanscape.egg-info}/PKG-INFO +6 -1
  25. {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape.egg-info/SOURCES.txt +12 -2
  26. {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape.egg-info/requires.txt +5 -0
  27. {lanscape-2.1.2 → lanscape-2.4.0a1}/pyproject.toml +8 -3
  28. {lanscape-2.1.2 → lanscape-2.4.0a1}/tests/test_api.py +30 -1
  29. lanscape-2.4.0a1/tests/test_port_scan_linux_fd_exhaustion.py +297 -0
  30. {lanscape-2.1.2 → lanscape-2.4.0a1}/tests/test_utils.py +2 -2
  31. lanscape-2.4.0a1/tests/test_websocket.py +940 -0
  32. lanscape-2.1.2/lanscape/core/web_browser.py +0 -210
  33. {lanscape-2.1.2 → lanscape-2.4.0a1}/LICENSE +0 -0
  34. {lanscape-2.1.2 → lanscape-2.4.0a1}/MANIFEST.in +0 -0
  35. {lanscape-2.1.2 → lanscape-2.4.0a1}/README.md +0 -0
  36. {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/__init__.py +0 -0
  37. {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/__main__.py +0 -0
  38. {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/core/__init__.py +0 -0
  39. {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/core/app_scope.py +0 -0
  40. {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/core/decorators.py +0 -0
  41. {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/core/device_alive.py +0 -0
  42. {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/core/errors.py +0 -0
  43. {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/core/ip_parser.py +0 -0
  44. {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/core/logger.py +0 -0
  45. {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/core/mac_lookup.py +0 -0
  46. {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/core/port_manager.py +0 -0
  47. {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/core/scan_config.py +0 -0
  48. {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/core/subnet_scan.py +0 -0
  49. {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/core/version_manager.py +0 -0
  50. {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/resources/mac_addresses/convert_csv.py +0 -0
  51. {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/resources/mac_addresses/mac_db.json +0 -0
  52. {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/resources/ports/convert_csv.py +0 -0
  53. {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/resources/ports/full.json +0 -0
  54. {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/resources/ports/large.json +0 -0
  55. {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/resources/ports/medium.json +0 -0
  56. {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/resources/ports/small.json +0 -0
  57. {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/resources/ports/test_port_list_scan.json +0 -0
  58. {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/resources/services/definitions.jsonc +0 -0
  59. {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/__init__.py +0 -0
  60. {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/app.py +0 -0
  61. {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/blueprints/__init__.py +0 -0
  62. {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/blueprints/api/__init__.py +0 -0
  63. {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/blueprints/api/scan.py +0 -0
  64. {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/blueprints/api/tools.py +0 -0
  65. {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/blueprints/web/__init__.py +0 -0
  66. {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/blueprints/web/routes.py +0 -0
  67. {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/shutdown_handler.py +0 -0
  68. {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/static/img/ico/android-chrome-192x192.png +0 -0
  69. {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/static/img/ico/android-chrome-512x512.png +0 -0
  70. {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/static/img/ico/apple-touch-icon.png +0 -0
  71. {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/static/img/ico/favicon-16x16.png +0 -0
  72. {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/static/img/ico/favicon-32x32.png +0 -0
  73. {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/static/img/ico/favicon.ico +0 -0
  74. {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/static/img/ico/site.webmanifest +0 -0
  75. {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/static/js/layout-sizing.js +0 -0
  76. {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/static/js/main.js +0 -0
  77. {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/static/js/on-tab-close.js +0 -0
  78. {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/static/js/shutdown-server.js +0 -0
  79. {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/static/js/subnet-info.js +0 -0
  80. {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/static/js/subnet-selector.js +0 -0
  81. {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/static/lanscape.webmanifest +0 -0
  82. {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/templates/base.html +0 -0
  83. {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/templates/core/head.html +0 -0
  84. {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/templates/core/scripts.html +0 -0
  85. {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/templates/error.html +0 -0
  86. {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/templates/info.html +0 -0
  87. {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/templates/scan/device-detail.html +0 -0
  88. {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/templates/scan/export.html +0 -0
  89. {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/templates/scan/overview.html +0 -0
  90. {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/templates/scan/scan-error.html +0 -0
  91. {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/templates/scan.html +0 -0
  92. {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape/ui/templates/shutdown.html +0 -0
  93. {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape.egg-info/dependency_links.txt +0 -0
  94. {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape.egg-info/entry_points.txt +0 -0
  95. {lanscape-2.1.2 → lanscape-2.4.0a1}/lanscape.egg-info/top_level.txt +0 -0
  96. {lanscape-2.1.2 → lanscape-2.4.0a1}/setup.cfg +0 -0
  97. {lanscape-2.1.2 → lanscape-2.4.0a1}/tests/test_decorators.py +0 -0
  98. {lanscape-2.1.2 → lanscape-2.4.0a1}/tests/test_env.py +0 -0
  99. {lanscape-2.1.2 → lanscape-2.4.0a1}/tests/test_globals.py +0 -0
  100. {lanscape-2.1.2 → lanscape-2.4.0a1}/tests/test_library.py +0 -0
  101. {lanscape-2.1.2 → lanscape-2.4.0a1}/tests/test_logging.py +0 -0
  102. {lanscape-2.1.2 → lanscape-2.4.0a1}/tests/test_port_scan.py +0 -0
  103. {lanscape-2.1.2 → lanscape-2.4.0a1}/tests/test_service_scan.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lanscape
3
- Version: 2.1.2
3
+ Version: 2.4.0a1
4
4
  Summary: A python based local network scanner
5
5
  Author-email: Michael Dennis <michael@dipduo.com>
6
6
  License-Expression: MIT
@@ -25,11 +25,16 @@ Requires-Dist: scapy<3.0,>=2.3.2
25
25
  Requires-Dist: tabulate==0.9.0
26
26
  Requires-Dist: pydantic
27
27
  Requires-Dist: icmplib
28
+ Requires-Dist: pwa-launcher>=1.1.0
29
+ Requires-Dist: websockets<14.0,>=12.0
28
30
  Provides-Extra: dev
29
31
  Requires-Dist: pytest>=8.0; extra == "dev"
30
32
  Requires-Dist: pytest-cov>=5.0; extra == "dev"
31
33
  Requires-Dist: pytest-xdist>=3.0; extra == "dev"
34
+ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
32
35
  Requires-Dist: openai>=1.0.0; extra == "dev"
36
+ Requires-Dist: pylint>=3.0; extra == "dev"
37
+ Requires-Dist: autopep8>=2.0; extra == "dev"
33
38
  Dynamic: license-file
34
39
 
35
40
  # LANscape
@@ -98,6 +98,10 @@ class Device(BaseModel):
98
98
  if self.alive:
99
99
  self.hostname = self._get_hostname()
100
100
  self._get_mac_addresses()
101
+ if not self.manufacturer:
102
+ self.manufacturer = self._get_manufacturer(
103
+ self.get_mac()
104
+ )
101
105
 
102
106
  # Fallback for pydantic v1: use dict() and enrich output
103
107
  if not _PYD_V2:
@@ -138,18 +142,29 @@ class Device(BaseModel):
138
142
  @timeout_enforcer(enforcer_timeout, False)
139
143
  def do_test():
140
144
  for attempt in range(port_config.retries + 1):
141
- sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
142
- sock.settimeout(port_config.timeout)
145
+ sock = None
143
146
  try:
147
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
148
+ sock.settimeout(port_config.timeout)
144
149
  result = sock.connect_ex((self.ip, port))
145
150
  if result == 0:
146
151
  if port not in self.ports:
147
152
  self.ports.append(port)
148
153
  return True
154
+ except OSError as e:
155
+ # Handle socket creation failures (e.g., "Too many open files")
156
+ # Log and continue to retry if attempts remain
157
+ log = logging.getLogger('Device.test_port')
158
+ log.debug(f"OSError on {self.ip}:{port} attempt {attempt + 1}: {e}")
149
159
  except Exception:
150
160
  pass # Connection failed, try again if retries remain
151
161
  finally:
152
- sock.close()
162
+ # Always close socket if it was created
163
+ if sock is not None:
164
+ try:
165
+ sock.close()
166
+ except Exception:
167
+ pass # Ignore errors during cleanup
153
168
 
154
169
  # Wait before retry (except on last attempt)
155
170
  if attempt < port_config.retries:
@@ -14,6 +14,8 @@ class RuntimeArgs:
14
14
  loglevel: str = 'INFO'
15
15
  flask_logging: bool = False
16
16
  persistent: bool = False
17
+ ws_server: bool = False
18
+ ws_port: int = 8766
17
19
 
18
20
 
19
21
  def parse_args() -> RuntimeArgs:
@@ -35,6 +37,10 @@ def parse_args() -> RuntimeArgs:
35
37
  help='Don\'t exit after browser is closed')
36
38
  parser.add_argument('--debug', action='store_true',
37
39
  help='Shorthand debug mode (equivalent to "--loglevel DEBUG --reloader")')
40
+ parser.add_argument('--ws-server', action='store_true',
41
+ help='Start WebSocket server instead of Flask UI')
42
+ parser.add_argument('--ws-port', type=int, default=8766,
43
+ help='Port for WebSocket server (default: 8766)')
38
44
 
39
45
  # Parse the arguments
40
46
  args = parser.parse_args()
@@ -154,7 +154,9 @@ def get_port_probes(port: int, strategy: ServiceScanStrategy):
154
154
 
155
155
  def scan_service(ip: str, port: int, cfg: ServiceScanConfig) -> str:
156
156
  """
157
- Synchronous function that attempts to identify the service running on a given port.
157
+ Synchronous function that attempts to identify the service
158
+ running on a given port.
159
+ TODO: This is AI slop and needs to be reworked properly.
158
160
  """
159
161
 
160
162
  async def _async_scan_service(
@@ -183,5 +185,38 @@ def scan_service(ip: str, port: int, cfg: ServiceScanConfig) -> str:
183
185
  log.debug(traceback.format_exc())
184
186
  return "Unknown"
185
187
 
186
- # Use asyncio.run to execute the asynchronous logic synchronously
187
- return asyncio.run(_async_scan_service(ip, port, cfg=cfg))
188
+ # Create and properly manage event loop to avoid file descriptor leaks
189
+ # Using new_event_loop + explicit close is safer in threaded environments
190
+ # than asyncio.run() which can leave resources open under heavy load
191
+ loop = None
192
+ try:
193
+ try:
194
+ # Try to get existing loop first (if running in async context)
195
+ loop = asyncio.get_running_loop()
196
+ # If we're already in an async context, just await directly
197
+ return asyncio.run_coroutine_threadsafe(
198
+ _async_scan_service(ip, port, cfg=cfg), loop
199
+ ).result(timeout=cfg.timeout + 5)
200
+ except RuntimeError:
201
+ # No running loop, create a new one
202
+ loop = asyncio.new_event_loop()
203
+ asyncio.set_event_loop(loop)
204
+ try:
205
+ return loop.run_until_complete(_async_scan_service(ip, port, cfg=cfg))
206
+ finally:
207
+ # Clean up the loop properly
208
+ try:
209
+ # Cancel all remaining tasks
210
+ pending = asyncio.all_tasks(loop)
211
+ for task in pending:
212
+ task.cancel()
213
+ # Run loop once more to process cancellations
214
+ if pending:
215
+ loop.run_until_complete(asyncio.gather(*pending, return_exceptions=True))
216
+ except Exception:
217
+ pass
218
+ finally:
219
+ loop.close()
220
+ except Exception as e:
221
+ log.error(f"Event loop error scanning {ip}:{port}: {e}")
222
+ return "Unknown"
@@ -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
  """
@@ -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 pwa_launcher import open_pwa, ChromiumNotFoundError
13
+
14
+
12
15
  from lanscape.core.logger import configure_logging
13
16
  from lanscape.core.runtime_args import parse_args
14
- from lanscape.core.web_browser import open_webapp
15
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,24 @@ def try_check_update():
70
78
  log.warning('Unable to check for updates.')
71
79
 
72
80
 
73
- def open_browser(url: str, wait=2) -> bool:
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
+ # Bind to localhost only for better Chrome compatibility
89
+ run_server(host='127.0.0.1', port=args.ws_port)
90
+ except KeyboardInterrupt:
91
+ log.info('WebSocket server stopped by user')
92
+ except Exception as e:
93
+ log.critical(f'WebSocket server failed: {e}')
94
+ log.debug(traceback.format_exc())
95
+ raise
96
+
97
+
98
+ def open_browser(url: str, wait=2) -> Popen | None:
74
99
  """
75
100
  Open a browser window to the specified
76
101
  url after waiting for the server to start
@@ -78,12 +103,18 @@ def open_browser(url: str, wait=2) -> bool:
78
103
  try:
79
104
  time.sleep(wait)
80
105
  log.info(f'Starting UI - http://127.0.0.1:{args.port}')
81
- return open_webapp(url)
82
-
106
+ return open_pwa(url)
107
+
108
+ except ChromiumNotFoundError:
109
+ success = webbrowser.open(url)
110
+ if success:
111
+ log.warning("Chromium browser not found. Falling back to default web browser.")
112
+ else:
113
+ log.warning(f"Cannot find any web browser. LANScape UI running on {url}")
83
114
  except BaseException:
84
115
  log.debug(traceback.format_exc())
85
116
  log.info(f'Unable to open web browser, server running on {url}')
86
- return False
117
+ return None
87
118
 
88
119
 
89
120
  def start_webserver_ui():
@@ -97,19 +128,16 @@ def start_webserver_ui():
97
128
  # if it was, dont open the browser again
98
129
  log.info('Opening UI as daemon')
99
130
  if not IS_FLASK_RELOAD:
100
- threading.Thread(
101
- target=open_browser,
102
- args=(uri,),
103
- daemon=True
104
- ).start()
131
+ open_browser(uri)
105
132
  start_webserver(args)
106
133
  else:
107
134
  flask_thread = start_webserver_daemon(args)
108
- app_closed = open_browser(uri)
135
+ proc = open_browser(uri)
136
+ if proc:
137
+ app_closed = proc.wait()
138
+ else:
139
+ app_closed = False
109
140
 
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
141
  if not app_closed or args.persistent:
114
142
  # not doing a direct join so i can still
115
143
  # terminate the app with ctrl+c
@@ -66,13 +66,16 @@ body:has(.submodule) footer {
66
66
 
67
67
  #header {
68
68
  background-color: var(--primary-bg);
69
- padding: 8px 20px;
69
+ padding: 8px 2%;
70
70
  margin: 0;
71
71
  display: block;
72
72
  position: relative;
73
73
  box-shadow: 0 0 10px var(--box-shadow);
74
74
  width: 100vw;
75
75
  }
76
+ #header .title {
77
+ font-size: 36px;
78
+ }
76
79
 
77
80
  footer {
78
81
  position: sticky;
@@ -163,7 +166,12 @@ details {
163
166
 
164
167
 
165
168
  #scan-form {
166
- width: 500px;
169
+ width: 60vw;
170
+ margin: 0;
171
+ min-width: 300px;
172
+ max-width: 700px;
173
+ }
174
+ #scan-form .form-group {
167
175
  margin: 0;
168
176
  }
169
177
  #scan-form label {
@@ -304,6 +312,9 @@ details {
304
312
  }
305
313
  #advanced-modal label {
306
314
  font-size: 12px;
315
+ text-overflow: ellipsis;
316
+ overflow: hidden;
317
+ white-space: nowrap;
307
318
  }
308
319
  #advanced-modal .form-check {
309
320
  width: fit-content;
@@ -447,8 +458,10 @@ button {
447
458
 
448
459
  #scan-form #scan-submit {
449
460
  border: none;
450
- padding: 10px 20px;
451
- margin-top: 15px;
461
+ padding: 10px 3%;
462
+ margin: 0 15px;
463
+ min-width: 55px;
464
+ height: 42px;
452
465
  }
453
466
 
454
467
  /* Button Styling */
@@ -564,8 +577,8 @@ input[type="range"] {
564
577
  /* Service Strategy Select Styles */
565
578
  .service-strategy-wrapper {
566
579
  position: relative;
567
- display: inline-block;
568
- width: 60%; /* Narrower than port list */
580
+ display: block;
581
+ width: 100%;
569
582
  }
570
583
 
571
584
  .service-strategy {
@@ -657,10 +670,6 @@ input[type="range"] {
657
670
  .table thead tr th.detail-col
658
671
  {
659
672
  width: 30px;
660
- /*
661
- background-color: var(--body-bg);
662
- border: 1px solid var(--text-almost-hidden);
663
- */
664
673
  }
665
674
  .table td:has(.info-icon-container) {
666
675
  width: 30px;
@@ -709,6 +718,12 @@ input[type="range"] {
709
718
  span.alt {
710
719
  color: var(--text-accent-color);
711
720
  }
721
+ span.no-wrap {
722
+ white-space: nowrap;
723
+ overflow: hidden;
724
+ text-overflow: ellipsis;
725
+ display: block;
726
+ }
712
727
  .colorful-buttons a{
713
728
  margin:2px;
714
729
  color: var(--text-color);
@@ -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
  }
@@ -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() {
@@ -139,15 +139,26 @@ function getScanConfig() {
139
139
  }
140
140
 
141
141
  function getPortLists(callback=null) {
142
- $.get('/api/port/list', function(data) {
143
- const customSelectDropdown = $('#port_list');
142
+ const customSelectDropdown = $('#port_list');
143
+
144
+ const renderOptions = (items) => {
144
145
  customSelectDropdown.empty();
145
-
146
- // Populate the dropdown with the options
147
- data.forEach(function(portList) {
148
- customSelectDropdown.append('<option>' + portList + '</option>');
146
+ items.forEach((item) => {
147
+ const name = item.name || item;
148
+ const count = item.count;
149
+ const label = count !== undefined ? `${name} (${count} ports)` : name;
150
+ customSelectDropdown.append(`<option value="${name}">${label}</option>`);
149
151
  });
152
+ };
153
+
154
+ $.get('/api/port/list/summary', function(data) {
155
+ renderOptions(data || []);
150
156
  if (callback) callback();
157
+ }).fail(function() {
158
+ $.get('/api/port/list', function(data) {
159
+ renderOptions(data || []);
160
+ if (callback) callback();
161
+ });
151
162
  });
152
163
  }
153
164
 
@@ -3,45 +3,43 @@
3
3
  {% block content %}
4
4
  <div id="header">
5
5
  <!-- Header and Scan Submission Inline -->
6
- <div class="d-flex justify-content-between align-items-center flex-nowrap">
6
+ <div class="d-flex justify-content-between align-items-center flex-wrap">
7
7
  <a href="/" class="text-decoration-none" aria-label="Go to homepage">
8
8
  <h1 class="title">
9
9
  <span>LAN</span>scape
10
10
  </h1>
11
11
  </a>
12
12
  <!-- Right side: settings + form -->
13
- <div class="d-flex align-items-center justify-content-end ms-auto">
14
- <form id="scan-form" class="d-flex align-items-center">
15
- <div class="form-group me-2">
16
- <!-- Above subnet input -->
17
- <div class="label-container">
18
- <label for="subnet">Subnet:</label>
19
- <div id="subnet-info"></div>
20
- </div>
21
- <!-- Subnet input with dropdown -->
22
- <div class="input-group">
23
- <button
24
- type="button"
25
- id="settings-btn"
26
- class="btn btn-secondary start"
27
- data-bs-toggle="tooltip"
28
- data-bs-placement="bottom"
29
- title="Advanced scan settings"
30
- >
31
- <i class="fa-solid fa-gear"></i>
32
- </button>
33
- <input type="text" id="subnet" name="subnet" class="form-control" value="{{ subnet }}" placeholder="Enter subnet">
34
- <button class="btn btn-secondary dropdown-toggle end" type="button" id="subnet-dropdown" data-bs-toggle="dropdown" aria-expanded="false"></button>
35
- <ul class="dropdown-menu" aria-labelledby="subnet-dropdown" id="dropdown-list">
36
- {% for subnet_option in alternate_subnets %}
37
- <li><a class="dropdown-item" href="#">{{ subnet_option['subnet'] }}</a></li>
38
- {% endfor %}
39
- </ul>
40
- </div>
13
+ <form id="scan-form" class="d-flex align-items-end">
14
+ <div class="form-group me-2">
15
+ <!-- Above subnet input -->
16
+ <div class="label-container">
17
+ <label for="subnet">Subnet:</label>
18
+ <div id="subnet-info"></div>
41
19
  </div>
42
- <button type="submit" id="scan-submit" class="btn btn-primary">Scan</button>
43
- </form>
44
- </div>
20
+ <!-- Subnet input with dropdown -->
21
+ <div class="input-group">
22
+ <button
23
+ type="button"
24
+ id="settings-btn"
25
+ class="btn btn-secondary start"
26
+ data-bs-toggle="tooltip"
27
+ data-bs-placement="bottom"
28
+ title="Advanced scan settings"
29
+ >
30
+ <i class="fa-solid fa-gear"></i>
31
+ </button>
32
+ <input type="text" id="subnet" name="subnet" class="form-control" value="{{ subnet }}" placeholder="Enter subnet">
33
+ <button class="btn btn-secondary dropdown-toggle end" type="button" id="subnet-dropdown" data-bs-toggle="dropdown" aria-expanded="false"></button>
34
+ <ul class="dropdown-menu" aria-labelledby="subnet-dropdown" id="dropdown-list">
35
+ {% for subnet_option in alternate_subnets %}
36
+ <li><a class="dropdown-item" href="#">{{ subnet_option['subnet'] }}</a></li>
37
+ {% endfor %}
38
+ </ul>
39
+ </div>
40
+ </div>
41
+ <button type="submit" id="scan-submit" class="btn btn-primary">Scan</button>
42
+ </form>
45
43
  </div>
46
44
 
47
45
  <div id="scan-progress-bar"></div>
@@ -94,7 +94,7 @@
94
94
  </div>
95
95
  <div class="col-1 descriptor">=</div>
96
96
  <div class="col-3">
97
- <label for="total-ping-attempts">Max pings per device</label>
97
+ <label for="total-ping-attempts" id="tpa-label">Max pings per device</label>
98
98
  <input type="number" id="total-ping-attempts" class="form-control" readonly>
99
99
  </div>
100
100
  </div>
@@ -201,7 +201,7 @@
201
201
  <div id="section-service-scan" class="form-group mt-2 config-section div-hide">
202
202
  <h6>Service Scanning</h6>
203
203
  <div class="row">
204
- <div class="col">
204
+ <div class="col-12">
205
205
  <label for="service_lookup_type" class="form-label">Strategy</label>
206
206
  <div class="service-strategy-wrapper">
207
207
  <select id="service_lookup_type" class="service-strategy">
@@ -211,11 +211,13 @@
211
211
  </select>
212
212
  </div>
213
213
  </div>
214
- <div class="col">
214
+ </div>
215
+ <div class="row mt-2">
216
+ <div class="col-12 col-md-6">
215
217
  <label for="service_timeout" class="form-label">Timeout (sec)</label>
216
218
  <input type="number" step="0.1" id="service_timeout" class="form-control">
217
219
  </div>
218
- <div class="col">
220
+ <div class="col-12 col-md-6 mt-2 mt-md-0">
219
221
  <label for="service_max_concurrent_probes" class="form-label">Max Concurrent Probes</label>
220
222
  <input type="number" id="service_max_concurrent_probes" class="form-control">
221
223
  </div>
@@ -7,20 +7,20 @@
7
7
  ></i>
8
8
  </div>
9
9
  </td>
10
- <td>
10
+ <td class="ip">
11
11
  <div>{{ device.ip }}</div>
12
12
  {% if device.hostname %}
13
- <div><span class=alt>{{device.hostname}}</span></div>
13
+ <span class="alt no-wrap">{{device.hostname}}</span>
14
14
  {% endif %}
15
15
  </td>
16
- <td>
16
+ <td class="mac">
17
17
  <div>{{ device.mac_addr or 'Unknown' }}</div>
18
18
  {% if device.manufacturer %}
19
- <div><span class="alt">{{device.manufacturer}}</span></div>
19
+ <span class="alt no-wrap">{{device.manufacturer}}</span>
20
20
  {% endif %}
21
21
  </td>
22
- <td>{{ device.ports | join(", ") }}</td>
23
- <td>
22
+ <td class="ports">{{ device.ports | join(", ") }}</td>
23
+ <td class="stage">
24
24
  {% if device.stage == 'complete' %}
25
25
  <span class="badge badge-success">complete</span>
26
26
  {% elif device.stage == 'found' %}
@@ -9,10 +9,10 @@
9
9
  <thead>
10
10
  <tr>
11
11
  <th class="detail-col" scope="col"></th>
12
- <th scope="col">IP / <span class="alt">Host</span></th>
13
- <th scope="col">MAC / <span class="alt">Manufacturer</span></th>
14
- <th scope="col">Open Ports</th>
15
- <th scope="col">Stage</th>
12
+ <th class="ip" scope="col">IP / <span class="alt">Host</span></th>
13
+ <th class="mac" scope="col">MAC / <span class="alt">Manufacturer</span></th>
14
+ <th class="ports" scope="col">Open Ports</th>
15
+ <th class="stage" scope="col">Stage</th>
16
16
  </tr>
17
17
  </thead>
18
18
  <tbody>
@@ -0,0 +1,31 @@
1
+ """
2
+ WebSocket interface for LANscape.
3
+
4
+ Provides a standalone WebSocket server that exposes all LANscape functionality,
5
+ allowing clients to initiate scans, manage port lists, and receive real-time
6
+ scan results with delta updates.
7
+ """
8
+
9
+ from lanscape.ui.ws.server import WebSocketServer, run_server
10
+ from lanscape.ui.ws.protocol import (
11
+ WSMessage,
12
+ WSRequest,
13
+ WSResponse,
14
+ WSError,
15
+ WSEvent,
16
+ MessageType
17
+ )
18
+ from lanscape.ui.ws.delta import DeltaTracker, ScanDeltaTracker
19
+
20
+ __all__ = [
21
+ 'WebSocketServer',
22
+ 'run_server',
23
+ 'WSMessage',
24
+ 'WSRequest',
25
+ 'WSResponse',
26
+ 'WSError',
27
+ 'WSEvent',
28
+ 'MessageType',
29
+ 'DeltaTracker',
30
+ 'ScanDeltaTracker'
31
+ ]