411 lines
12 KiB
JavaScript
411 lines
12 KiB
JavaScript
/* Pi-hole: A black hole for Internet advertisements
|
|
* (c) 2023 Pi-hole, LLC (https://pi-hole.net)
|
|
* Network-wide ad blocking via your own hardware.
|
|
*
|
|
* This file is copyright under the latest version of the EUPL.
|
|
* Please see LICENSE file for your rights under this license. */
|
|
|
|
/* global apiFailure:false, Chart:false, THEME_COLORS:false, customTooltips:false, htmlLegendPlugin:false,doughnutTooltip:false, ChartDeferred:false, REFRESH_INTERVAL: false, utils: false, i18n */
|
|
|
|
"use strict";
|
|
|
|
let hostinfoTimer = null;
|
|
let cachePieChart = null;
|
|
let cacheSize = 0;
|
|
let cacheEntries = 0;
|
|
|
|
// Register the ChartDeferred plugin to all charts:
|
|
Chart.register(ChartDeferred);
|
|
Chart.defaults.set("plugins.deferred", {
|
|
yOffset: "20%",
|
|
delay: 300,
|
|
});
|
|
|
|
function updateCachePie(data) {
|
|
const v = [];
|
|
const c = [];
|
|
const k = [];
|
|
let i = 0;
|
|
|
|
// Compute total number of cache entries
|
|
cacheEntries = 0;
|
|
for (const item of Object.keys(data)) {
|
|
cacheEntries += data[item].valid;
|
|
cacheEntries += data[item].stale;
|
|
}
|
|
|
|
// Sort data by value, put OTHER always as last
|
|
const sorted = Object.keys(data).sort((a, b) => {
|
|
if (a === "OTHER") {
|
|
return 1;
|
|
}
|
|
|
|
if (b === "OTHER") {
|
|
return -1;
|
|
}
|
|
|
|
return data[b].valid + data[b].stale - (data[a].valid + data[a].stale);
|
|
});
|
|
|
|
// Rebuild data object
|
|
const tmp = {};
|
|
for (const item of sorted) {
|
|
tmp[item] = data[item];
|
|
}
|
|
|
|
data = tmp;
|
|
|
|
// Add empty space to chart
|
|
data.empty = {};
|
|
data.empty.valid = cacheSize - cacheEntries;
|
|
|
|
// Fill chart with data
|
|
for (const [item, value] of Object.entries(data)) {
|
|
const itemLabel =
|
|
item === "empty"
|
|
? i18n.t("shared.empty")
|
|
: item === "OTHER"
|
|
? i18n.t("settings.system.other")
|
|
: item;
|
|
if (value.valid > 0) {
|
|
v.push((100 * value.valid) / cacheSize);
|
|
c.push(item !== "empty" ? THEME_COLORS[i++ % THEME_COLORS.length] : "#80808040");
|
|
k.push(itemLabel);
|
|
}
|
|
|
|
if (value.stale > 0) {
|
|
// There are no stale empty entries
|
|
v.push((100 * value.stale) / cacheSize);
|
|
c.push(THEME_COLORS[i++ % THEME_COLORS.length]);
|
|
k.push(`${itemLabel} (${i18n.t("shared.stale")})`);
|
|
}
|
|
}
|
|
|
|
// Build a single dataset with the data to be pushed
|
|
const dd = { data: v, backgroundColor: c };
|
|
// and push it at once
|
|
cachePieChart.data.datasets[0] = dd;
|
|
cachePieChart.data.labels = k;
|
|
|
|
// Passing 'none' will prevent rotation animation for further updates
|
|
//https://www.chartjs.org/docs/latest/developers/updates.html#preventing-animations
|
|
cachePieChart.update("none");
|
|
}
|
|
|
|
function updateHostInfo() {
|
|
$.ajax({
|
|
url: document.body.dataset.apiurl + "/info/host",
|
|
})
|
|
.done(data => {
|
|
const host = data.host;
|
|
const uname = host.uname;
|
|
if (uname.domainname !== "(none)") {
|
|
$("#sysinfo-hostname").text(uname.nodename + "." + uname.domainname);
|
|
} else {
|
|
$("#sysinfo-hostname").text(uname.nodename);
|
|
}
|
|
|
|
$("#sysinfo-kernel").text(
|
|
uname.sysname +
|
|
" " +
|
|
uname.nodename +
|
|
" " +
|
|
uname.release +
|
|
" " +
|
|
uname.version +
|
|
" " +
|
|
uname.machine
|
|
);
|
|
clearTimeout(hostinfoTimer);
|
|
hostinfoTimer = utils.setTimer(updateHostInfo, REFRESH_INTERVAL.hosts);
|
|
})
|
|
.fail(data => {
|
|
apiFailure(data);
|
|
});
|
|
}
|
|
|
|
// Walk nested objects, create a dash-separated global key and assign the value
|
|
// to the corresponding element (add percentage for DNS replies)
|
|
function setMetrics(data, prefix) {
|
|
const cacheData = {};
|
|
for (const [key, val] of Object.entries(data)) {
|
|
if (prefix === "sysinfo-dns-cache-content-") {
|
|
// Create table row for each DNS cache entry
|
|
// (if table exists)
|
|
if ($("#dns-cache-table").length > 0) {
|
|
const name =
|
|
val.name !== "OTHER"
|
|
? val.name !== null
|
|
? i18n.tf("settings.system.valid_metric", val.name)
|
|
: i18n.tf("settings.system.valid_metric_type", val.type)
|
|
: i18n.t("settings.system.valid_metric_other");
|
|
const tr =
|
|
"<tr><th>" +
|
|
i18n.tf("settings.system.n_records_in_cache", name) +
|
|
"</th><td>" +
|
|
val.count +
|
|
"</td></tr>";
|
|
// Append row to DNS cache table
|
|
$("#dns-cache-table").append(tr);
|
|
}
|
|
|
|
cacheData[val.name] = val.count;
|
|
} else if (typeof val === "object") {
|
|
setMetrics(val, prefix + key + "-");
|
|
} else if (prefix === "sysinfo-dns-replies-") {
|
|
// Compute and display percentage of DNS replies in addition to the absolute value
|
|
const lval = val.toLocaleString();
|
|
const percent = (100 * val) / data.sum;
|
|
$("#" + prefix + key).text(lval + " (" + percent.toFixed(1) + "%)");
|
|
} else {
|
|
const lval = val.toLocaleString();
|
|
$("#" + prefix + key).text(lval);
|
|
}
|
|
}
|
|
|
|
// Draw pie chart if data is available
|
|
if (Object.keys(cacheData).length > 0) {
|
|
updateCachePie(cacheData);
|
|
}
|
|
}
|
|
|
|
let metricsTimer = null;
|
|
|
|
function updateMetrics() {
|
|
$.ajax({
|
|
url: document.body.dataset.apiurl + "/info/metrics",
|
|
})
|
|
.done(data => {
|
|
const metrics = data.metrics;
|
|
$("#dns-cache-table").empty();
|
|
|
|
// Set global cache size
|
|
cacheSize = metrics.dns.cache.size;
|
|
|
|
// Set metrics
|
|
setMetrics(metrics, "sysinfo-");
|
|
|
|
$("#cache-utilization").text(
|
|
cacheEntries.toLocaleString() + " (" + ((100 * cacheEntries) / cacheSize).toFixed(1) + "%)"
|
|
);
|
|
|
|
$("div[id^='sysinfo-metrics-overlay']").hide();
|
|
clearTimeout(metricsTimer);
|
|
metricsTimer = utils.setTimer(updateMetrics, REFRESH_INTERVAL.metrics);
|
|
})
|
|
.fail(data => {
|
|
apiFailure(data);
|
|
});
|
|
}
|
|
|
|
function showQueryLoggingButton(state) {
|
|
if (state) {
|
|
$("#loggingButton").addClass("btn-warning");
|
|
$("#loggingButton").removeClass("btn-success");
|
|
$("#loggingButton").text(i18n.t("settings.system.disable_query_logging"));
|
|
$("#loggingButton").data("state", "enabled");
|
|
} else {
|
|
$("#loggingButton").addClass("btn-success");
|
|
$("#loggingButton").removeClass("btn-warning");
|
|
$("#loggingButton").text(i18n.t("settings.system.enable_query_logging"));
|
|
$("#loggingButton").data("state", "disabled");
|
|
}
|
|
}
|
|
|
|
function getLoggingButton() {
|
|
$.ajax({
|
|
url: document.body.dataset.apiurl + "/config/dns/queryLogging",
|
|
})
|
|
.done(data => {
|
|
showQueryLoggingButton(data.config.dns.queryLogging);
|
|
})
|
|
.fail(data => {
|
|
apiFailure(data);
|
|
});
|
|
}
|
|
|
|
i18n.waitForTranslations(() => {
|
|
$(".confirm-restartdns").confirm({
|
|
text:
|
|
i18n.t("settings.system.sure_restart_dns_server") +
|
|
"<br><br>" +
|
|
i18n.t("settings.system.this_interrupt_internet") +
|
|
"<br>" +
|
|
i18n.t("settings.system.you_will_be_logged_out"),
|
|
title: i18n.t("settings.confirmation_required"),
|
|
confirm() {
|
|
$.ajax({
|
|
url: document.body.dataset.apiurl + "/action/restartdns",
|
|
type: "POST",
|
|
}).fail(data => {
|
|
apiFailure(data);
|
|
});
|
|
},
|
|
cancel() {
|
|
// nothing to do
|
|
},
|
|
confirmButton: i18n.t("settings.system.restart_confirm"),
|
|
cancelButton: i18n.t("settings.system.restart_deny"),
|
|
post: true,
|
|
confirmButtonClass: "btn-danger",
|
|
cancelButtonClass: "btn-default",
|
|
dialogClass: "modal-dialog",
|
|
});
|
|
|
|
$(".confirm-flushlogs").confirm({
|
|
text:
|
|
i18n.t("settings.system.sure_to_flush_logs") +
|
|
"<br><br>" +
|
|
`<strong>${i18n.t("settings.system.flush_logs_cannot_be_undone")}</strong>`,
|
|
title: i18n.t("settings.confirmation_required"),
|
|
confirm() {
|
|
$.ajax({
|
|
url: document.body.dataset.apiurl + "/action/flush/logs",
|
|
type: "POST",
|
|
}).fail(data => {
|
|
apiFailure(data);
|
|
});
|
|
},
|
|
cancel() {
|
|
// nothing to do
|
|
},
|
|
confirmButton: i18n.t("settings.system.flush_logs_confirm"),
|
|
cancelButton: i18n.t("settings.system.flush_logs_deny"),
|
|
post: true,
|
|
confirmButtonClass: "btn-danger",
|
|
cancelButtonClass: "btn-default",
|
|
dialogClass: "modal-dialog",
|
|
});
|
|
|
|
$(".confirm-flusharp").confirm({
|
|
text:
|
|
i18n.t("settings.system.sure_to_flush_network_table") +
|
|
"<br><br>" +
|
|
`<strong>${i18n.t("settings.system.flush_table_cannot_be_undone")}</strong>`,
|
|
title: i18n.t("settings.confirmation_required"),
|
|
confirm() {
|
|
$.ajax({
|
|
url: document.body.dataset.apiurl + "/action/flush/arp",
|
|
type: "POST",
|
|
}).fail(data => {
|
|
apiFailure(data);
|
|
});
|
|
},
|
|
cancel() {
|
|
// nothing to do
|
|
},
|
|
confirmButton: i18n.t("settings.system.flush_table_confirm"),
|
|
cancelButton: i18n.t("settings.system.flush_table_deny"),
|
|
post: true,
|
|
confirmButtonClass: "btn-danger",
|
|
cancelButtonClass: "btn-default",
|
|
dialogClass: "modal-dialog",
|
|
});
|
|
|
|
$("#loggingButton").confirm({
|
|
text:
|
|
i18n.t("settings.system.sure_to_switch_query_logging_mode") +
|
|
"<br><br>" +
|
|
"<strong>" +
|
|
i18n.t("settings.system.dns_server_will_restart") +
|
|
"</strong><br>" +
|
|
i18n.t("settings.system.cache_clear_and_interrupt_internet") +
|
|
"<br>" +
|
|
i18n.t("settings.system.furthermore_you_will_be_logged_out"),
|
|
title: i18n.t("settings.confirmation_required"),
|
|
confirm() {
|
|
const data = {};
|
|
data.config = {};
|
|
data.config.dns = {};
|
|
data.config.dns.queryLogging = $("#loggingButton").data("state") !== "enabled";
|
|
$.ajax({
|
|
url: document.body.dataset.apiurl + "/config/dns/queryLogging",
|
|
type: "PATCH",
|
|
dataType: "json",
|
|
processData: false,
|
|
contentType: "application/json; charset=utf-8",
|
|
data: JSON.stringify(data),
|
|
})
|
|
.done(data => {
|
|
showQueryLoggingButton(data.config.dns.queryLogging);
|
|
})
|
|
.fail(data => {
|
|
apiFailure(data);
|
|
});
|
|
},
|
|
cancel() {
|
|
// nothing to do
|
|
},
|
|
confirmButton: i18n.t("settings.system.change_logging_confirm"),
|
|
cancelButton: i18n.t("settings.system.change_logging_deny"),
|
|
post: true,
|
|
confirmButtonClass: "btn-danger",
|
|
cancelButtonClass: "btn-default",
|
|
dialogClass: "modal-dialog",
|
|
});
|
|
|
|
updateHostInfo();
|
|
updateMetrics();
|
|
getLoggingButton();
|
|
|
|
const ctx = document.getElementById("cachePieChart").getContext("2d");
|
|
cachePieChart = new Chart(ctx, {
|
|
type: "doughnut",
|
|
data: {
|
|
labels: [],
|
|
datasets: [{ data: [], parsing: false }],
|
|
},
|
|
plugins: [htmlLegendPlugin],
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: true,
|
|
elements: {
|
|
arc: {
|
|
borderColor: $(".box").css("background-color"),
|
|
},
|
|
},
|
|
plugins: {
|
|
htmlLegend: {
|
|
containerID: "cache-legend",
|
|
},
|
|
legend: {
|
|
display: false,
|
|
},
|
|
tooltip: {
|
|
// Disable the on-canvas tooltip
|
|
enabled: false,
|
|
external: customTooltips,
|
|
callbacks: {
|
|
title() {
|
|
return i18n.t("settings.system.cache_content");
|
|
},
|
|
label: doughnutTooltip,
|
|
},
|
|
},
|
|
},
|
|
animation: {
|
|
duration: 750,
|
|
},
|
|
},
|
|
});
|
|
|
|
$.ajax({
|
|
url: document.body.dataset.apiurl + "/network/gateway",
|
|
})
|
|
.done(data => {
|
|
const gateway = data.gateway;
|
|
// Get first object in gateway that has family == "inet"
|
|
const inet = gateway.find(obj => obj.family === "inet");
|
|
// Get first object in gateway that has family == "inet6"
|
|
const inet6 = gateway.find(obj => obj.family === "inet6");
|
|
|
|
$("#sysinfo-gw-v4-addr").text(inet ? inet.local.join("\n") : "N/A");
|
|
$("#sysinfo-gw-v4-iface").text(inet ? inet.interface : "N/A");
|
|
$("#sysinfo-gw-v6-addr").text(inet6 ? inet6.local.join("\n") : "N/A");
|
|
$("#sysinfo-gw-v6-iface").text(inet6 ? inet6.interface : "N/A");
|
|
})
|
|
.fail(data => {
|
|
apiFailure(data);
|
|
});
|
|
});
|