merge watcher into dev #33

Merged
serversdown merged 2 commits from watcher into dev 2026-03-18 14:54:34 -04:00
Showing only changes of commit 76667454b3 - Show all commits

View File

@@ -34,13 +34,13 @@
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<!-- Status dot --> <!-- Status dot -->
{% if agent.status == 'ok' %} {% if agent.status == 'ok' %}
<span class="inline-block w-3 h-3 rounded-full bg-green-500 flex-shrink-0"></span> <span class="status-dot inline-block w-3 h-3 rounded-full bg-green-500 flex-shrink-0"></span>
{% elif agent.status == 'pending' %} {% elif agent.status == 'pending' %}
<span class="inline-block w-3 h-3 rounded-full bg-yellow-400 flex-shrink-0"></span> <span class="status-dot inline-block w-3 h-3 rounded-full bg-yellow-400 flex-shrink-0"></span>
{% elif agent.status in ('missing', 'error') %} {% elif agent.status in ('missing', 'error') %}
<span class="inline-block w-3 h-3 rounded-full bg-red-500 flex-shrink-0"></span> <span class="status-dot inline-block w-3 h-3 rounded-full bg-red-500 flex-shrink-0"></span>
{% else %} {% else %}
<span class="inline-block w-3 h-3 rounded-full bg-gray-400 flex-shrink-0"></span> <span class="status-dot inline-block w-3 h-3 rounded-full bg-gray-400 flex-shrink-0"></span>
{% endif %} {% endif %}
<div> <div>
@@ -60,15 +60,15 @@
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<!-- Status badge --> <!-- Status badge -->
{% if agent.status == 'ok' %} {% if agent.status == 'ok' %}
<span class="px-2.5 py-1 text-xs font-semibold rounded-full bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300">OK</span> <span class="status-badge px-2.5 py-1 text-xs font-semibold rounded-full bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300">OK</span>
{% elif agent.status == 'pending' %} {% elif agent.status == 'pending' %}
<span class="px-2.5 py-1 text-xs font-semibold rounded-full bg-yellow-100 text-yellow-700 dark:bg-yellow-900 dark:text-yellow-300">Pending</span> <span class="status-badge px-2.5 py-1 text-xs font-semibold rounded-full bg-yellow-100 text-yellow-700 dark:bg-yellow-900 dark:text-yellow-300">Pending</span>
{% elif agent.status == 'missing' %} {% elif agent.status == 'missing' %}
<span class="px-2.5 py-1 text-xs font-semibold rounded-full bg-red-100 text-red-700 dark:bg-red-900 dark:text-red-300">Missing</span> <span class="status-badge px-2.5 py-1 text-xs font-semibold rounded-full bg-red-100 text-red-700 dark:bg-red-900 dark:text-red-300">Missing</span>
{% elif agent.status == 'error' %} {% elif agent.status == 'error' %}
<span class="px-2.5 py-1 text-xs font-semibold rounded-full bg-red-100 text-red-700 dark:bg-red-900 dark:text-red-300">Error</span> <span class="status-badge px-2.5 py-1 text-xs font-semibold rounded-full bg-red-100 text-red-700 dark:bg-red-900 dark:text-red-300">Error</span>
{% else %} {% else %}
<span class="px-2.5 py-1 text-xs font-semibold rounded-full bg-gray-100 text-gray-600 dark:bg-slate-700 dark:text-gray-400">Unknown</span> <span class="status-badge px-2.5 py-1 text-xs font-semibold rounded-full bg-gray-100 text-gray-600 dark:bg-slate-700 dark:text-gray-400">Unknown</span>
{% endif %} {% endif %}
<!-- Trigger Update button --> <!-- Trigger Update button -->
@@ -83,10 +83,10 @@
</div> </div>
<!-- Meta row --> <!-- Meta row -->
<div class="px-6 py-3 bg-gray-50 dark:bg-slate-850 border-b border-gray-100 dark:border-slate-700 flex flex-wrap gap-6 text-sm"> <div class="px-6 py-3 bg-gray-50 dark:bg-slate-800 border-b border-gray-100 dark:border-slate-700 flex flex-wrap gap-6 text-sm">
<div> <div>
<span class="text-gray-500 dark:text-gray-400">Last seen</span> <span class="text-gray-500 dark:text-gray-400">Last seen</span>
<span class="ml-2 font-medium text-gray-800 dark:text-gray-200"> <span class="last-seen-value ml-2 font-medium text-gray-800 dark:text-gray-200">
{% if agent.last_seen %} {% if agent.last_seen %}
{{ agent.last_seen }} {{ agent.last_seen }}
{% if agent.age_minutes is not none %} {% if agent.age_minutes is not none %}
@@ -97,14 +97,12 @@
{% endif %} {% endif %}
</span> </span>
</div> </div>
{% if agent.update_pending %} <div class="update-pending-indicator flex items-center gap-1.5 text-yellow-600 dark:text-yellow-400 {% if not agent.update_pending %}hidden{% endif %}">
<div class="flex items-center gap-1.5 text-yellow-600 dark:text-yellow-400">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
</svg> </svg>
<span class="text-xs font-semibold">Update pending — will apply on next heartbeat</span> <span class="text-xs font-semibold">Update pending — will apply on next heartbeat</span>
</div> </div>
{% endif %}
</div> </div>
<!-- Log tail --> <!-- Log tail -->
@@ -112,11 +110,16 @@
<div class="px-6 py-4"> <div class="px-6 py-4">
<div class="flex items-center justify-between mb-2"> <div class="flex items-center justify-between mb-2">
<span class="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wide">Log Tail</span> <span class="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wide">Log Tail</span>
<div class="flex items-center gap-3">
<button onclick="expandLog('{{ agent.id | replace(' ', '-') }}')" id="expand-{{ agent.id | replace(' ', '-') }}" class="text-xs text-gray-400 hover:text-gray-600 dark:hover:text-gray-200">
Expand
</button>
<button onclick="toggleLog('{{ agent.id | replace(' ', '-') }}')" class="text-xs text-gray-400 hover:text-gray-600 dark:hover:text-gray-200"> <button onclick="toggleLog('{{ agent.id | replace(' ', '-') }}')" class="text-xs text-gray-400 hover:text-gray-600 dark:hover:text-gray-200">
Toggle Toggle
</button> </button>
</div> </div>
<pre id="log-{{ agent.id | replace(' ', '-') }}" class="text-xs font-mono bg-gray-900 text-green-400 rounded-lg p-3 overflow-x-auto max-h-48 overflow-y-auto leading-relaxed hidden">{{ agent.log_tail | log_tail_display }}</pre> </div>
<pre id="log-{{ agent.id | replace(' ', '-') }}" class="text-xs font-mono bg-gray-900 text-green-400 rounded-lg p-3 overflow-x-auto max-h-96 overflow-y-auto leading-relaxed hidden">{{ agent.log_tail | log_tail_display }}</pre>
</div> </div>
{% else %} {% else %}
<div class="px-6 py-4 text-xs text-gray-400 dark:text-gray-500 italic">No log data received yet.</div> <div class="px-6 py-4 text-xs text-gray-400 dark:text-gray-500 italic">No log data received yet.</div>
@@ -136,7 +139,8 @@
function triggerUpdate(agentId) { function triggerUpdate(agentId) {
if (!confirm('Trigger update for ' + agentId + '?\n\nThe watcher will self-update on its next heartbeat cycle.')) return; if (!confirm('Trigger update for ' + agentId + '?\n\nThe watcher will self-update on its next heartbeat cycle.')) return;
const btn = document.getElementById('btn-update-' + agentId.replace(/ /g, '-')); const safeId = agentId.replace(/ /g, '-');
const btn = document.getElementById('btn-update-' + safeId);
btn.disabled = true; btn.disabled = true;
btn.textContent = 'Sending...'; btn.textContent = 'Sending...';
@@ -151,7 +155,12 @@ function triggerUpdate(agentId) {
btn.textContent = 'Update Queued'; btn.textContent = 'Update Queued';
btn.classList.remove('bg-seismo-orange', 'hover:bg-orange-600'); btn.classList.remove('bg-seismo-orange', 'hover:bg-orange-600');
btn.classList.add('bg-green-600'); btn.classList.add('bg-green-600');
setTimeout(() => location.reload(), 1500); // Show the pending indicator immediately without a reload
const card = document.getElementById('agent-' + safeId);
if (card) {
const indicator = card.querySelector('.update-pending-indicator');
if (indicator) indicator.classList.remove('hidden');
}
} else { } else {
btn.textContent = 'Error'; btn.textContent = 'Error';
btn.classList.add('bg-red-600'); btn.classList.add('bg-red-600');
@@ -170,7 +179,95 @@ function toggleLog(agentId) {
if (el) el.classList.toggle('hidden'); if (el) el.classList.toggle('hidden');
} }
// Auto-refresh function expandLog(agentId) {
setTimeout(() => location.reload(), 30000); const el = document.getElementById('log-' + agentId);
const btn = document.getElementById('expand-' + agentId);
if (!el) return;
el.classList.remove('hidden');
if (el.classList.contains('max-h-96')) {
el.classList.remove('max-h-96');
el.style.maxHeight = 'none';
if (btn) btn.textContent = 'Collapse';
} else {
el.classList.add('max-h-96');
el.style.maxHeight = '';
if (btn) btn.textContent = 'Expand';
}
}
// Status colors for dot and badge by status value
const STATUS_DOT = {
ok: 'bg-green-500',
pending: 'bg-yellow-400',
missing: 'bg-red-500',
error: 'bg-red-500',
};
const STATUS_BADGE_CLASSES = {
ok: 'bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300',
pending: 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900 dark:text-yellow-300',
missing: 'bg-red-100 text-red-700 dark:bg-red-900 dark:text-red-300',
error: 'bg-red-100 text-red-700 dark:bg-red-900 dark:text-red-300',
};
const STATUS_BADGE_DEFAULT = 'bg-gray-100 text-gray-600 dark:bg-slate-700 dark:text-gray-400';
const DOT_COLORS = ['bg-green-500', 'bg-yellow-400', 'bg-red-500', 'bg-gray-400'];
const BADGE_COLORS = [
'bg-green-100', 'text-green-700', 'dark:bg-green-900', 'dark:text-green-300',
'bg-yellow-100', 'text-yellow-700', 'dark:bg-yellow-900', 'dark:text-yellow-300',
'bg-red-100', 'text-red-700', 'dark:bg-red-900', 'dark:text-red-300',
'bg-gray-100', 'text-gray-600', 'dark:bg-slate-700', 'dark:text-gray-400',
];
function patchAgent(card, agent) {
// Status dot
const dot = card.querySelector('.status-dot');
if (dot) {
dot.classList.remove(...DOT_COLORS);
dot.classList.add(STATUS_DOT[agent.status] || 'bg-gray-400');
}
// Status badge
const badge = card.querySelector('.status-badge');
if (badge) {
badge.classList.remove(...BADGE_COLORS);
const label = agent.status ? agent.status.charAt(0).toUpperCase() + agent.status.slice(1) : 'Unknown';
badge.textContent = label === 'Ok' ? 'OK' : label;
const cls = STATUS_BADGE_CLASSES[agent.status] || STATUS_BADGE_DEFAULT;
badge.classList.add(...cls.split(' '));
}
// Last seen / age
const lastSeen = card.querySelector('.last-seen-value');
if (lastSeen) {
if (agent.last_seen) {
const age = agent.age_minutes != null
? ` <span class="text-gray-400 dark:text-gray-500 font-normal">(${agent.age_minutes}m ago)</span>`
: '';
lastSeen.innerHTML = agent.last_seen + age;
} else {
lastSeen.textContent = 'Never';
}
}
// Update pending indicator
const indicator = card.querySelector('.update-pending-indicator');
if (indicator) {
indicator.classList.toggle('hidden', !agent.update_pending);
}
}
function liveRefresh() {
fetch('/api/admin/watchers')
.then(r => r.json())
.then(agents => {
agents.forEach(agent => {
const safeId = agent.id.replace(/ /g, '-');
const card = document.getElementById('agent-' + safeId);
if (card) patchAgent(card, agent);
});
})
.catch(() => {}); // silently ignore fetch errors
}
setInterval(liveRefresh, 30000);
</script> </script>
{% endblock %} {% endblock %}