bmc_hub/app/settings/frontend/sql_console.html

190 lines
7.8 KiB
HTML
Raw Normal View History

{% extends "shared/frontend/base.html" %}
{% block title %}SQL Console - BMC Hub{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="d-flex align-items-center justify-content-between mb-4">
<div>
<h2 class="mb-1"><i class="bi bi-database me-2"></i>SQL Console</h2>
<div class="text-muted small">Read-only SQL for superadmin. Kun SELECT/WITH queries.</div>
</div>
</div>
<div class="card border-0 shadow-sm mb-4">
<div class="card-body">
<div class="mb-3">
<label class="form-label">Presets</label>
<div class="d-flex flex-wrap gap-2" id="sqlPresets">
<button type="button" class="btn btn-outline-secondary btn-sm" data-preset="serverTime">Server time</button>
<button type="button" class="btn btn-outline-secondary btn-sm" data-preset="telefoniLatest">Telefoni latest</button>
<button type="button" class="btn btn-outline-secondary btn-sm" data-preset="telefoniCount">Telefoni count</button>
<button type="button" class="btn btn-outline-secondary btn-sm" data-preset="missionLatest">Mission latest</button>
<button type="button" class="btn btn-outline-secondary btn-sm" data-preset="sourceCompare">Source compare</button>
</div>
</div>
<div class="mb-3">
<label for="sqlQuery" class="form-label">SQL query</label>
<textarea id="sqlQuery" class="form-control" rows="8" spellcheck="false" placeholder="SELECT * FROM telefoni_opkald ORDER BY started_at DESC"></textarea>
</div>
<div class="row g-3 align-items-end">
<div class="col-md-2">
<label for="sqlLimit" class="form-label">Limit</label>
<input id="sqlLimit" type="number" class="form-control" value="200" min="1" max="1000" />
</div>
<div class="col-md-10 d-flex gap-2 justify-content-end">
<button id="runSqlBtn" class="btn btn-primary">
<i class="bi bi-play-fill me-1"></i>Run Query
</button>
</div>
</div>
<div id="sqlStatus" class="alert alert-info py-2 mt-3 mb-0">Ready</div>
</div>
</div>
<div class="card border-0 shadow-sm">
<div class="card-body">
<div class="table-responsive">
<table class="table table-sm table-striped align-middle mb-0">
<thead id="sqlResultHead"></thead>
<tbody id="sqlResultBody">
<tr><td class="text-muted">No results yet</td></tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
const SQL_PRESETS = {
serverTime: {
query: 'SELECT now() AS server_time',
limit: 50,
},
telefoniLatest: {
query: `SELECT id, callid, direction, ekstern_nummer, started_at, ended_at, duration_sec\nFROM telefoni_opkald\nORDER BY started_at DESC\nLIMIT 50`,
limit: 50,
},
telefoniCount: {
query: 'SELECT COUNT(*) AS total_calls, MAX(started_at) AS latest_started_at FROM telefoni_opkald',
limit: 50,
},
missionLatest: {
query: `SELECT call_id, state, caller_number, contact_name, company_name, started_at, ended_at\nFROM mission_call_state\nORDER BY started_at DESC\nLIMIT 50`,
limit: 50,
},
sourceCompare: {
query: `WITH telefoni AS (\n SELECT 'telefoni_opkald'::text AS source, COUNT(*)::bigint AS total_calls, MAX(started_at) AS latest_started_at\n FROM telefoni_opkald\n), mission AS (\n SELECT 'mission_call_state'::text AS source, COUNT(*)::bigint AS total_calls, MAX(started_at) AS latest_started_at\n FROM mission_call_state\n)\nSELECT * FROM telefoni\nUNION ALL\nSELECT * FROM mission`,
limit: 50,
},
};
function escapeHtml(value) {
return String(value ?? '')
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#039;');
}
function renderResults(data) {
const head = document.getElementById('sqlResultHead');
const body = document.getElementById('sqlResultBody');
const columns = Array.isArray(data.columns) ? data.columns : [];
const rows = Array.isArray(data.rows) ? data.rows : [];
if (columns.length === 0) {
head.innerHTML = '';
body.innerHTML = '<tr><td class="text-muted">No rows returned</td></tr>';
return;
}
head.innerHTML = '<tr>' + columns.map((c) => `<th>${escapeHtml(c)}</th>`).join('') + '</tr>';
if (rows.length === 0) {
body.innerHTML = `<tr><td colspan="${columns.length}" class="text-muted">No rows returned</td></tr>`;
return;
}
body.innerHTML = rows.map((row) => {
return '<tr>' + columns.map((c) => {
const value = row[c];
if (value === null || value === undefined) return '<td class="text-muted">NULL</td>';
if (typeof value === 'object') return `<td>${escapeHtml(JSON.stringify(value))}</td>`;
return `<td>${escapeHtml(String(value))}</td>`;
}).join('') + '</tr>';
}).join('');
}
async function runSqlQuery() {
const query = (document.getElementById('sqlQuery').value || '').trim();
const limitInput = Number(document.getElementById('sqlLimit').value || 200);
const limit = Number.isFinite(limitInput) ? limitInput : 200;
const status = document.getElementById('sqlStatus');
const button = document.getElementById('runSqlBtn');
status.className = 'alert alert-info py-2 mt-3 mb-0';
status.textContent = 'Running query...';
button.disabled = true;
try {
const response = await fetch('/settings/sql/execute', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ query, limit }),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
const message = errorData.detail || 'Failed to run query';
status.className = 'alert alert-danger py-2 mt-3 mb-0';
status.textContent = message;
return;
}
const data = await response.json();
renderResults(data);
status.className = 'alert alert-success py-2 mt-3 mb-0';
status.textContent = `Rows returned: ${data.row_count} (limit ${data.limit})`;
} catch (error) {
status.className = 'alert alert-danger py-2 mt-3 mb-0';
status.textContent = 'Network or server error while running query';
} finally {
button.disabled = false;
}
}
document.addEventListener('DOMContentLoaded', () => {
const queryEl = document.getElementById('sqlQuery');
const limitEl = document.getElementById('sqlLimit');
document.getElementById('runSqlBtn').addEventListener('click', runSqlQuery);
queryEl.addEventListener('keydown', (event) => {
if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {
event.preventDefault();
runSqlQuery();
}
});
document.querySelectorAll('#sqlPresets [data-preset]').forEach((button) => {
button.addEventListener('click', () => {
const key = button.getAttribute('data-preset');
const preset = SQL_PRESETS[key];
if (!preset) return;
queryEl.value = preset.query;
limitEl.value = String(preset.limit || 50);
queryEl.focus();
});
});
queryEl.value = SQL_PRESETS.serverTime.query;
limitEl.value = String(SQL_PRESETS.serverTime.limit);
});
</script>
{% endblock %}