No One Can Stop Me Now

This commit is contained in:
DigiJ
2026-03-13 23:48:47 -07:00
parent 4d3570781e
commit 1a138a2bd0
428 changed files with 519668 additions and 259 deletions

View File

@@ -48,11 +48,7 @@ def index():
@defense_bp.route('/linux')
@login_required
def linux_index():
from core.menu import MainMenu
menu = MainMenu()
menu.load_modules()
modules = {k: v for k, v in menu.modules.items() if v.category == 'defense'}
return render_template('defense_linux.html', modules=modules)
return render_template('defense_linux.html')
@defense_bp.route('/linux/audit', methods=['POST'])

View File

@@ -1,13 +1,16 @@
"""Hack Hijack — web routes for scanning and taking over compromised systems."""
import json
import queue
import threading
import time
import uuid
from flask import Blueprint, render_template, request, jsonify, Response
from web.auth import login_required
hack_hijack_bp = Blueprint('hack_hijack', __name__)
# Running scans keyed by job_id
# job_id -> {'q': Queue, 'result': dict|None, 'error': str|None, 'done': bool, 'cancel': bool}
_running_scans: dict = {}
@@ -37,34 +40,86 @@ def start_scan():
if not target:
return jsonify({'ok': False, 'error': 'Target IP required'})
# Validate scan type
if scan_type not in ('quick', 'full', 'nmap', 'custom'):
scan_type = 'quick'
job_id = str(uuid.uuid4())[:8]
result_holder = {'result': None, 'error': None, 'done': False}
_running_scans[job_id] = result_holder
q = queue.Queue()
job = {'q': q, 'result': None, 'error': None, 'done': False, 'cancel': False}
_running_scans[job_id] = job
def _push(evt_type, **kw):
kw['type'] = evt_type
kw['ts'] = time.time()
q.put(kw)
def do_scan():
try:
svc = _svc()
# Build a progress callback that feeds the queue
def progress_cb(current, total, message=''):
_push('progress', current=current, total=total,
pct=round(current * 100 / total) if total else 0,
msg=message)
def port_found_cb(port_info):
_push('port_found',
port=port_info.get('port') or (port_info.port if hasattr(port_info, 'port') else 0),
service=getattr(port_info, 'service', port_info.get('service', '')),
banner=getattr(port_info, 'banner', port_info.get('banner', ''))[:80])
def status_cb(msg):
_push('status', msg=msg)
r = svc.scan_target(
target,
scan_type=scan_type,
custom_ports=custom_ports,
timeout=3.0,
progress_cb=progress_cb,
port_found_cb=port_found_cb,
status_cb=status_cb,
)
result_holder['result'] = r.to_dict()
job['result'] = r.to_dict()
except Exception as e:
result_holder['error'] = str(e)
job['error'] = str(e)
_push('error', msg=str(e))
finally:
result_holder['done'] = True
job['done'] = True
_push('done', ok=job['error'] is None)
threading.Thread(target=do_scan, daemon=True).start()
return jsonify({'ok': True, 'job_id': job_id,
'message': f'Scan started on {target} ({scan_type})'})
@hack_hijack_bp.route('/hack-hijack/scan/<job_id>/stream')
@login_required
def scan_stream(job_id):
"""SSE stream for live scan progress."""
job = _running_scans.get(job_id)
if not job:
def _err():
yield f"data: {json.dumps({'type': 'error', 'msg': 'Job not found'})}\n\n"
return Response(_err(), mimetype='text/event-stream')
def generate():
q = job['q']
while True:
try:
item = q.get(timeout=0.5)
yield f"data: {json.dumps(item)}\n\n"
if item.get('type') == 'done':
break
except queue.Empty:
if job['done']:
break
yield ': keepalive\n\n'
return Response(generate(), mimetype='text/event-stream',
headers={'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no'})
@hack_hijack_bp.route('/hack-hijack/scan/<job_id>', methods=['GET'])
@login_required
def scan_status(job_id):
@@ -75,7 +130,6 @@ def scan_status(job_id):
return jsonify({'ok': True, 'done': False, 'message': 'Scan in progress...'})
if holder['error']:
return jsonify({'ok': False, 'error': holder['error'], 'done': True})
# Clean up
_running_scans.pop(job_id, None)
return jsonify({'ok': True, 'done': True, 'result': holder['result']})

473
web/routes/port_scanner.py Normal file
View File

