Add manual refresh buttons for all servers and individual servers
All checks were successful
Build-Publish / build (linux/amd64) (push) Successful in 4s
Build-Publish / build (linux/arm64) (push) Successful in 12s
Build-Publish / create-manifest (push) Successful in 1s
Build-Publish / publish-template (push) Successful in 8s

This commit is contained in:
j
2026-03-08 16:31:03 +13:00
parent 939a79a91e
commit 4428ed60c4
3 changed files with 118 additions and 1 deletions

View File

@@ -248,6 +248,12 @@ def collect_all():
logger.info("Collection complete, updated %d servers", len(results)) logger.info("Collection complete, updated %d servers", len(results))
_collect_event = threading.Event()
def trigger_collect():
"""Wake up the collector loop to run immediately."""
_collect_event.set()
def collector_loop(): def collector_loop():
time.sleep(2) # Brief pause to let Flask start time.sleep(2) # Brief pause to let Flask start
while True: while True:
@@ -255,7 +261,8 @@ def collector_loop():
collect_all() collect_all()
except Exception as e: except Exception as e:
logger.error("Collection loop error: %s", e) logger.error("Collection loop error: %s", e)
time.sleep(COLLECTION_INTERVAL) _collect_event.wait(timeout=COLLECTION_INTERVAL)
_collect_event.clear()
# --- Web Routes --- # --- Web Routes ---
@@ -297,6 +304,54 @@ def api_servers():
return jsonify(result) return jsonify(result)
@app.route('/api/refresh', methods=['POST'])
def api_refresh():
trigger_collect()
return jsonify({'ok': True, 'message': 'Collection triggered'})
@app.route('/api/servers/<int:server_id>/refresh', methods=['POST'])
def api_refresh_one(server_id):
server = Server.query.get_or_404(server_id)
try:
ssh_key = load_ssh_key()
except Exception as e:
return jsonify({'ok': False, 'error': str(e)}), 500
entry = {
'group': server.group_name,
'username': server.username,
'hostname': server.hostname,
'url': server.url,
}
result = collect_one(entry, ssh_key)
server.is_online = result.get('is_online', False)
server.last_collected = datetime.now(timezone.utc)
server.details = result
# Extract primary IP
default_iface = ''
routing = result.get('routing', {})
if isinstance(routing, dict):
default_iface = routing.get('interface', '')
primary_ip = ''
for iface in result.get('net', []):
ipv4 = iface.get('ipv4', '')
if not ipv4 or ipv4.startswith('127.'):
continue
iface_name = iface.get('name', '') or iface.get('_name', '')
if iface_name == default_iface:
primary_ip = ipv4
break
if not primary_ip:
primary_ip = ipv4
server.primary_ip = primary_ip
db.session.commit()
return jsonify({'ok': True})
@app.route('/api/servers/<int:server_id>/notes', methods=['PUT']) @app.route('/api/servers/<int:server_id>/notes', methods=['PUT'])
def api_update_notes(server_id): def api_update_notes(server_id):
from flask import request from flask import request

View File

@@ -31,6 +31,50 @@ header h1 {
color: #64748b; color: #64748b;
} }
.refresh-btn {
margin-left: auto;
background: #334155;
color: #94a3b8;
border: 1px solid #475569;
border-radius: 6px;
padding: 4px 12px;
font-size: 0.8rem;
cursor: pointer;
transition: all 0.2s;
}
.refresh-btn:hover {
background: #3b82f6;
color: #fff;
border-color: #3b82f6;
}
.refresh-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.refresh-btn-sm {
margin-left: auto;
background: none;
color: #475569;
border: none;
font-size: 0.85rem;
cursor: pointer;
padding: 0 4px;
transition: color 0.2s;
flex-shrink: 0;
}
.refresh-btn-sm:hover {
color: #3b82f6;
}
.refresh-btn-sm:disabled {
opacity: 0.4;
cursor: not-allowed;
}
main { main {
max-width: 1400px; max-width: 1400px;
margin: 0 auto; margin: 0 auto;

View File

@@ -10,6 +10,7 @@
<header> <header>
<h1>Infrastructure Map</h1> <h1>Infrastructure Map</h1>
<span class="subtitle">Auto-refreshes every 60s | Built: {{ build_date }}</span> <span class="subtitle">Auto-refreshes every 60s | Built: {{ build_date }}</span>
<button class="refresh-btn" onclick="triggerRefresh(this)" title="Force re-collect from all servers">&#x21bb; Refresh</button>
</header> </header>
<main> <main>
@@ -52,6 +53,7 @@
{% if server.url %} {% if server.url %}
<a href="{{ server.url }}" class="server-link" target="_blank" rel="noopener" onclick="event.stopPropagation();" title="{{ server.url }}">&#x2197;</a> <a href="{{ server.url }}" class="server-link" target="_blank" rel="noopener" onclick="event.stopPropagation();" title="{{ server.url }}">&#x2197;</a>
{% endif %} {% endif %}
<button class="refresh-btn-sm" onclick="event.stopPropagation(); refreshServer(this, {{ server.id }})" title="Refresh this server">&#x21bb;</button>
</div> </div>
<div class="server-ip">{{ server.primary_ip or 'No IP' }}</div> <div class="server-ip">{{ server.primary_ip or 'No IP' }}</div>
<div class="server-os">{% if sys.get('platform') %}{{ sys.get('platform')|capitalize }} {{ sys.get('platform_version', '') }} / {% endif %}{{ sys.get('os_pretty', '') }}</div> <div class="server-os">{% if sys.get('platform') %}{{ sys.get('platform')|capitalize }} {{ sys.get('platform_version', '') }} / {% endif %}{{ sys.get('os_pretty', '') }}</div>
@@ -411,6 +413,22 @@
if (card) toggleDetails(card); if (card) toggleDetails(card);
} }
function refreshServer(btn, serverId) {
btn.disabled = true;
btn.textContent = '...';
fetch('/api/servers/' + serverId + '/refresh', {method: 'POST'})
.then(() => location.reload());
}
function triggerRefresh(btn) {
btn.disabled = true;
btn.textContent = 'Collecting...';
fetch('/api/refresh', {method: 'POST'}).then(() => {
// Wait a few seconds for collection to finish, then reload
setTimeout(() => location.reload(), 8000);
});
}
// If no servers have data yet, refresh quickly; otherwise every 60s // If no servers have data yet, refresh quickly; otherwise every 60s
const hasData = document.querySelectorAll('.server-card').length > 0; const hasData = document.querySelectorAll('.server-card').length > 0;
const refreshMs = hasData ? 60000 : 5000; const refreshMs = hasData ? 60000 : 5000;