From 9d288b743009b8402450ae3f6fedce3384ba481c Mon Sep 17 00:00:00 2001 From: j Date: Mon, 9 Mar 2026 18:48:08 +1300 Subject: [PATCH] Add global notes/links overview modals and fix SSE drain race condition --- app/__pycache__/app.cpython-312.pyc | Bin 37341 -> 38400 bytes app/app.py | 73 ++++++++++++++++++---------- app/static/style.css | 16 ++++++ app/templates/index.html | 59 ++++++++++++++++++++++ 4 files changed, 121 insertions(+), 27 deletions(-) diff --git a/app/__pycache__/app.cpython-312.pyc b/app/__pycache__/app.cpython-312.pyc index 165684498cdd997abbd6fbaecae74fc10e1677e7..fcb753119fbb136cae9f5a65bf62cbd91a78450d 100644 GIT binary patch delta 6079 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 Le4t2;vC97eN~8o& delta 5226 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 diff --git a/app/app.py b/app/app.py index 83dc115..fc331af 100644 --- a/app/app.py +++ b/app/app.py @@ -240,12 +240,6 @@ def collect_all(): # Update database (all in main collector thread) with app.app_context(): - # Remove servers no longer in config - config_keys = {(e['username'], e['hostname']) for e in entries} - for server in Server.query.all(): - if (server.username, server.hostname) not in config_keys: - db.session.delete(server) - for key, (entry, result) in results.items(): server = Server.query.filter_by( username=entry['username'], @@ -289,12 +283,23 @@ def collector_loop(): @app.route('/') def index(): + # Get group order from config file and filter to configured servers only + config_entries = parse_infrastructure_conf() + config_keys = {(e['username'], e['hostname']) for e in config_entries} + group_order = [] + for e in config_entries: + g = e['group'] + if g not in group_order: + group_order.append(g) + all_servers = Server.query.order_by(Server.group_name, Server.primary_ip).all() - # Separate parents and children + # Separate parents and children (only servers in current config) children_map = {} # parent_hostname -> [child_servers] parents = [] for s in all_servers: + if (s.username, s.hostname) not in config_keys: + continue if s.parent_hostname: children_map.setdefault(s.parent_hostname, []).append(s) else: @@ -304,14 +309,6 @@ def index(): for hostname in children_map: children_map[hostname].sort(key=lambda s: _ip_sort_key(s.primary_ip)) - # Get group order from config file - config_entries = parse_infrastructure_conf() - group_order = [] - for e in config_entries: - g = e['group'] - if g not in group_order: - group_order.append(g) - # Group parents, preserving config order groups = {} for s in parents: @@ -430,11 +427,6 @@ def api_refresh_stream(): # Update database with app.app_context(): - config_keys = {(e['username'], e['hostname']) for e in entries} - for server in Server.query.all(): - if (server.username, server.hostname) not in config_keys: - db.session.delete(server) - for key, (entry, result) in results.items(): server = Server.query.filter_by( username=entry['username'], hostname=entry['hostname'], @@ -530,19 +522,19 @@ def api_refresh_one_stream(server_id): t.start() # Stream progress while collecting - while not host_done.is_set(): + result = None + while not host_done.is_set() or not progress_q.empty(): try: kind, val = progress_q.get(timeout=0.3) if kind == 'progress': yield f"data: {val}\n\n" elif kind == 'result': - break + result = val except _queue.Empty: continue t.join() - # Drain remaining messages - result = None + # Drain anything remaining while not progress_q.empty(): kind, val = progress_q.get_nowait() if kind == 'progress': @@ -592,18 +584,18 @@ def api_refresh_one_stream(server_id): ct = threading.Thread(target=_collect_child) ct.start() - while not child_done.is_set(): + child_result = None + while not child_done.is_set() or not child_q.empty(): try: kind, val = child_q.get(timeout=0.3) if kind == 'progress': yield f"data: {val}\n\n" elif kind == 'result': - break + child_result = val except _queue.Empty: continue ct.join() - child_result = None while not child_q.empty(): kind, val = child_q.get_nowait() if kind == 'progress': @@ -655,6 +647,33 @@ def api_update_links(server_id): return jsonify({'ok': True}) +@app.route('/api/all-notes') +def api_all_notes(): + servers = Server.query.filter(Server.notes != '', Server.notes != None).all() + return jsonify([{ + 'id': s.id, + 'hostname': s.hostname, + 'group_name': s.group_name, + 'notes': s.notes, + } for s in servers]) + + +@app.route('/api/all-links') +def api_all_links(): + servers = Server.query.filter(Server.links != None, Server.links != '[]').all() + result = [] + for s in servers: + links = s.links or [] + if links: + result.append({ + 'id': s.id, + 'hostname': s.hostname, + 'group_name': s.group_name, + 'links': links, + }) + return jsonify(result) + + def _ip_sort_key(ip_str): if not ip_str: return [999, 999, 999, 999] diff --git a/app/static/style.css b/app/static/style.css index 02b32ed..5cfa83c 100644 --- a/app/static/style.css +++ b/app/static/style.css @@ -31,6 +31,22 @@ header h1 { color: #64748b; } +.header-btn { + background: #334155; + color: #94a3b8; + border: 1px solid #475569; + border-radius: 6px; + padding: 4px 12px; + font-size: 0.8rem; + cursor: pointer; + transition: all 0.2s; +} + +.header-btn:hover { + background: #475569; + color: #e2e8f0; +} + .refresh-btn { margin-left: auto; background: #334155; diff --git a/app/templates/index.html b/app/templates/index.html index 7ec0a70..e5691f5 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -10,6 +10,8 @@

Infrastructure Map

Auto-refreshes every 60s | Built: {{ build_date }} + +
@@ -502,7 +504,64 @@ + + +