@@ -0,0 +1,473 @@
"""Advanced Port Scanner — streaming SSE-based port scanner with nmap integration."""
import json
import queue
import socket
import subprocess
import threading
import time
import uuid
from datetime import datetime, timezone
from typing import Optional
from flask import Blueprint, render_template, request, jsonify, Response
from web.auth import login_required
port_scanner_bp = Blueprint('port_scanner', __name__, url_prefix='/port-scanner')
# job_id -> {'q': Queue, 'result': dict|None, 'done': bool, 'cancel': bool}
_jobs: dict = {}
# ── Common port lists ──────────────────────────────────────────────────────────
QUICK_PORTS = [
21, 22, 23, 25, 53, 80, 110, 111, 135, 139, 143, 443, 445, 993, 995,
1723, 3306, 3389, 5900, 8080, 8443, 8888,
]
COMMON_PORTS = [
20, 21, 22, 23, 25, 53, 67, 68, 69, 80, 88, 110, 111, 119, 123, 135,
137, 138, 139, 143, 161, 162, 179, 194, 389, 443, 445, 465, 500, 514,
515, 587, 593, 631, 636, 873, 902, 993, 995, 1080, 1194, 1433, 1521,
1723, 1883, 2049, 2121, 2181, 2222, 2375, 2376, 2483, 2484, 3000, 3306,
3389, 3690, 4000, 4040, 4333, 4444, 4567, 4899, 5000, 5432, 5601, 5672,
5900, 5984, 6000, 6379, 6443, 6881, 7000, 7001, 7080, 7443, 7474, 8000,
8001, 8008, 8080, 8081, 8082, 8083, 8088, 8089, 8161, 8333, 8443, 8444,
8500, 8888, 8983, 9000, 9001, 9042, 9090, 9092, 9200, 9300, 9418, 9999,
10000, 11211, 15432, 15672, 27017, 27018, 27019, 28017, 50000, 54321,
]
SERVICE_MAP = {
20: 'FTP-data', 21: 'FTP', 22: 'SSH', 23: 'Telnet', 25: 'SMTP',
53: 'DNS', 67: 'DHCP', 68: 'DHCP', 69: 'TFTP', 80: 'HTTP',
88: 'Kerberos', 110: 'POP3', 111: 'RPC', 119: 'NNTP', 123: 'NTP',
135: 'MS-RPC', 137: 'NetBIOS-NS', 138: 'NetBIOS-DGM', 139: 'NetBIOS-SSN',
143: 'IMAP', 161: 'SNMP', 162: 'SNMP-Trap', 179: 'BGP', 194: 'IRC',
389: 'LDAP', 443: 'HTTPS', 445: 'SMB', 465: 'SMTPS', 500: 'IKE/ISAKMP',
514: 'Syslog/RSH', 515: 'LPD', 587: 'SMTP-Submission', 631: 'IPP',
636: 'LDAPS', 873: 'rsync', 993: 'IMAPS', 995: 'POP3S',
1080: 'SOCKS', 1194: 'OpenVPN', 1433: 'MSSQL', 1521: 'Oracle',
1723: 'PPTP', 1883: 'MQTT', 2049: 'NFS', 2181: 'Zookeeper',
2222: 'SSH-alt', 2375: 'Docker', 2376: 'Docker-TLS', 3000: 'Grafana',
3306: 'MySQL', 3389: 'RDP', 3690: 'SVN', 4444: 'Meterpreter',
5000: 'Flask/Dev', 5432: 'PostgreSQL', 5601: 'Kibana', 5672: 'AMQP/RabbitMQ',
5900: 'VNC', 5984: 'CouchDB', 6379: 'Redis', 6443: 'Kubernetes-API',
7474: 'Neo4j', 8080: 'HTTP-Alt', 8443: 'HTTPS-Alt', 8500: 'Consul',
8888: 'Jupyter/HTTP-Alt', 9000: 'SonarQube/PHP-FPM', 9001: 'Tor/Supervisor',
9042: 'Cassandra', 9090: 'Prometheus/Cockpit', 9092: 'Kafka',
9200: 'Elasticsearch', 9300: 'Elasticsearch-node', 9418: 'Git',
10000: 'Webmin', 11211: 'Memcached', 15672: 'RabbitMQ-Mgmt',
27017: 'MongoDB', 27018: 'MongoDB', 27019: 'MongoDB', 50000: 'DB2',
}
PROBE_MAP = {
21: b'',
22: b'',
23: b'',
25: b'',
80: b'HEAD / HTTP/1.0\r\nHost: localhost\r\n\r\n',
110: b'',
143: b'',
443: b'',
3306: b'',
5432: b'',
6379: b'INFO\r\n',
8080: b'HEAD / HTTP/1.0\r\nHost: localhost\r\n\r\n',
8443: b'',
8888: b'HEAD / HTTP/1.0\r\nHost: localhost\r\n\r\n',
9200: b'GET / HTTP/1.0\r\nHost: localhost\r\n\r\n',
27017: b'',
}
def _push(q: queue.Queue, event_type: str, data: dict) -> None:
data['type'] = event_type
data['ts'] = time.time()
q.put(data)
def _grab_banner(sock: socket.socket, port: int, timeout: float = 2.0) -> str:
try:
sock.settimeout(timeout)
probe = PROBE_MAP.get(port, b'')
if probe:
sock.sendall(probe)
raw = sock.recv(2048)
return raw.decode('utf-8', errors='replace').strip()[:512]
except Exception:
return ''
def _identify_service(port: int, banner: str) -> str:
bl = banner.lower()
if 'ssh-' in bl:
return 'SSH'
if 'ftp' in bl or '220 ' in bl[:10]:
return 'FTP'
if 'smtp' in bl or ('220 ' in bl and 'mail' in bl):
return 'SMTP'
if 'http/' in bl or '<html' in bl or '<!doctype' in bl:
return 'HTTP'
if 'mysql' in bl:
return 'MySQL'
if 'redis' in bl:
return 'Redis'
if 'mongo' in bl:
return 'MongoDB'
if 'postgresql' in bl:
return 'PostgreSQL'
if 'rabbitmq' in bl:
return 'RabbitMQ'
if 'elastic' in bl:
return 'Elasticsearch'
return SERVICE_MAP.get(port, 'unknown')
def _scan_port(host: str, port: int, timeout: float) -> Optional[dict]:
"""TCP connect scan a single port. Returns port info dict or None if closed."""
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(timeout)
err = sock.connect_ex((host, port))
if err == 0:
banner = _grab_banner(sock, port)
sock.close()
service = _identify_service(port, banner)
return {
'port': port,
'protocol': 'tcp',
'state': 'open',
'service': service,
'banner': banner,
}
sock.close()
except Exception:
pass
return None
def _run_nmap_scan(host: str, ports: list, options: dict, q: queue.Queue,
job: dict) -> Optional[list]:
"""Run nmap and parse output. Returns list of port dicts or None if nmap unavailable."""
import shutil
nmap = shutil.which('nmap')
if not nmap:
_push(q, 'warning', {'msg': 'nmap not found — falling back to TCP connect scan'})
return None
port_str = ','.join(str(p) for p in sorted(ports))
cmd = [nmap, '-Pn', '--open', '-p', port_str]
if options.get('service_detection'):
cmd += ['-sV', '--version-intensity', '5']
if options.get('os_detection'):
cmd += ['-O', '--osscan-guess']
if options.get('aggressive'):
cmd += ['-A']
if options.get('timing'):
cmd += [f'-T{options["timing"]}']
else:
cmd += ['-T4']
cmd += ['-oN', '-', '--host-timeout', '120s', host]
_push(q, 'nmap_start', {'cmd': ' '.join(cmd[:-1] + ['<target>'])})
open_ports = []
os_guess = ''
nmap_raw_lines = []
try:
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
text=True, bufsize=1)
for line in proc.stdout:
line = line.rstrip()
if job.get('cancel'):
proc.terminate()
break
nmap_raw_lines.append(line)
_push(q, 'nmap_line', {'line': line})
# Parse open port lines: "80/tcp open http Apache httpd 2.4.54"
stripped = line.strip()
if '/tcp' in stripped or '/udp' in stripped:
parts = stripped.split()
if len(parts) >= 2 and parts[1] == 'open':
port_proto = parts[0].split('/')
port_num = int(port_proto[0])
proto = port_proto[1] if len(port_proto) > 1 else 'tcp'
service = parts[2] if len(parts) > 2 else SERVICE_MAP.get(port_num, 'unknown')
version = ' '.join(parts[3:]) if len(parts) > 3 else ''
open_ports.append({
'port': port_num,
'protocol': proto,
'state': 'open',
'service': service,
'banner': version,
})
_push(q, 'port_open', {
'port': port_num, 'protocol': proto,
'service': service, 'banner': version,
})
# OS detection line
if 'OS details:' in line or 'Running:' in line:
os_guess = line.split(':', 1)[-1].strip()
proc.wait(timeout=10)
except Exception as exc:
_push(q, 'warning', {'msg': f'nmap error: {exc} — falling back to TCP connect scan'})
return None
return open_ports, os_guess, '\n'.join(nmap_raw_lines)
def _socket_scan(host: str, ports: list, timeout: float, concurrency: int,
q: queue.Queue, job: dict) -> list:
"""Concurrent TCP connect scan. Returns list of open port dicts."""
open_ports = []
lock = threading.Lock()
scanned = [0]
total = len(ports)
start = time.time()
def worker(port: int):
if job.get('cancel'):
return
result = _scan_port(host, port, timeout)
with lock:
scanned[0] += 1
done = scanned[0]
if result:
with lock:
open_ports.append(result)
_push(q, 'port_open', {
'port': result['port'], 'protocol': result['protocol'],
'service': result['service'], 'banner': result['banner'],
})
# Progress every 25 ports or on first/last
if done == 1 or done % 25 == 0 or done == total:
elapsed = time.time() - start
rate = done / elapsed if elapsed > 0 else 0
eta = int((total - done) / rate) if rate > 0 else 0
_push(q, 'progress', {
'current': done, 'total': total,
'pct': round(done * 100 / total),
'eta': f'{eta}s' if eta < 3600 else f'{eta//60}m',
'port': port,
'open_count': len(open_ports),
})
sem = threading.Semaphore(concurrency)
threads = []
for port in sorted(ports):
if job.get('cancel'):
break
sem.acquire()
def _run(p=port):
try:
worker(p)
finally:
sem.release()
t = threading.Thread(target=_run, daemon=True)
threads.append(t)
t.start()
for t in threads:
t.join(timeout=timeout + 1)
return sorted(open_ports, key=lambda x: x['port'])
def _do_scan(job_id: str, host: str, ports: list, options: dict) -> None:
"""Main scan worker — runs in a background thread."""
job = _jobs[job_id]
q = job['q']
start = time.time()
_push(q, 'start', {
'target': host, 'total_ports': len(ports),
'mode': options.get('mode', 'custom'),
})
# Resolve hostname
ip = host
try:
ip = socket.gethostbyname(host)
if ip != host:
_push(q, 'info', {'msg': f'Resolved {host}{ip}'})
except Exception:
_push(q, 'warning', {'msg': f'Could not resolve {host} — using as-is'})
open_ports = []
os_guess = ''
nmap_raw = ''
try:
use_nmap = options.get('use_nmap', False)
timeout = float(options.get('timeout', 1.0))
concurrency = int(options.get('concurrency', 100))
if use_nmap:
nmap_result = _run_nmap_scan(ip, ports, options, q, job)
if nmap_result is not None:
open_ports, os_guess, nmap_raw = nmap_result
else:
# fallback
_push(q, 'info', {'msg': 'Running TCP connect scan fallback...'})
open_ports = _socket_scan(ip, ports, timeout, concurrency, q, job)
else:
_push(q, 'info', {'msg': f'Scanning {len(ports)} ports on {ip} '
f'(concurrency={concurrency}, timeout={timeout}s)'})
open_ports = _socket_scan(ip, ports, timeout, concurrency, q, job)
duration = round(time.time() - start, 2)
result = {
'target': host,
'ip': ip,
'scan_time': datetime.now(timezone.utc).isoformat(),
'duration': duration,
'ports_scanned': len(ports),
'open_ports': open_ports,
'os_guess': os_guess,
'nmap_raw': nmap_raw,
'options': options,
}
job['result'] = result
_push(q, 'done', {
'open_count': len(open_ports),
'ports_scanned': len(ports),
'duration': duration,
'os_guess': os_guess,
})
except Exception as exc:
_push(q, 'error', {'msg': str(exc)})
finally:
job['done'] = True
# ── Routes ────────────────────────────────────────────────────────────────────
@port_scanner_bp.route('/')
@login_required
def index():
return render_template('port_scanner.html')
@port_scanner_bp.route('/start', methods=['POST'])
@login_required
def start_scan():
data = request.get_json(silent=True) or {}
host = data.get('target', '').strip()
if not host:
return jsonify({'ok': False, 'error': 'Target required'})
mode = data.get('mode', 'common')
if mode == 'quick':
ports = list(QUICK_PORTS)
elif mode == 'common':
ports = list(COMMON_PORTS)
elif mode == 'full':
ports = list(range(1, 65536))
elif mode == 'custom':
raw = data.get('ports', '').strip()
ports = _parse_port_spec(raw)
if not ports:
return jsonify({'ok': False, 'error': 'No valid ports in custom range'})
else:
ports = list(COMMON_PORTS)
options = {
'mode': mode,
'use_nmap': bool(data.get('use_nmap', False)),
'service_detection': bool(data.get('service_detection', False)),
'os_detection': bool(data.get('os_detection', False)),
'aggressive': bool(data.get('aggressive', False)),
'timing': data.get('timing', '4'),
'timeout': float(data.get('timeout', 1.0)),
'concurrency': min(int(data.get('concurrency', 200)), 500),
}
job_id = str(uuid.uuid4())[:8]
job = {'q': queue.Queue(), 'result': None, 'done': False, 'cancel': False}
_jobs[job_id] = job
t = threading.Thread(target=_do_scan, args=(job_id, host, ports, options), daemon=True)
t.start()
return jsonify({'ok': True, 'job_id': job_id, 'port_count': len(ports)})
@port_scanner_bp.route('/stream/<job_id>')
@login_required
def stream(job_id):
job = _jobs.get(job_id)
if not job:
def err_gen():
yield f"data: {json.dumps({'type': 'error', 'msg': 'Job not found'})}\n\n"
return Response(err_gen(), mimetype='text/event-stream')
def generate():
q = job['q']
while True:
try:
item = q.get(timeout=0.5)
yield f"data: {json.dumps(item)}\n\n"
if item.get('type') in ('done', 'error'):
break
except queue.Empty:
if job['done']:
break
yield ': keepalive\n\n'
return Response(generate(), mimetype='text/event-stream',
headers={'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no'})
@port_scanner_bp.route('/result/<job_id>')
@login_required
def get_result(job_id):
job = _jobs.get(job_id)
if not job:
return jsonify({'ok': False, 'error': 'Job not found'})
if not job['done']:
return jsonify({'ok': True, 'done': False})
return jsonify({'ok': True, 'done': True, 'result': job['result']})
@port_scanner_bp.route('/cancel/<job_id>', methods=['POST'])
@login_required
def cancel_scan(job_id):
job = _jobs.get(job_id)
if job:
job['cancel'] = True
return jsonify({'ok': True})
# ── Helpers ───────────────────────────────────────────────────────────────────
def _parse_port_spec(spec: str) -> list:
"""Parse port specification: '22,80,443', '1-1024', '22,80-100,443'."""
ports = set()
for part in spec.split(','):
part = part.strip()
if '-' in part:
try:
lo, hi = part.split('-', 1)
lo, hi = int(lo.strip()), int(hi.strip())
if 1 <= lo <= hi <= 65535:
ports.update(range(lo, hi + 1))
except ValueError:
pass
else:
try:
p = int(part)
if 1 <= p <= 65535:
ports.add(p)
except ValueError:
pass
return sorted(ports)

