mirror of
https://github.com/swissmakers/fail2ban-ui.git
synced 2026-04-17 05:53:15 +02:00
restructure the main html-file for better understanding
This commit is contained in:
@@ -1,7 +1,9 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8"/>
|
<meta charset="UTF-8"/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
|
||||||
<title>Fail2ban UI Dashboard</title>
|
<title>Fail2ban UI Dashboard</title>
|
||||||
<!-- Bootstrap 5 (CDN) -->
|
<!-- Bootstrap 5 (CDN) -->
|
||||||
<link
|
<link
|
||||||
@@ -30,8 +32,11 @@
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="bg-light">
|
<body class="bg-light">
|
||||||
<!-- NavBar -->
|
<!-- ******************************************************************* -->
|
||||||
|
<!-- NAVIGATION : -->
|
||||||
|
<!-- ******************************************************************* -->
|
||||||
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
|
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<a class="navbar-brand" href="#">
|
<a class="navbar-brand" href="#">
|
||||||
@@ -57,7 +62,8 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
<!-- ******************************************************************* -->
|
||||||
|
|
||||||
<!-- Reload Banner -->
|
<!-- Reload Banner -->
|
||||||
<div id="reloadBanner" class="bg-warning text-dark p-3 text-center">
|
<div id="reloadBanner" class="bg-warning text-dark p-3 text-center">
|
||||||
@@ -67,61 +73,67 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- ******************************************************************* -->
|
||||||
|
<!-- APP Sections (Pages) : -->
|
||||||
|
<!-- ******************************************************************* -->
|
||||||
|
|
||||||
<!-- Dashboard Section -->
|
<!-- Dashboard Section -->
|
||||||
<div id="dashboardSection" class="container my-4">
|
<div id="dashboardSection" class="container my-4">
|
||||||
<h1 class="mb-4">Dashboard</h1>
|
<h1 class="mb-4">Dashboard</h1>
|
||||||
<div id="dashboard"></div>
|
<div id="dashboard"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Filter Debug Section -->
|
||||||
|
<div id="filterSection" style="display: none;" class="container my-4">
|
||||||
|
<h2>Filter Debug</h2>
|
||||||
|
<!-- Dropdown of available jail/filters -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="filterSelect" class="form-label">Select a Filter</label>
|
||||||
|
<select id="filterSelect" class="form-select"></select>
|
||||||
|
</div>
|
||||||
|
<!-- Textarea for log lines to test -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Log Lines</label>
|
||||||
|
<textarea id="logLinesTextarea" class="form-control" rows="6" disabled></textarea>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-secondary" onclick="testSelectedFilter()">Test Filter</button>
|
||||||
|
<hr/>
|
||||||
|
<div id="testResults"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Settings Section -->
|
<!-- Settings Section -->
|
||||||
<div id="settingsSection" style="display: none;" class="container my-4">
|
<div id="settingsSection" style="display: none;" class="container my-4">
|
||||||
<h2>Settings</h2>
|
<h2>Settings</h2>
|
||||||
<form onsubmit="saveSettings(event)">
|
<form onsubmit="saveSettings(event)">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="languageSelect" class="form-label">Language</label>
|
<label for="languageSelect" class="form-label">Language</label>
|
||||||
<select id="languageSelect" class="form-select">
|
<select id="languageSelect" class="form-select" disabled>
|
||||||
<option value="en">English</option>
|
<option value="en">English</option>
|
||||||
<option value="de">Deutsch</option>
|
<option value="de">Deutsch</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="alertEmail" class="form-label">Alert Email</label>
|
<label for="alertEmail" class="form-label">Alert Email</label>
|
||||||
<input type="email" class="form-control" id="alertEmail"/>
|
<input type="email" class="form-control" id="alertEmail"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Alert Countries</label>
|
<label for="alertCountries" class="form-label">Select alert Countries</label>
|
||||||
<small class="text-muted">(Choose which country bans trigger an email. "all" for everything.)</small>
|
<p class="text-muted">Choose which country-IP bans should trigger an email. With CTRL, you can select multiple.</p>
|
||||||
<input type="text" class="form-control" id="alertCountries"
|
<select id="alertCountries" class="form-select" multiple size="7">
|
||||||
placeholder="e.g. all or DE,CH"/>
|
<option value="ALL">ALL (Every Country)</option>
|
||||||
|
<option value="CH">Switzerland (CH)</option>
|
||||||
|
<option value="DE">Germany (DE)</option>
|
||||||
|
<option value="IT">Italy (IT)</option>
|
||||||
|
<option value="FR">France (FR)</option>
|
||||||
|
<option value="UK">England (UK)</option>
|
||||||
|
<option value="US">United States (US)</option>
|
||||||
|
<!-- Maybe i will add more later.. -->
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="submit" class="btn btn-primary">Save</button>
|
<button type="submit" class="btn btn-primary">Save</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- ******************************************************************* -->
|
||||||
<!-- Filter Debug Section -->
|
|
||||||
<div id="filterSection" style="display: none;" class="container my-4">
|
|
||||||
<h2>Filter Debug</h2>
|
|
||||||
|
|
||||||
<!-- Dropdown of available filters -->
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="filterSelect" class="form-label">Select a Filter</label>
|
|
||||||
<select id="filterSelect" class="form-select"></select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Textarea for log lines to test -->
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">Log Lines</label>
|
|
||||||
<textarea id="logLinesTextarea" class="form-control" rows="6"></textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button class="btn btn-secondary" onclick="testSelectedFilter()">Test Filter</button>
|
|
||||||
<hr/>
|
|
||||||
|
|
||||||
<div id="testResults"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Footer -->
|
<!-- Footer -->
|
||||||
<footer class="text-center mt-4 mb-4">
|
<footer class="text-center mt-4 mb-4">
|
||||||
@@ -134,6 +146,9 @@
|
|||||||
</p>
|
</p>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
<!-- ******************************************************************* -->
|
||||||
|
<!-- APP Components (HTML) : -->
|
||||||
|
<!-- ******************************************************************* -->
|
||||||
<!-- Loading Overlay -->
|
<!-- Loading Overlay -->
|
||||||
<div id="loading-overlay" class="d-flex">
|
<div id="loading-overlay" class="d-flex">
|
||||||
<div class="spinner-border text-light" role="status">
|
<div class="spinner-border text-light" role="status">
|
||||||
@@ -165,6 +180,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- ******************************************************************* -->
|
||||||
|
|
||||||
<!-- Bootstrap 5 JS (for modal, etc.) -->
|
<!-- Bootstrap 5 JS (for modal, etc.) -->
|
||||||
<script
|
<script
|
||||||
@@ -172,11 +188,22 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
// For information: We avoid ES6 backticks in our JS, to prevent confusion with the Go template parser.
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
// We avoid ES6 backticks here to prevent confusion with the Go template parser.
|
//*******************************************************************
|
||||||
|
//* Init page and main-components : *
|
||||||
|
//*******************************************************************
|
||||||
|
|
||||||
|
// Init and run first function, when DOM is ready
|
||||||
var currentJailForConfig = null;
|
var currentJailForConfig = null;
|
||||||
|
window.addEventListener('DOMContentLoaded', function() {
|
||||||
|
showLoading(true);
|
||||||
|
checkReloadNeeded();
|
||||||
|
fetchSummary().then(function() {
|
||||||
|
showLoading(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Toggle the loading overlay (with !important)
|
// Toggle the loading overlay (with !important)
|
||||||
function showLoading(show) {
|
function showLoading(show) {
|
||||||
@@ -188,12 +215,43 @@ function showLoading(show) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('DOMContentLoaded', function() {
|
// Check if there is still a reload of the fail2ban service needed
|
||||||
showLoading(true);
|
function checkReloadNeeded() {
|
||||||
fetchSummary().then(function() {
|
fetch('/api/settings')
|
||||||
showLoading(false);
|
.then(res => res.json())
|
||||||
});
|
.then(data => {
|
||||||
});
|
if (data.reloadNeeded) {
|
||||||
|
document.getElementById('reloadBanner').style.display = 'block';
|
||||||
|
} else {
|
||||||
|
document.getElementById('reloadBanner').style.display = 'none';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => console.error('Error checking reloadNeeded:', err));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load dynamically the other pages when navigating in nav
|
||||||
|
function showSection(sectionId) {
|
||||||
|
// hide all sections
|
||||||
|
document.getElementById('dashboardSection').style.display = 'none';
|
||||||
|
document.getElementById('filterSection').style.display = 'none';
|
||||||
|
document.getElementById('settingsSection').style.display = 'none';
|
||||||
|
|
||||||
|
// show the requested section
|
||||||
|
document.getElementById(sectionId).style.display = 'block';
|
||||||
|
|
||||||
|
// If it's filterSection, load filters
|
||||||
|
if (sectionId === 'filterSection') {
|
||||||
|
showFilterSection();
|
||||||
|
}
|
||||||
|
// If it's settingsSection, load settings
|
||||||
|
if (sectionId === 'settingsSection') {
|
||||||
|
loadSettings();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//*******************************************************************
|
||||||
|
//* Fetch data and render dashboard : *
|
||||||
|
//*******************************************************************
|
||||||
|
|
||||||
// Fetch summary (jails, stats, last 5 bans)
|
// Fetch summary (jails, stats, last 5 bans)
|
||||||
function fetchSummary() {
|
function fetchSummary() {
|
||||||
@@ -228,7 +286,7 @@ function renderDashboard(data) {
|
|||||||
+ ' <tr>'
|
+ ' <tr>'
|
||||||
+ ' <th>Jail Name</th>'
|
+ ' <th>Jail Name</th>'
|
||||||
+ ' <th>Total Banned</th>'
|
+ ' <th>Total Banned</th>'
|
||||||
+ ' <th>New in Last Hour</th>'
|
+ ' <th>New Last Hour</th>'
|
||||||
+ ' <th>Banned IPs (Unban)</th>'
|
+ ' <th>Banned IPs (Unban)</th>'
|
||||||
+ ' </tr>'
|
+ ' </tr>'
|
||||||
+ ' </thead>'
|
+ ' </thead>'
|
||||||
@@ -305,6 +363,10 @@ function renderBannedIPs(jailName, ips) {
|
|||||||
return content;
|
return content;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//*******************************************************************
|
||||||
|
//* Functions to manage IP-bans : *
|
||||||
|
//*******************************************************************
|
||||||
|
|
||||||
// Unban IP
|
// Unban IP
|
||||||
function unbanIP(jail, ip) {
|
function unbanIP(jail, ip) {
|
||||||
if (!confirm("Unban IP " + ip + " from jail " + jail + "?")) {
|
if (!confirm("Unban IP " + ip + " from jail " + jail + "?")) {
|
||||||
@@ -317,7 +379,7 @@ function unbanIP(jail, ip) {
|
|||||||
if (data.error) {
|
if (data.error) {
|
||||||
alert("Error: " + data.error);
|
alert("Error: " + data.error);
|
||||||
} else {
|
} else {
|
||||||
alert(data.message || "IP unbanned");
|
alert(data.message || "IP unbanned successfully");
|
||||||
}
|
}
|
||||||
return fetchSummary();
|
return fetchSummary();
|
||||||
})
|
})
|
||||||
@@ -329,7 +391,11 @@ function unbanIP(jail, ip) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open the jail config modal
|
//*******************************************************************
|
||||||
|
//* Filter-mod and config-mod actions : *
|
||||||
|
//*******************************************************************
|
||||||
|
|
||||||
|
// Open jail/filter modal and load filter-config
|
||||||
function openJailConfigModal(jailName) {
|
function openJailConfigModal(jailName) {
|
||||||
currentJailForConfig = jailName;
|
currentJailForConfig = jailName;
|
||||||
var textArea = document.getElementById('jailConfigTextarea');
|
var textArea = document.getElementById('jailConfigTextarea');
|
||||||
@@ -358,7 +424,7 @@ function openJailConfigModal(jailName) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save jail config
|
// Save filter config for the current opened jail
|
||||||
function saveJailConfig() {
|
function saveJailConfig() {
|
||||||
if (!currentJailForConfig) return;
|
if (!currentJailForConfig) return;
|
||||||
showLoading(true);
|
showLoading(true);
|
||||||
@@ -374,7 +440,8 @@ function saveJailConfig() {
|
|||||||
if (data.error) {
|
if (data.error) {
|
||||||
alert("Error saving config: " + data.error);
|
alert("Error saving config: " + data.error);
|
||||||
} else {
|
} else {
|
||||||
alert(data.message || "Config saved");
|
//alert(data.message || "Config saved");
|
||||||
|
console.log("Filter saved successfully. Reload needed? " + data.reloadNeeded);
|
||||||
// Hide modal
|
// Hide modal
|
||||||
var modalEl = document.getElementById('jailConfigModal');
|
var modalEl = document.getElementById('jailConfigModal');
|
||||||
var modalObj = bootstrap.Modal.getInstance(modalEl);
|
var modalObj = bootstrap.Modal.getInstance(modalEl);
|
||||||
@@ -391,44 +458,32 @@ function saveJailConfig() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reload Fail2ban
|
// Load current settings when opening settings page
|
||||||
function reloadFail2ban() {
|
|
||||||
if (!confirm("Reload Fail2ban now?")) return;
|
|
||||||
showLoading(true);
|
|
||||||
fetch('/api/fail2ban/reload', { method: 'POST' })
|
|
||||||
.then(function(res) { return res.json(); })
|
|
||||||
.then(function(data) {
|
|
||||||
if (data.error) {
|
|
||||||
alert("Error: " + data.error);
|
|
||||||
} else {
|
|
||||||
alert(data.message || "Fail2ban reloaded");
|
|
||||||
// Hide reload banner
|
|
||||||
document.getElementById('reloadBanner').style.display = 'none';
|
|
||||||
// Refresh data
|
|
||||||
return fetchSummary();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(function(err) {
|
|
||||||
alert("Error: " + err);
|
|
||||||
})
|
|
||||||
.finally(function() {
|
|
||||||
showLoading(false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadSettings() {
|
function loadSettings() {
|
||||||
showLoading(true);
|
showLoading(true);
|
||||||
fetch('/api/settings')
|
fetch('/api/settings')
|
||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
// populate the form
|
// populate language, email, etc...
|
||||||
document.getElementById('languageSelect').value = data.language || 'en';
|
document.getElementById('languageSelect').value = data.language || 'en';
|
||||||
document.getElementById('alertEmail').value = data.alertEmail || '';
|
document.getElementById('alertEmail').value = data.sender || '';
|
||||||
if (data.alertCountries && data.alertCountries.length > 0) {
|
|
||||||
if (data.alertCountries[0] === 'all') {
|
// alertCountries multi
|
||||||
document.getElementById('alertCountries').value = 'all';
|
const select = document.getElementById('alertCountries');
|
||||||
} else {
|
// clear selection
|
||||||
document.getElementById('alertCountries').value = data.alertCountries.join(',');
|
for (let i = 0; i < select.options.length; i++) {
|
||||||
|
select.options[i].selected = false;
|
||||||
|
}
|
||||||
|
if (!data.alertCountries || data.alertCountries.length === 0) {
|
||||||
|
// default to "ALL"
|
||||||
|
select.options[0].selected = true;
|
||||||
|
} else {
|
||||||
|
// Mark them selected
|
||||||
|
for (let i = 0; i < select.options.length; i++) {
|
||||||
|
let val = select.options[i].value;
|
||||||
|
if (data.alertCountries.includes(val)) {
|
||||||
|
select.options[i].selected = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -438,25 +493,30 @@ function loadSettings() {
|
|||||||
.finally(() => showLoading(false));
|
.finally(() => showLoading(false));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Save settings when hit the save button
|
||||||
function saveSettings(e) {
|
function saveSettings(e) {
|
||||||
e.preventDefault(); // prevent form submission
|
e.preventDefault(); // prevent form submission
|
||||||
|
|
||||||
showLoading(true);
|
showLoading(true);
|
||||||
const lang = document.getElementById('languageSelect').value;
|
const lang = document.getElementById('languageSelect').value;
|
||||||
const mail = document.getElementById('alertEmail').value;
|
const mail = document.getElementById('alertEmail').value;
|
||||||
const countries = document.getElementById('alertCountries').value;
|
|
||||||
|
|
||||||
let countryList = [];
|
const select = document.getElementById('alertCountries');
|
||||||
if (!countries || countries.trim() === '') {
|
let chosenCountries = [];
|
||||||
countryList.push('all');
|
for (let i = 0; i < select.options.length; i++) {
|
||||||
} else {
|
if (select.options[i].selected) {
|
||||||
countryList = countries.split(',').map(s => s.trim());
|
chosenCountries.push(select.options[i].value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If user selected "ALL", we override everything
|
||||||
|
if (chosenCountries.includes("ALL")) {
|
||||||
|
chosenCountries = ["all"];
|
||||||
}
|
}
|
||||||
|
|
||||||
const body = {
|
const body = {
|
||||||
language: lang,
|
language: lang,
|
||||||
alertEmail: mail,
|
sender: mail,
|
||||||
alertCountries: countryList
|
alertCountries: chosenCountries
|
||||||
};
|
};
|
||||||
|
|
||||||
fetch('/api/settings', {
|
fetch('/api/settings', {
|
||||||
@@ -464,41 +524,20 @@ function saveSettings(e) {
|
|||||||
headers: {'Content-Type': 'application/json'},
|
headers: {'Content-Type': 'application/json'},
|
||||||
body: JSON.stringify(body)
|
body: JSON.stringify(body)
|
||||||
})
|
})
|
||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
alert('Error saving settings: ' + data.error);
|
alert('Error saving settings: ' + data.error);
|
||||||
} else {
|
} else {
|
||||||
alert(data.message || 'Settings saved');
|
//alert(data.message || 'Settings saved');
|
||||||
if (data.needsRestart) {
|
console.log("Settings saved successfully. Reload needed? " + data.reloadNeeded);
|
||||||
// show the same "reload" banner used for filter changes
|
if (data.reloadNeeded) {
|
||||||
document.getElementById('reloadBanner').style.display = 'block';
|
document.getElementById('reloadBanner').style.display = 'block';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
})
|
.catch(err => alert('Error: ' + err))
|
||||||
.catch(err => {
|
.finally(() => showLoading(false));
|
||||||
alert('Error: ' + err);
|
|
||||||
})
|
|
||||||
.finally(() => showLoading(false));
|
|
||||||
}
|
|
||||||
|
|
||||||
function showSection(sectionId) {
|
|
||||||
// hide all sections
|
|
||||||
document.getElementById('dashboardSection').style.display = 'none';
|
|
||||||
document.getElementById('filterSection').style.display = 'none';
|
|
||||||
document.getElementById('settingsSection').style.display = 'none';
|
|
||||||
|
|
||||||
// show the requested section
|
|
||||||
document.getElementById(sectionId).style.display = 'block';
|
|
||||||
|
|
||||||
// If it's filterSection, load filters
|
|
||||||
if (sectionId === 'filterSection') {
|
|
||||||
showFilterSection();
|
|
||||||
}
|
|
||||||
// If it's settingsSection, load settings
|
|
||||||
if (sectionId === 'settingsSection') {
|
|
||||||
loadSettings();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load the list of filters from /api/filters
|
// Load the list of filters from /api/filters
|
||||||
@@ -589,6 +628,35 @@ function showFilterSection() {
|
|||||||
document.getElementById('logLinesTextarea').value = '';
|
document.getElementById('logLinesTextarea').value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//*******************************************************************
|
||||||
|
//* Reload fail2ban action : *
|
||||||
|
//*******************************************************************
|
||||||
|
|
||||||
|
// Reload Fail2ban
|
||||||
|
function reloadFail2ban() {
|
||||||
|
if (!confirm("It can happen that some logs are not parsed during the reload of fail2ban. Reload Fail2ban now?")) return;
|
||||||
|
showLoading(true);
|
||||||
|
fetch('/api/fail2ban/reload', { method: 'POST' })
|
||||||
|
.then(function(res) { return res.json(); })
|
||||||
|
.then(function(data) {
|
||||||
|
if (data.error) {
|
||||||
|
alert("Error: " + data.error);
|
||||||
|
} else {
|
||||||
|
alert(data.message || "Fail2ban reloaded");
|
||||||
|
// Hide reload banner
|
||||||
|
document.getElementById('reloadBanner').style.display = 'none';
|
||||||
|
// Refresh data
|
||||||
|
return fetchSummary();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function(err) {
|
||||||
|
alert("Error: " + err);
|
||||||
|
})
|
||||||
|
.finally(function() {
|
||||||
|
showLoading(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user