Initial commit — Server_Monitorizare_v2
This commit is contained in:
612
templates/ansible/execute.html
Normal file
612
templates/ansible/execute.html
Normal file
@@ -0,0 +1,612 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Execute Ansible Playbook - Server Monitoring{% endblock %}
|
||||
{% block page_title %}Execute Ansible Playbook{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.playbook-card {
|
||||
border: 2px solid #e9ecef; border-radius: 8px;
|
||||
transition: all 0.2s; cursor: pointer;
|
||||
}
|
||||
.playbook-card:hover { border-color: #0d6efd; box-shadow: 0 3px 10px rgba(13,110,253,.15); }
|
||||
.playbook-card.selected { border-color: #198754; background-color: #f8fff9; }
|
||||
.json-editor { font-family: 'Courier New', monospace; background-color: #f8f9fa; }
|
||||
#liveCard { display: none; }
|
||||
#liveTerminal {
|
||||
background: #1e1e1e; color: #d4d4d4;
|
||||
font-family: 'Courier New', monospace; font-size: .78rem;
|
||||
line-height: 1.5; height: 380px; overflow-y: auto;
|
||||
white-space: pre-wrap; word-break: break-all;
|
||||
border-radius: 0 0 8px 8px; padding: 12px 16px;
|
||||
}
|
||||
#liveTerminal .ansi-ok { color: #4ec9b0; }
|
||||
#liveTerminal .ansi-changed { color: #dcdcaa; }
|
||||
#liveTerminal .ansi-fail { color: #f44747; }
|
||||
#liveTerminal .ansi-unreachable { color: #ce9178; }
|
||||
#liveTerminal .ansi-task { color: #9cdcfe; font-weight: bold; }
|
||||
#liveTerminal .ansi-play { color: #c586c0; font-weight: bold; }
|
||||
.live-header {
|
||||
background: #252526; color: #ccc; border-radius: 8px 8px 0 0;
|
||||
padding: 8px 16px; font-size: .8rem; display: flex; align-items: center; gap: 10px;
|
||||
}
|
||||
.pulse-dot {
|
||||
width: 10px; height: 10px; border-radius: 50%;
|
||||
background: #4ec9b0; animation: pulse 1.2s infinite; display: inline-block;
|
||||
}
|
||||
.pulse-dot.done { background: #6a9955; animation: none; }
|
||||
.pulse-dot.error { background: #f44747; animation: none; }
|
||||
@keyframes pulse { 0%,100%{opacity:1;} 50%{opacity:.3;} }
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid">
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}{% for category, message in messages %}
|
||||
<div class="alert alert-{{ 'danger' if category=='error' else category }} alert-dismissible fade show">
|
||||
{{ message }}<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
{% endfor %}{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<form method="POST" id="executeForm">
|
||||
<div class="row g-3 mb-3">
|
||||
|
||||
<!-- ── Playbook selection ──────────────────────────────────── -->
|
||||
<div class="col-lg-5">
|
||||
<div class="card h-100">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h5 class="mb-0"><i class="fas fa-book me-2"></i>Select Playbook</h5>
|
||||
</div>
|
||||
<div class="card-body" style="overflow-y:auto; max-height:480px;">
|
||||
<h6 class="text-muted fw-bold mb-2 small text-uppercase">Built-in</h6>
|
||||
<div class="card playbook-card mb-2" data-name="update_devices" onclick="selectPlaybook('update_devices')">
|
||||
<div class="card-body py-2">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div>
|
||||
<h6 class="mb-1 text-primary"><i class="fas fa-download me-1"></i>Update Devices</h6>
|
||||
<p class="small text-muted mb-0">Update all packages on monitoring devices</p>
|
||||
</div>
|
||||
<span class="badge bg-success ms-2 flex-shrink-0">Built-in</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card playbook-card mb-2" data-name="restart_service" onclick="selectPlaybook('restart_service')">
|
||||
<div class="card-body py-2">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div>
|
||||
<h6 class="mb-1 text-success"><i class="fas fa-redo me-1"></i>Restart Service</h6>
|
||||
<p class="small text-muted mb-0">Restart monitoring services on devices</p>
|
||||
</div>
|
||||
<span class="badge bg-success ms-2 flex-shrink-0">Built-in</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card playbook-card mb-3" data-name="system_health" onclick="selectPlaybook('system_health')">
|
||||
<div class="card-body py-2">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div>
|
||||
<h6 class="mb-1 text-info"><i class="fas fa-heartbeat me-1"></i>System Health</h6>
|
||||
<p class="small text-muted mb-0">Check system health and monitoring status</p>
|
||||
</div>
|
||||
<span class="badge bg-success ms-2 flex-shrink-0">Built-in</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h6 class="text-muted fw-bold mb-2 small text-uppercase">Custom Playbooks</h6>
|
||||
<select class="form-select" id="customPlaybook">
|
||||
<option value="">— select custom playbook —</option>
|
||||
</select>
|
||||
<input type="hidden" name="playbook" id="selectedPlaybook">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Target selection ────────────────────────────────────── -->
|
||||
<div class="col-lg-7">
|
||||
<div class="card h-100">
|
||||
<div class="card-header bg-success text-white d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0"><i class="fas fa-crosshairs me-2"></i>Select Target</h5>
|
||||
<span id="targetCountBadge" class="badge bg-light text-dark">—</span>
|
||||
</div>
|
||||
<div class="card-body p-3">
|
||||
<!-- Mode pills -->
|
||||
<ul class="nav nav-pills nav-fill mb-3">
|
||||
<li class="nav-item">
|
||||
<button type="button" class="nav-link active" id="pill-all" onclick="setTargetMode('all')">
|
||||
<i class="fas fa-globe me-1"></i>All Hosts
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<button type="button" class="nav-link" id="pill-group" onclick="setTargetMode('group')">
|
||||
<i class="fas fa-layer-group me-1"></i>By Group
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<button type="button" class="nav-link" id="pill-host" onclick="setTargetMode('host')">
|
||||
<i class="fas fa-server me-1"></i>By Host
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<!-- All Hosts panel -->
|
||||
<div id="panel-all">
|
||||
{% set ns = namespace(total=0) %}
|
||||
{% for g in inventory.groups.values() %}{% set ns.total = ns.total + g.hosts|length %}{% endfor %}
|
||||
{% if ns.total > 0 %}
|
||||
<div class="alert alert-info mb-0">
|
||||
<i class="fas fa-info-circle me-2"></i>
|
||||
Will run on <strong>all {{ ns.total }} host(s)</strong>
|
||||
across <strong>{{ inventory.groups|length }} group(s)</strong>.
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-warning mb-0">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>
|
||||
No hosts in inventory yet.
|
||||
<a href="{{ url_for('ansible_web.devices') }}">Add devices to inventory</a> first.
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- By Group panel -->
|
||||
<div id="panel-group" class="d-none" style="max-height:300px;overflow-y:auto;">
|
||||
{% if inventory.groups %}
|
||||
{% for group_name, group in inventory.groups.items() %}
|
||||
<div class="form-check border rounded p-2 mb-2">
|
||||
<input class="form-check-input group-cb" type="checkbox"
|
||||
id="grp-{{ group_name }}" value="{{ group_name }}"
|
||||
data-count="{{ group.hosts|length }}"
|
||||
onchange="updateTargetCount()">
|
||||
<label class="form-check-label w-100" for="grp-{{ group_name }}">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<strong>{{ group_name }}</strong>
|
||||
<span class="badge bg-secondary">{{ group.hosts|length }} host(s)</span>
|
||||
</div>
|
||||
<small class="text-muted">
|
||||
{% for h in group.hosts[:3] %}{{ h.hostname }}{% if not loop.last %}, {% endif %}{% endfor %}{% if group.hosts|length > 3 %} +{{ group.hosts|length - 3 }} more{% endif %}
|
||||
</small>
|
||||
</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p class="text-muted text-center py-3">No groups in inventory.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- By Host panel -->
|
||||
<div id="panel-host" class="d-none" style="max-height:300px;overflow-y:auto;">
|
||||
{% if all_inv_hosts %}
|
||||
<div class="mb-2 d-flex gap-2">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="checkAllHosts(true)">
|
||||
<i class="fas fa-check-double me-1"></i>Select All
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="checkAllHosts(false)">Clear</button>
|
||||
</div>
|
||||
{% for h in all_inv_hosts %}
|
||||
<div class="form-check border-bottom py-2">
|
||||
<input class="form-check-input host-cb" type="checkbox"
|
||||
id="invhost-{{ h.hostname }}" value="{{ h.hostname }}"
|
||||
onchange="updateTargetCount()">
|
||||
<label class="form-check-label d-flex justify-content-between w-100" for="invhost-{{ h.hostname }}">
|
||||
<strong>{{ h.hostname }}</strong>
|
||||
<code class="text-muted" style="font-size:.8rem;">{{ h.ip }}</code>
|
||||
</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p class="text-muted text-center py-3">No hosts in inventory yet.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- /top row -->
|
||||
|
||||
<!-- ── Advanced Options ──────────────────────────────────────── -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header bg-info text-white">
|
||||
<h6 class="mb-0">
|
||||
<i class="fas fa-cogs me-1"></i>Advanced Options
|
||||
<button type="button" class="btn btn-outline-light btn-sm ms-2"
|
||||
data-bs-toggle="collapse" data-bs-target="#advancedOptions">
|
||||
<i class="fas fa-chevron-down"></i>
|
||||
</button>
|
||||
</h6>
|
||||
</div>
|
||||
<div class="collapse" id="advancedOptions">
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<h6>Execution Settings</h6>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Priority</label>
|
||||
<select class="form-select" name="priority">
|
||||
<option value="1">1 — Low</option>
|
||||
<option value="3">3 — Below Normal</option>
|
||||
<option value="5" selected>5 — Normal</option>
|
||||
<option value="7">7 — High</option>
|
||||
<option value="10">10 — Critical</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Max Retries</label>
|
||||
<select class="form-select" name="max_retries">
|
||||
<option value="0" selected>0 — No retries</option>
|
||||
<option value="1">1 retry</option>
|
||||
<option value="2">2 retries</option>
|
||||
<option value="3">3 retries</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="checkMode" name="check_mode">
|
||||
<label class="form-check-label" for="checkMode">Dry run (check mode)</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Extra Variables (JSON)</label>
|
||||
<textarea class="form-control json-editor" name="extra_vars" id="extraVars" rows="6"
|
||||
placeholder='{"variable": "value"}'></textarea>
|
||||
<div class="form-text">Optional variables passed to playbook</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<h6>Common Variables</h6>
|
||||
<ul class="list-unstyled small">
|
||||
<li><code>timeout: 600</code></li>
|
||||
<li><code>reboot_timeout: 900</code></li>
|
||||
<li><code>become_user: "root"</code></li>
|
||||
<li><code>force: true</code></li>
|
||||
</ul>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="addCommonVars()">
|
||||
<i class="fas fa-plus me-1"></i>Add Common Vars
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Summary bar + Execute button ─────────────────────────── -->
|
||||
<div class="card mb-3">
|
||||
<div class="card-body py-3">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-3">
|
||||
<small class="text-muted d-block">Playbook</small>
|
||||
<strong id="summaryPlaybook">None selected</strong>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<small class="text-muted d-block">Target</small>
|
||||
<strong id="summaryDevices">None selected</strong>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<small class="text-muted d-block">Est. Duration</small>
|
||||
<strong id="estimatedDuration">Unknown</strong>
|
||||
</div>
|
||||
<div class="col-md-2 text-end">
|
||||
<button type="submit" class="btn btn-primary btn-lg w-100" id="executeButton">
|
||||
<i class="fas fa-play me-1"></i>Execute
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hidden host inputs populated by JS before submit -->
|
||||
<div id="hostsContainer"></div>
|
||||
|
||||
</form>
|
||||
|
||||
<!-- ── Live Execution Output ──────────────────────────────────── -->
|
||||
<div class="row mt-3" id="liveCard">
|
||||
<div class="col-12">
|
||||
<div class="card shadow">
|
||||
<div class="card-header d-flex justify-content-between align-items-center py-2">
|
||||
<span class="fw-semibold">
|
||||
<i class="fas fa-terminal me-2"></i>Live Execution Output
|
||||
</span>
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<span id="liveStatusBadge" class="badge bg-secondary">Waiting…</span>
|
||||
<a id="liveDetailsLink" href="#" class="btn btn-sm btn-outline-primary" style="display:none">
|
||||
<i class="fas fa-external-link-alt me-1"></i>Full Details
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="live-header">
|
||||
<span class="pulse-dot" id="livePulseDot"></span>
|
||||
<span id="liveStatusText">Initializing…</span>
|
||||
<span class="ms-auto text-muted" id="liveElapsed"></span>
|
||||
</div>
|
||||
<div id="liveTerminal"></div>
|
||||
<div class="card-footer d-flex justify-content-between align-items-center py-2">
|
||||
<small class="text-muted" id="liveHostSummary"></small>
|
||||
<div class="d-flex gap-2">
|
||||
<button class="btn btn-sm btn-outline-secondary" onclick="toggleAutoScroll()">
|
||||
<i class="fas fa-arrow-down" id="autoScrollIcon"></i> Auto-scroll
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-danger" id="liveStopBtn" onclick="stopPolling()">
|
||||
<i class="fas fa-stop"></i> Stop polling
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- /container-fluid -->
|
||||
|
||||
<script>
|
||||
// ── State ────────────────────────────────────────────────────────────
|
||||
let selectedPlaybook = null;
|
||||
let targetMode = 'all';
|
||||
|
||||
// Live output state
|
||||
let pollTimer = null;
|
||||
let executionId = null;
|
||||
let autoScroll = true;
|
||||
let pollStartTime = null;
|
||||
|
||||
// ── Playbook selection ───────────────────────────────────────────────
|
||||
function selectPlaybook(name) {
|
||||
document.querySelectorAll('.playbook-card').forEach(c => c.classList.remove('selected'));
|
||||
document.getElementById('customPlaybook').value = '';
|
||||
document.querySelector(`.playbook-card[data-name="${name}"]`)?.classList.add('selected');
|
||||
selectedPlaybook = name;
|
||||
document.getElementById('selectedPlaybook').value = name;
|
||||
updateSummary();
|
||||
}
|
||||
|
||||
document.getElementById('customPlaybook').addEventListener('change', function() {
|
||||
if (this.value) {
|
||||
document.querySelectorAll('.playbook-card').forEach(c => c.classList.remove('selected'));
|
||||
selectedPlaybook = this.value;
|
||||
document.getElementById('selectedPlaybook').value = this.value;
|
||||
updateSummary();
|
||||
}
|
||||
});
|
||||
|
||||
// ── Target mode ──────────────────────────────────────────────────────
|
||||
function setTargetMode(mode) {
|
||||
targetMode = mode;
|
||||
['all','group','host'].forEach(m => {
|
||||
document.getElementById(`panel-${m}`).classList.toggle('d-none', m !== mode);
|
||||
document.getElementById(`pill-${m}`).classList.toggle('active', m === mode);
|
||||
});
|
||||
updateTargetCount();
|
||||
}
|
||||
|
||||
function updateTargetCount() {
|
||||
let count = 0, label = '';
|
||||
if (targetMode === 'all') {
|
||||
count = document.querySelectorAll('.host-cb').length;
|
||||
label = count > 0 ? `All — ${count} host(s)` : 'No hosts in inventory';
|
||||
} else if (targetMode === 'group') {
|
||||
const grps = document.querySelectorAll('.group-cb:checked');
|
||||
grps.forEach(cb => count += parseInt(cb.dataset.count || '0'));
|
||||
label = grps.length > 0 ? `${grps.length} group(s) / ${count} host(s)` : 'None selected';
|
||||
} else {
|
||||
count = document.querySelectorAll('.host-cb:checked').length;
|
||||
label = count > 0 ? `${count} host(s) selected` : 'None selected';
|
||||
}
|
||||
document.getElementById('targetCountBadge').textContent = label;
|
||||
updateSummary();
|
||||
}
|
||||
|
||||
function checkAllHosts(check) {
|
||||
document.querySelectorAll('.host-cb').forEach(cb => cb.checked = check);
|
||||
updateTargetCount();
|
||||
}
|
||||
|
||||
function getTargetCount() {
|
||||
if (targetMode === 'all') return document.querySelectorAll('.host-cb').length;
|
||||
if (targetMode === 'group') {
|
||||
let c = 0;
|
||||
document.querySelectorAll('.group-cb:checked').forEach(cb => c += parseInt(cb.dataset.count || '0'));
|
||||
return c;
|
||||
}
|
||||
return document.querySelectorAll('.host-cb:checked').length;
|
||||
}
|
||||
|
||||
// Populate hidden <input name="hosts"> elements before submit
|
||||
function buildHiddenInputs() {
|
||||
const container = document.getElementById('hostsContainer');
|
||||
container.innerHTML = '';
|
||||
const add = v => {
|
||||
const inp = document.createElement('input');
|
||||
inp.type = 'hidden'; inp.name = 'hosts'; inp.value = v;
|
||||
container.appendChild(inp);
|
||||
};
|
||||
if (targetMode === 'all') {
|
||||
document.querySelectorAll('.host-cb').forEach(cb => add(cb.value));
|
||||
if (container.children.length === 0) add('all'); // fallback if inventory empty
|
||||
} else if (targetMode === 'group') {
|
||||
document.querySelectorAll('.group-cb:checked').forEach(cb => add(cb.value));
|
||||
} else {
|
||||
document.querySelectorAll('.host-cb:checked').forEach(cb => add(cb.value));
|
||||
}
|
||||
}
|
||||
|
||||
// ── Summary bar ──────────────────────────────────────────────────────
|
||||
function updateSummary() {
|
||||
document.getElementById('summaryPlaybook').textContent = selectedPlaybook || 'None selected';
|
||||
const count = getTargetCount();
|
||||
const sd = document.getElementById('summaryDevices');
|
||||
if (targetMode === 'all') {
|
||||
sd.textContent = count > 0 ? `All hosts (${count})` : 'No hosts in inventory';
|
||||
} else if (targetMode === 'group') {
|
||||
const g = document.querySelectorAll('.group-cb:checked').length;
|
||||
sd.textContent = g > 0 ? `${g} group(s), ~${count} host(s)` : 'None selected';
|
||||
} else {
|
||||
sd.textContent = count > 0 ? `${count} host(s) selected` : 'None selected';
|
||||
}
|
||||
const dur = document.getElementById('estimatedDuration');
|
||||
if (selectedPlaybook && count > 0) {
|
||||
if (selectedPlaybook === 'update_devices')
|
||||
dur.textContent = `${Math.max(5, count * 2)}–${count * 10} min`;
|
||||
else if (selectedPlaybook === 'restart_service')
|
||||
dur.textContent = `${count}–${count * 2} min`;
|
||||
else dur.textContent = 'Varies';
|
||||
} else { dur.textContent = 'Unknown'; }
|
||||
}
|
||||
|
||||
function addCommonVars() {
|
||||
const el = document.getElementById('extraVars');
|
||||
const common = {"timeout": 600, "reboot_timeout": 900, "become_user": "root"};
|
||||
try {
|
||||
let cur = el.value.trim() ? JSON.parse(el.value) : {};
|
||||
Object.assign(cur, common);
|
||||
el.value = JSON.stringify(cur, null, 2);
|
||||
} catch { el.value = JSON.stringify(common, null, 2); }
|
||||
}
|
||||
|
||||
// ── Form submission ──────────────────────────────────────────────────
|
||||
document.getElementById('executeForm').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
if (!selectedPlaybook) { alert('Please select a playbook'); return; }
|
||||
const count = getTargetCount();
|
||||
if (count === 0) { alert('Please select at least one host or group'); return; }
|
||||
const extraRaw = document.getElementById('extraVars').value.trim();
|
||||
if (extraRaw) {
|
||||
try { JSON.parse(extraRaw); }
|
||||
catch { alert('Invalid JSON in Extra Variables'); return; }
|
||||
}
|
||||
const grpCount = document.querySelectorAll('.group-cb:checked').length;
|
||||
const targetDesc = targetMode === 'all' ? `all hosts (${count})` :
|
||||
targetMode === 'group' ? `${grpCount} group(s)` :
|
||||
`${count} host(s)`;
|
||||
if (!confirm(`Execute "${selectedPlaybook}" on ${targetDesc}?\nThis cannot be undone.`)) return;
|
||||
|
||||
buildHiddenInputs();
|
||||
|
||||
const btn = document.getElementById('executeButton');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Starting…';
|
||||
|
||||
resetLiveCard();
|
||||
document.getElementById('liveCard').style.display = '';
|
||||
|
||||
fetch(this.action, {
|
||||
method: 'POST',
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest' },
|
||||
body: new FormData(this),
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
executionId = data.execution_id;
|
||||
pollStartTime = Date.now();
|
||||
const link = document.getElementById('liveDetailsLink');
|
||||
link.href = `/ansible/executions/${executionId}`;
|
||||
link.style.display = '';
|
||||
startPolling();
|
||||
} else {
|
||||
setLiveError(data.error || 'Unknown error');
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="fas fa-play me-1"></i>Execute';
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
setLiveError('Network error: ' + err);
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="fas fa-play me-1"></i>Execute';
|
||||
});
|
||||
});
|
||||
|
||||
// ── Live terminal ────────────────────────────────────────────────────
|
||||
function resetLiveCard() {
|
||||
document.getElementById('liveTerminal').textContent = '';
|
||||
document.getElementById('liveStatusBadge').className = 'badge bg-secondary';
|
||||
document.getElementById('liveStatusBadge').textContent = 'Starting…';
|
||||
document.getElementById('liveStatusText').textContent = 'Initializing…';
|
||||
document.getElementById('livePulseDot').className = 'pulse-dot';
|
||||
document.getElementById('liveHostSummary').textContent = '';
|
||||
document.getElementById('liveElapsed').textContent = '';
|
||||
document.getElementById('liveDetailsLink').style.display = 'none';
|
||||
document.getElementById('liveStopBtn').style.display = '';
|
||||
}
|
||||
|
||||
function startPolling() {
|
||||
pollTimer = setInterval(pollLive, 2000);
|
||||
pollLive();
|
||||
}
|
||||
|
||||
function stopPolling() {
|
||||
clearInterval(pollTimer);
|
||||
pollTimer = null;
|
||||
document.getElementById('liveStopBtn').style.display = 'none';
|
||||
document.getElementById('liveStatusText').textContent += ' (polling stopped)';
|
||||
}
|
||||
|
||||
function pollLive() {
|
||||
if (!executionId) return;
|
||||
fetch(`/api/ansible/executions/${executionId}/live`)
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (!data.success) { setLiveError(data.error); return; }
|
||||
renderLiveData(data);
|
||||
if (['completed','failed','cancelled','timeout'].includes(data.status)) {
|
||||
stopPolling();
|
||||
document.getElementById('liveStopBtn').style.display = 'none';
|
||||
document.getElementById('executeButton').disabled = false;
|
||||
document.getElementById('executeButton').innerHTML = '<i class="fas fa-play me-1"></i>Execute';
|
||||
}
|
||||
})
|
||||
.catch(err => console.warn('Poll error:', err));
|
||||
}
|
||||
|
||||
function renderLiveData(data) {
|
||||
const badge = document.getElementById('liveStatusBadge');
|
||||
const dot = document.getElementById('livePulseDot');
|
||||
const colors = { running:'bg-primary', completed:'bg-success', failed:'bg-danger',
|
||||
cancelled:'bg-warning', timeout:'bg-warning' };
|
||||
badge.className = 'badge ' + (colors[data.status] || 'bg-secondary');
|
||||
badge.textContent = data.status.charAt(0).toUpperCase() + data.status.slice(1);
|
||||
dot.className = data.status === 'running' ? 'pulse-dot' :
|
||||
data.status === 'completed' ? 'pulse-dot done' : 'pulse-dot error';
|
||||
document.getElementById('liveStatusText').textContent =
|
||||
data.summary_message || `Running ${data.playbook_name} on ${(data.target_hosts||[]).join(', ')}`;
|
||||
if (pollStartTime) {
|
||||
const sec = Math.round((Date.now() - pollStartTime) / 1000);
|
||||
document.getElementById('liveElapsed').textContent = `${sec}s elapsed`;
|
||||
}
|
||||
if (data.status !== 'running') {
|
||||
document.getElementById('liveHostSummary').innerHTML =
|
||||
`✅ ${data.successful_hosts||0} ok ❌ ${data.failed_hosts||0} failed ⚠️ ${data.unreachable_hosts||0} unreachable`;
|
||||
}
|
||||
const terminal = document.getElementById('liveTerminal');
|
||||
const colorised = (data.log || '')
|
||||
.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>')
|
||||
.replace(/(PLAY\s+\[.*?\])/g, '<span class="ansi-play">$1</span>')
|
||||
.replace(/(TASK\s+\[.*?\])/g, '<span class="ansi-task">$1</span>')
|
||||
.replace(/(ok:.*)/g, '<span class="ansi-ok">$1</span>')
|
||||
.replace(/(changed:.*)/g, '<span class="ansi-changed">$1</span>')
|
||||
.replace(/(fatal:.*)/g, '<span class="ansi-fail">$1</span>')
|
||||
.replace(/(FAILED!.*)/g, '<span class="ansi-fail">$1</span>')
|
||||
.replace(/(UNREACHABLE!.*)/g, '<span class="ansi-unreachable">$1</span>');
|
||||
terminal.innerHTML = colorised;
|
||||
if (autoScroll) terminal.scrollTop = terminal.scrollHeight;
|
||||
}
|
||||
|
||||
function setLiveError(msg) {
|
||||
document.getElementById('liveStatusBadge').className = 'badge bg-danger';
|
||||
document.getElementById('liveStatusBadge').textContent = 'Error';
|
||||
document.getElementById('livePulseDot').className = 'pulse-dot error';
|
||||
document.getElementById('liveTerminal').textContent = 'Error: ' + msg;
|
||||
}
|
||||
|
||||
function toggleAutoScroll() {
|
||||
autoScroll = !autoScroll;
|
||||
document.getElementById('autoScrollIcon').style.opacity = autoScroll ? '1' : '0.3';
|
||||
}
|
||||
|
||||
// ── Initialize ───────────────────────────────────────────────────────
|
||||
updateTargetCount();
|
||||
updateSummary();
|
||||
{% if preselect_playbook %}
|
||||
selectPlaybook('{{ preselect_playbook }}');
|
||||
{% endif %}
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user