View File

@@ -162,6 +162,38 @@ def mms():
return jsonify({'ok': True, 'messages': msgs, 'count': len(msgs)})
@rcs_tools_bp.route('/rcs-via-mms')
@login_required
def rcs_via_mms():
thread_id = request.args.get('thread_id')
tid = int(thread_id) if thread_id else None
limit = int(request.args.get('limit', 200))
msgs = _get_rcs().read_rcs_via_mms(tid, limit)
rcs_count = sum(1 for m in msgs if m.get('is_rcs'))
return jsonify({'ok': True, 'messages': msgs, 'count': len(msgs), 'rcs_count': rcs_count})
@rcs_tools_bp.route('/rcs-only')
@login_required
def rcs_only():
limit = int(request.args.get('limit', 200))
msgs = _get_rcs().read_rcs_only(limit)
return jsonify({'ok': True, 'messages': msgs, 'count': len(msgs)})
@rcs_tools_bp.route('/rcs-threads')
@login_required
def rcs_threads():
threads = _get_rcs().read_rcs_threads()
return jsonify({'ok': True, 'threads': threads, 'count': len(threads)})
@rcs_tools_bp.route('/backup-rcs-xml', methods=['POST'])
@login_required
def backup_rcs_xml():
return jsonify(_get_rcs().backup_rcs_to_xml())
@rcs_tools_bp.route('/drafts')
@login_required
def drafts():

View File

@@ -21,42 +21,83 @@ _debug_enabled: bool = False
_debug_handler_installed: bool = False
def _buf_append(level: str, name: str, raw: str, msg: str, exc: str = '') -> None:
"""Thread-safe append to the debug buffer."""
entry: dict = {'ts': time.time(), 'level': level, 'name': name, 'raw': raw, 'msg': msg}
if exc:
entry['exc'] = exc
_debug_buffer.append(entry)
class _DebugBufferHandler(logging.Handler):
"""Captures log records into the in-memory debug buffer."""
"""Captures ALL log records into the in-memory debug buffer (always active)."""
def emit(self, record: logging.LogRecord) -> None:
if not _debug_enabled:
return
try:
entry: dict = {
'ts': record.created,
'level': record.levelname,
'name': record.name,
'raw': record.getMessage(),
'msg': self.format(record),
}
exc_text = ''
if record.exc_info:
import traceback as _tb
entry['exc'] = ''.join(_tb.format_exception(*record.exc_info))
_debug_buffer.append(entry)
exc_text = ''.join(_tb.format_exception(*record.exc_info))
_buf_append(
level=record.levelname,
name=record.name,
raw=record.getMessage(),
msg=self.format(record),
exc=exc_text,
)
except Exception:
pass
class _PrintCapture:
"""Wraps sys.stdout or sys.stderr — passes through AND feeds lines to the debug buffer."""
def __init__(self, original, level: str = 'STDOUT'):
self._orig = original
self._level = level
self._line_buf = ''
def write(self, text: str) -> int:
self._orig.write(text)
self._line_buf += text
while '\n' in self._line_buf:
line, self._line_buf = self._line_buf.split('\n', 1)
if line.strip():
_buf_append(self._level, 'print', line, line)
return len(text)
def flush(self) -> None:
self._orig.flush()
def __getattr__(self, name):
return getattr(self._orig, name)
def _ensure_debug_handler() -> None:
"""Install logging handler + stdout/stderr capture once, at startup."""
global _debug_handler_installed
if _debug_handler_installed:
return
# Logging handler
handler = _DebugBufferHandler()
handler.setLevel(logging.DEBUG)
handler.setFormatter(logging.Formatter('%(name)s%(message)s'))
root = logging.getLogger()
root.addHandler(handler)
# Lower root level to DEBUG so records reach the handler
if root.level == logging.NOTSET or root.level > logging.DEBUG:
root.setLevel(logging.DEBUG)
# stdout / stderr capture
import sys as _sys
if not isinstance(_sys.stdout, _PrintCapture):
_sys.stdout = _PrintCapture(_sys.stdout, 'STDOUT')
if not isinstance(_sys.stderr, _PrintCapture):
_sys.stderr = _PrintCapture(_sys.stderr, 'STDERR')
_debug_handler_installed = True
# Install immediately so we capture from process start, not just after toggle
_ensure_debug_handler()
settings_bp = Blueprint('settings', __name__, url_prefix='/settings')
@@ -429,28 +470,42 @@ def discovery_stop():
@settings_bp.route('/debug/toggle', methods=['POST'])
@login_required
def debug_toggle():
"""Enable or disable the debug log capture."""
"""Enable or disable the debug console UI (capture always runs)."""
global _debug_enabled
data = request.get_json(silent=True) or {}
_debug_enabled = bool(data.get('enabled', False))
if _debug_enabled:
_ensure_debug_handler()
logging.getLogger('autarch.debug').info('Debug console enabled')
logging.getLogger('autarch.debug').info('Debug console opened')
return jsonify({'ok': True, 'enabled': _debug_enabled})
@settings_bp.route('/debug/stream')
@login_required
def debug_stream():
"""SSE stream — pushes new log records to the browser as they arrive."""
"""SSE stream — pushes log records to the browser as they arrive.
On connect: sends the last 200 buffered entries as history, then streams
new entries live. Handles deque wrap-around correctly.
"""
def generate():
sent = 0
buf = list(_debug_buffer)
# Send last 200 entries as catch-up history
history_start = max(0, len(buf) - 200)
for entry in buf[history_start:]:
yield f"data: {json.dumps(entry)}\n\n"
sent = len(buf)
while True:
time.sleep(0.2)
buf = list(_debug_buffer)
while sent < len(buf):
n = len(buf)
if sent > n:
# deque wrapped; re-orient to current tail
sent = n
while sent < n:
yield f"data: {json.dumps(buf[sent])}\n\n"
sent += 1
time.sleep(0.25)
yield ': keepalive\n\n'
return Response(generate(), mimetype='text/event-stream',
headers={'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no'})