mirror of
https://github.com/pi-hole/web.git
synced 2026-01-09 06:20:56 +08:00
Merge branch 'development-v6' into fix/adlist_list
This commit is contained in:
commit
be21f650f1
2
.github/workflows/codeql.yml
vendored
2
.github/workflows/codeql.yml
vendored
@ -21,7 +21,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4.1.0
|
||||
uses: actions/checkout@v4.1.1
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
|
||||
2
.github/workflows/codespell.yml
vendored
2
.github/workflows/codespell.yml
vendored
@ -11,7 +11,7 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout repository
|
||||
uses: actions/checkout@v4.1.0
|
||||
uses: actions/checkout@v4.1.1
|
||||
-
|
||||
name: Spell-Checking
|
||||
uses: codespell-project/actions-codespell@master
|
||||
|
||||
2
.github/workflows/editorconfig-checker.yml
vendored
2
.github/workflows/editorconfig-checker.yml
vendored
@ -9,6 +9,6 @@ jobs:
|
||||
name: editorconfig-checker
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: actions/checkout@v4.1.1
|
||||
- uses: editorconfig-checker/action-editorconfig-checker@main
|
||||
- run: editorconfig-checker
|
||||
|
||||
2
.github/workflows/sync-back-to-dev.yml
vendored
2
.github/workflows/sync-back-to-dev.yml
vendored
@ -11,7 +11,7 @@ jobs:
|
||||
name: Syncing branches
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4.1.0
|
||||
uses: actions/checkout@v4.1.1
|
||||
- name: Opening pull request
|
||||
run: gh pr create -B devel -H master --title 'Sync master back into development' --body 'Created by Github action' --label 'internal'
|
||||
env:
|
||||
|
||||
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
@ -19,10 +19,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Clone repository
|
||||
uses: actions/checkout@v4.1.0
|
||||
uses: actions/checkout@v4.1.1
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v3.8.1
|
||||
uses: actions/setup-node@v4.0.0
|
||||
with:
|
||||
node-version: "16.x"
|
||||
cache: npm
|
||||
|
||||
4
index.lp
4
index.lp
@ -22,8 +22,8 @@ mg.include('scripts/pi-hole/lua/header_authenticated.lp','r')
|
||||
<div class="icon">
|
||||
<i class="fas fa-globe-americas"></i>
|
||||
</div>
|
||||
<a href="network.lp" class="small-box-footer" title="">
|
||||
<span id="total_clients">-</span> active clients <i class="fa fa-arrow-circle-right"></i>
|
||||
<a href="network.lp" class="small-box-footer" title="" id="total_clients">
|
||||
<span id="active_clients">-</span> active clients <i class="fa fa-arrow-circle-right"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -165,6 +165,7 @@ mg.include('scripts/pi-hole/lua/header_authenticated.lp','r')
|
||||
<th>Type</th>
|
||||
<th>Domain</th>
|
||||
<th>Client</th>
|
||||
<th><i class="fas fa-stopwatch" title="Query reply time"></i></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
@ -175,6 +176,7 @@ mg.include('scripts/pi-hole/lua/header_authenticated.lp','r')
|
||||
<th>Type</th>
|
||||
<th>Domain</th>
|
||||
<th>Client</th>
|
||||
<th><i class="fas fa-stopwatch" title="Query reply time"></i></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</tfoot>
|
||||
@ -192,7 +194,6 @@ mg.include('scripts/pi-hole/lua/header_authenticated.lp','r')
|
||||
<script src="<?=pihole.fileversion('scripts/vendor/bootstrap-select.min.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('scripts/pi-hole/js/ip-address-sorting.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('scripts/vendor/daterangepicker.min.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('scripts/pi-hole/js/utils.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('scripts/pi-hole/js/queries.js')?>"></script>
|
||||
|
||||
<? mg.include('scripts/pi-hole/lua/footer.lp','r')?>
|
||||
|
||||
@ -87,7 +87,6 @@ const htmlLegendPlugin = {
|
||||
});
|
||||
}
|
||||
|
||||
textLink.style.color = item.fontColor;
|
||||
textLink.style.margin = 0;
|
||||
textLink.style.padding = 0;
|
||||
textLink.style.textDecoration = item.hidden ? "line-through" : "";
|
||||
@ -103,11 +102,11 @@ const htmlLegendPlugin = {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
var customTooltips = function (context) {
|
||||
var tooltip = context.tooltip;
|
||||
var tooltipEl = document.getElementById(this._chart.canvas.id + "-customTooltip");
|
||||
var tooltipEl = document.getElementById(this.chart.canvas.id + "-customTooltip");
|
||||
if (!tooltipEl) {
|
||||
// Create Tooltip Element once per chart
|
||||
tooltipEl = document.createElement("div");
|
||||
tooltipEl.id = this._chart.canvas.id + "-customTooltip";
|
||||
tooltipEl.id = this.chart.canvas.id + "-customTooltip";
|
||||
tooltipEl.classList.add("chartjs-tooltip");
|
||||
tooltipEl.innerHTML = "<div class='arrow'></div> <table></table>";
|
||||
// avoid browser's font-zoom since we know that <body>'s
|
||||
@ -121,7 +120,7 @@ var customTooltips = function (context) {
|
||||
tooltipEl.style.fontSize = tooltip.options.bodyFont.size / fontZoom + "px";
|
||||
tooltipEl.style.fontStyle = tooltip.options.bodyFont.style;
|
||||
// append Tooltip next to canvas-containing box
|
||||
tooltipEl.ancestor = this._chart.canvas.closest(".box[id]").parentNode;
|
||||
tooltipEl.ancestor = this.chart.canvas.closest(".box[id]").parentNode;
|
||||
tooltipEl.ancestor.append(tooltipEl);
|
||||
}
|
||||
|
||||
@ -175,7 +174,7 @@ var customTooltips = function (context) {
|
||||
tableRoot.innerHTML = innerHtml;
|
||||
}
|
||||
|
||||
var canvasPos = this._chart.canvas.getBoundingClientRect();
|
||||
var canvasPos = this.chart.canvas.getBoundingClientRect();
|
||||
var boxPos = tooltipEl.ancestor.getBoundingClientRect();
|
||||
var offsetX = canvasPos.left - boxPos.left;
|
||||
var offsetY = canvasPos.top - boxPos.top;
|
||||
|
||||
@ -405,34 +405,47 @@ function delItems(ids) {
|
||||
}
|
||||
|
||||
function addClient() {
|
||||
var ip = utils.escapeHtml($("#select").val().trim());
|
||||
const comment = utils.escapeHtml($("#new_comment").val());
|
||||
|
||||
utils.disableAll();
|
||||
utils.showAlert("info", "", "Adding client...", ip);
|
||||
|
||||
if (ip.length === 0) {
|
||||
utils.enableAll();
|
||||
utils.showAlert("warning", "", "Warning", "Please specify a client IP or MAC address");
|
||||
return;
|
||||
}
|
||||
// Check if the user wants to add multiple IPs (space or newline separated)
|
||||
// If so, split the input and store it in an array
|
||||
var ips = utils.escapeHtml($("#select").val().trim()).split(/[\s,]+/);
|
||||
// Remove empty elements
|
||||
ips = ips.filter(function (el) {
|
||||
return el !== "";
|
||||
});
|
||||
const ipStr = JSON.stringify(ips);
|
||||
|
||||
// Validate input, can be:
|
||||
// - IPv4 address (with and without CIDR)
|
||||
// - IPv6 address (with and without CIDR)
|
||||
// - MAC address (in the form AA:BB:CC:DD:EE:FF)
|
||||
// - host name (arbitrary form, we're only checking against some reserved characters)
|
||||
if (utils.validateIPv4CIDR(ip) || utils.validateIPv6CIDR(ip) || utils.validateMAC(ip)) {
|
||||
// Convert input to upper case (important for MAC addresses)
|
||||
ip = ip.toUpperCase();
|
||||
} else if (!utils.validateHostname(ip)) {
|
||||
for (var i = 0; i < ips.length; i++) {
|
||||
if (
|
||||
utils.validateIPv4CIDR(ips[i]) ||
|
||||
utils.validateIPv6CIDR(ips[i]) ||
|
||||
utils.validateMAC(ips[i])
|
||||
) {
|
||||
// Convert input to upper case (important for MAC addresses)
|
||||
ips[i] = ips[i].toUpperCase();
|
||||
} else if (!utils.validateHostname(ips[i])) {
|
||||
utils.showAlert(
|
||||
"warning",
|
||||
"",
|
||||
"Warning",
|
||||
"Input is neither a valid IP or MAC address nor a valid host name!"
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
utils.disableAll();
|
||||
utils.showAlert("info", "", "Adding client(s)...", ipStr);
|
||||
|
||||
if (ips.length === 0) {
|
||||
utils.enableAll();
|
||||
utils.showAlert(
|
||||
"warning",
|
||||
"",
|
||||
"Warning",
|
||||
"Input is neither a valid IP or MAC address nor a valid host name!"
|
||||
);
|
||||
utils.showAlert("warning", "", "Warning", "Please specify a client IP or MAC address");
|
||||
return;
|
||||
}
|
||||
|
||||
@ -441,10 +454,10 @@ function addClient() {
|
||||
method: "post",
|
||||
dataType: "json",
|
||||
processData: false,
|
||||
data: JSON.stringify({ client: ip, comment: comment }),
|
||||
success: function () {
|
||||
data: JSON.stringify({ client: ips, comment: comment }),
|
||||
success: function (data) {
|
||||
utils.enableAll();
|
||||
utils.showAlert("success", "fas fa-plus", "Successfully added client", ip);
|
||||
utils.listsAlert("client", ips, data);
|
||||
reloadClientSuggestions();
|
||||
table.ajax.reload(null, false);
|
||||
table.rows().deselect();
|
||||
|
||||
@ -497,46 +497,54 @@ function addDomain() {
|
||||
commentEl = $("#new_regex_comment");
|
||||
}
|
||||
|
||||
var domain = utils.escapeHtml(domainEl.val());
|
||||
const comment = utils.escapeHtml(commentEl.val());
|
||||
|
||||
utils.disableAll();
|
||||
utils.showAlert("info", "", "Adding domain...", domain);
|
||||
// Check if the user wants to add multiple domains (space or newline separated)
|
||||
// If so, split the input and store it in an array
|
||||
var domains = utils.escapeHtml(domainEl.val()).split(/[\s,]+/);
|
||||
// Remove empty elements
|
||||
domains = domains.filter(function (el) {
|
||||
return el !== "";
|
||||
});
|
||||
const domainStr = JSON.stringify(domains);
|
||||
|
||||
if (domain.length < 2) {
|
||||
utils.disableAll();
|
||||
utils.showAlert("info", "", "Adding domain(s)...", domainStr);
|
||||
|
||||
if (domains.length === 0) {
|
||||
utils.enableAll();
|
||||
utils.showAlert("warning", "", "Warning", "Please specify a domain");
|
||||
utils.showAlert("warning", "", "Warning", "Please specify at least one domain");
|
||||
return;
|
||||
}
|
||||
|
||||
// strip "*." if specified by user in wildcard mode
|
||||
if (kind === "exact" && wildcardChecked && domain.startsWith("*.")) {
|
||||
domain = domain.substr(2);
|
||||
for (var i = 0; i < domains.length; i++) {
|
||||
if (kind === "exact" && wildcardChecked) {
|
||||
// Transform domain to wildcard if specified by user
|
||||
domains[i] = "(\\.|^)" + domains[i].replaceAll(".", "\\.") + "$";
|
||||
kind = "regex";
|
||||
|
||||
// strip leading "*." if specified by user in wildcard mode
|
||||
if (domains[i].startsWith("*.")) domains[i] = domains[i].substr(2);
|
||||
}
|
||||
}
|
||||
|
||||
// determine list type
|
||||
const type = action === "add_deny" ? "deny" : "allow";
|
||||
|
||||
// Transform domain to wildcard if specified by user
|
||||
if (kind === "exact" && wildcardChecked) {
|
||||
domain = "(\\.|^)" + domain.replaceAll(".", "\\.") + "$";
|
||||
kind = "regex";
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: "/api/domains/" + type + "/" + kind,
|
||||
method: "post",
|
||||
dataType: "json",
|
||||
processData: false,
|
||||
data: JSON.stringify({
|
||||
domain: domain,
|
||||
domain: domains,
|
||||
comment: comment,
|
||||
type: type,
|
||||
kind: kind,
|
||||
}),
|
||||
success: function () {
|
||||
success: function (data) {
|
||||
utils.enableAll();
|
||||
utils.showAlert("success", "fas fa-plus", "Successfully added domain", domain);
|
||||
utils.listsAlert("domain", domains, data);
|
||||
table.ajax.reload(null, false);
|
||||
table.rows().deselect();
|
||||
|
||||
|
||||
@ -499,13 +499,21 @@ function delItems(ids) {
|
||||
|
||||
function addList(event) {
|
||||
const type = event.data.type;
|
||||
const address = utils.escapeHtml($("#new_address").val());
|
||||
const comment = utils.escapeHtml($("#new_comment").val());
|
||||
|
||||
utils.disableAll();
|
||||
utils.showAlert("info", "", "Adding subscribed " + type + "list...", address);
|
||||
// Check if the user wants to add multiple domains (space or newline separated)
|
||||
// If so, split the input and store it in an array
|
||||
var addresses = utils.escapeHtml($("#new_address").val()).split(/[\s,]+/);
|
||||
// Remove empty elements
|
||||
addresses = addresses.filter(function (el) {
|
||||
return el !== "";
|
||||
});
|
||||
const addressestr = JSON.stringify(addresses);
|
||||
|
||||
if (address.length === 0) {
|
||||
utils.disableAll();
|
||||
utils.showAlert("info", "", "Adding subscribed " + type + "list(s)...", addressestr);
|
||||
|
||||
if (addresses.length === 0) {
|
||||
// enable the ui elements again
|
||||
utils.enableAll();
|
||||
utils.showAlert("warning", "", "Warning", "Please specify " + type + "list address");
|
||||
@ -517,10 +525,10 @@ function addList(event) {
|
||||
method: "post",
|
||||
dataType: "json",
|
||||
processData: false,
|
||||
data: JSON.stringify({ address: address, comment: comment, type: type }),
|
||||
success: function () {
|
||||
data: JSON.stringify({ address: addresses, comment: comment, type: type }),
|
||||
success: function (data) {
|
||||
utils.enableAll();
|
||||
utils.showAlert("success", "fas fa-plus", "Successfully added " + type + "list", address);
|
||||
utils.listsAlert("list", addresses, data);
|
||||
table.ajax.reload(null, false);
|
||||
table.rows().deselect();
|
||||
|
||||
|
||||
@ -277,13 +277,24 @@ function delItems(ids) {
|
||||
}
|
||||
|
||||
function addGroup() {
|
||||
const name = utils.escapeHtml($("#new_name").val());
|
||||
const comment = utils.escapeHtml($("#new_comment").val());
|
||||
|
||||
utils.disableAll();
|
||||
utils.showAlert("info", "", "Adding group...", name);
|
||||
// Check if the user wants to add multiple groups (space or newline separated)
|
||||
// If so, split the input and store it in an array
|
||||
var names = utils
|
||||
.escapeHtml($("#new_name"))
|
||||
.val()
|
||||
.split(/[\s,]+/);
|
||||
// Remove empty elements
|
||||
names = names.filter(function (el) {
|
||||
return el !== "";
|
||||
});
|
||||
const groupStr = JSON.stringify(names);
|
||||
|
||||
if (name.length === 0) {
|
||||
utils.disableAll();
|
||||
utils.showAlert("info", "", "Adding group(s)...", groupStr);
|
||||
|
||||
if (names.length === 0) {
|
||||
// enable the ui elements again
|
||||
utils.enableAll();
|
||||
utils.showAlert("warning", "", "Warning", "Please specify a group name");
|
||||
@ -296,13 +307,13 @@ function addGroup() {
|
||||
dataType: "json",
|
||||
processData: false,
|
||||
data: JSON.stringify({
|
||||
name: name,
|
||||
name: names,
|
||||
comment: comment,
|
||||
enabled: true,
|
||||
}),
|
||||
success: function () {
|
||||
success: function (data) {
|
||||
utils.enableAll();
|
||||
utils.showAlert("success", "fas fa-plus", "Successfully added group", name);
|
||||
utils.listsAlert("group", names, data);
|
||||
$("#new_name").val("");
|
||||
$("#new_comment").val("");
|
||||
table.ajax.reload();
|
||||
|
||||
@ -16,6 +16,13 @@ var queryTypePieChart, forwardDestinationPieChart;
|
||||
var failures = 0;
|
||||
function updateQueriesOverTime() {
|
||||
$.getJSON("/api/history", function (data) {
|
||||
// Remove graph if there are no results (e.g. new
|
||||
// installation or privacy mode enabled)
|
||||
if (jQuery.isEmptyObject(data.history)) {
|
||||
$("#queries-over-time").remove();
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove possibly already existing data
|
||||
timeLineChart.data.labels = [];
|
||||
timeLineChart.data.datasets = [];
|
||||
@ -123,13 +130,10 @@ function updateClientsOverTime() {
|
||||
// Remove graph if there are no results (e.g. new
|
||||
// installation or privacy mode enabled)
|
||||
if (jQuery.isEmptyObject(data.history)) {
|
||||
$("#clients-over-time").parent().remove();
|
||||
$("#clients").remove();
|
||||
return;
|
||||
}
|
||||
|
||||
// remove last data point for line charts as it is not representative there
|
||||
if (utils.getGraphType() === "line") data.history.splice(-1, 1);
|
||||
|
||||
var i,
|
||||
labels = [];
|
||||
data.clients.forEach(function (client) {
|
||||
@ -384,7 +388,11 @@ function updateSummaryData(runOnce) {
|
||||
$.getJSON("/api/stats/summary", function (data) {
|
||||
var intl = new Intl.NumberFormat();
|
||||
glowIfChanged($("span#dns_queries"), intl.format(parseInt(data.queries.total, 10)));
|
||||
glowIfChanged($("span#total_clients"), intl.format(parseInt(data.clients.total, 10)));
|
||||
glowIfChanged($("span#active_clients"), intl.format(parseInt(data.clients.active, 10)));
|
||||
$("a#total_clients").attr(
|
||||
"title",
|
||||
intl.format(parseInt(data.clients.total, 10)) + " total clients"
|
||||
);
|
||||
glowIfChanged($("span#blocked_queries"), intl.format(parseFloat(data.queries.blocked)));
|
||||
glowIfChanged(
|
||||
$("span#percent_blocked"),
|
||||
@ -416,7 +424,7 @@ $(function () {
|
||||
var ticksColor = utils.getCSSval("graphs-ticks", "color");
|
||||
var ctx = document.getElementById("queryOverTimeChart").getContext("2d");
|
||||
timeLineChart = new Chart(ctx, {
|
||||
type: utils.getGraphType(),
|
||||
type: "bar",
|
||||
data: {
|
||||
labels: [],
|
||||
datasets: [{ data: [], parsing: false }],
|
||||
@ -471,7 +479,7 @@ $(function () {
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
xAxes: {
|
||||
x: {
|
||||
type: "time",
|
||||
stacked: true,
|
||||
offset: false,
|
||||
@ -491,7 +499,7 @@ $(function () {
|
||||
color: ticksColor,
|
||||
},
|
||||
},
|
||||
yAxes: {
|
||||
y: {
|
||||
stacked: true,
|
||||
beginAtZero: true,
|
||||
ticks: {
|
||||
@ -527,7 +535,7 @@ $(function () {
|
||||
if (clientsChartEl) {
|
||||
ctx = clientsChartEl.getContext("2d");
|
||||
clientsChart = new Chart(ctx, {
|
||||
type: utils.getGraphType(),
|
||||
type: "bar",
|
||||
data: {
|
||||
labels: [],
|
||||
datasets: [{ data: [], parsing: false }],
|
||||
@ -566,7 +574,7 @@ $(function () {
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
xAxes: {
|
||||
x: {
|
||||
type: "time",
|
||||
stacked: true,
|
||||
offset: false,
|
||||
@ -586,7 +594,7 @@ $(function () {
|
||||
color: ticksColor,
|
||||
},
|
||||
},
|
||||
yAxes: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
ticks: {
|
||||
color: ticksColor,
|
||||
|
||||
@ -208,6 +208,23 @@ function parseQueryStatus(data) {
|
||||
};
|
||||
}
|
||||
|
||||
function formatReplyTime(replyTime, type) {
|
||||
if (type === "display") {
|
||||
// Units:
|
||||
// - seconds if replytime >= 1 second
|
||||
// - milliseconds if reply time >= 100 µs
|
||||
// - microseconds otherwise
|
||||
return replyTime < 1e-4
|
||||
? (1e6 * replyTime).toFixed(1) + " µs"
|
||||
: replyTime < 1
|
||||
? (1e3 * replyTime).toFixed(1) + " ms"
|
||||
: replyTime.toFixed(1) + " s";
|
||||
}
|
||||
|
||||
// else: return the number itself (for sorting and searching)
|
||||
return replyTime;
|
||||
}
|
||||
|
||||
function formatInfo(data) {
|
||||
// DNSSEC status
|
||||
var dnssecStatus = data.dnssec,
|
||||
@ -303,20 +320,10 @@ function formatInfo(data) {
|
||||
|
||||
// Always show reply info, add reply delay if applicable
|
||||
var replyInfo = "";
|
||||
if (data.reply.type !== "UNKNOWN") {
|
||||
replyInfo = divStart + "Reply:  " + data.reply.type;
|
||||
if (data.reply.time >= 0 && data.reply.type !== "UNKNOWN") {
|
||||
replyInfo +=
|
||||
" (" +
|
||||
(data.reply.time < 1
|
||||
? (1e3 * data.reply.time).toFixed(1) + " ms)"
|
||||
: data.reply.time.toFixed(1) + " s)");
|
||||
}
|
||||
|
||||
replyInfo += "</div>";
|
||||
} else {
|
||||
replyInfo = divStart + "Reply: No reply received</div>";
|
||||
}
|
||||
replyInfo =
|
||||
data.reply.type !== "UNKNOWN"
|
||||
? divStart + "Reply:  " + data.reply.type + "</div>"
|
||||
: divStart + "Reply: No reply received</div>";
|
||||
|
||||
// Compile extra info for displaying
|
||||
return (
|
||||
@ -515,9 +522,10 @@ $(function () {
|
||||
},
|
||||
},
|
||||
{ data: "status", width: "1%" },
|
||||
{ data: "type", width: "5%" },
|
||||
{ data: "type", width: "1%" },
|
||||
{ data: "domain", width: "45%" },
|
||||
{ data: "client.ip", width: "29%", type: "ip-address", render: $.fn.dataTable.render.text() },
|
||||
{ data: "client.ip", width: "29%", type: "ip-address" },
|
||||
{ data: "reply.time", width: "4%", render: formatReplyTime },
|
||||
{ data: null, width: "10%", sortable: false, searchable: false },
|
||||
],
|
||||
lengthMenu: [
|
||||
@ -569,7 +577,7 @@ $(function () {
|
||||
}
|
||||
|
||||
if (querystatus.buttontext !== false) {
|
||||
$("td:eq(5)", row).html(querystatus.buttontext);
|
||||
$("td:eq(6)", row).html(querystatus.buttontext);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@ -9,6 +9,7 @@
|
||||
|
||||
var apiSessionsTable = null;
|
||||
var ownSessionID = null;
|
||||
var deleted = 0;
|
||||
var TOTPdata = null;
|
||||
|
||||
function renderBool(data, type) {
|
||||
@ -60,7 +61,7 @@ $(function () {
|
||||
},
|
||||
],
|
||||
drawCallback: function () {
|
||||
$('button[id^="deleteSession_"]').on("click", deleteSession);
|
||||
$('button[id^="deleteSession_"]').on("click", deleteThisSession);
|
||||
|
||||
// Hide buttons if all messages were deleted
|
||||
var hasRows = this.api().rows({ filter: "applied" }).data().length > 0;
|
||||
@ -70,7 +71,7 @@ $(function () {
|
||||
$("body > .bootstrap-select.dropdown").remove();
|
||||
},
|
||||
rowCallback: function (row, data) {
|
||||
$(row).attr("data-id", data.ip);
|
||||
$(row).attr("data-id", data.id);
|
||||
var button =
|
||||
'<button type="button" class="btn btn-danger btn-xs" id="deleteSession_' +
|
||||
data.id +
|
||||
@ -148,7 +149,7 @@ $(function () {
|
||||
ids.push(parseInt($(this).attr("data-id"), 10));
|
||||
});
|
||||
// Delete all selected rows at once
|
||||
delSession(ids);
|
||||
deleteMultipleSessions(ids);
|
||||
},
|
||||
},
|
||||
],
|
||||
@ -186,13 +187,18 @@ $(function () {
|
||||
});
|
||||
});
|
||||
|
||||
function deleteSession() {
|
||||
// Passes the button data-del-id attribute as ID
|
||||
var ids = [$(this).attr("data-del-id")];
|
||||
delSessions(ids);
|
||||
function deleteThisSession() {
|
||||
// This function is called when a red trash button is clicked
|
||||
// We get the ID of the current item from the data-del-id attribute
|
||||
const thisID = parseInt(this.attr("data-del-id"), 10);
|
||||
deleted = 0;
|
||||
deleteOneSession(thisID, 1, false);
|
||||
}
|
||||
|
||||
function delSessions(ids) {
|
||||
function deleteMultipleSessions(ids) {
|
||||
// This function is called when multiple sessions are selected and the gray
|
||||
// trash button is clicked
|
||||
|
||||
// Check input validity
|
||||
if (!Array.isArray(ids)) return;
|
||||
|
||||
@ -206,9 +212,12 @@ function delSessions(ids) {
|
||||
return parseInt(value, 10);
|
||||
});
|
||||
|
||||
// Check if own session is selected
|
||||
// Check if own session is selected and remove it when deleting multiple
|
||||
// We need this only when multiple sessions are removed to ensure we do not
|
||||
// accidentally remove our own session and thus log us out *before* we can
|
||||
// remove the other sessions
|
||||
let ownSessionDelete = false;
|
||||
if (ids.includes(ownSessionID)) {
|
||||
if (ids.includes(ownSessionID) && ids.length > 1) {
|
||||
ownSessionDelete = true;
|
||||
// Strip own session ID from array
|
||||
ids = ids.filter(function (value) {
|
||||
@ -217,24 +226,29 @@ function delSessions(ids) {
|
||||
}
|
||||
|
||||
// Loop through IDs and delete them
|
||||
deleted = 0;
|
||||
for (const id of ids) {
|
||||
if (Object.hasOwnProperty.call(ids, id)) {
|
||||
delSession(ids[id]);
|
||||
}
|
||||
}
|
||||
|
||||
// Delete own session last (if selected)
|
||||
if (ownSessionDelete) {
|
||||
delSession(ownSessionID);
|
||||
deleteOneSession(id, ids.length, ownSessionDelete);
|
||||
}
|
||||
}
|
||||
|
||||
function delSession(id) {
|
||||
function deleteOneSession(id, len, ownSessionDelete) {
|
||||
// This function is called to delete a single session
|
||||
// If we are batch deleting, we ensure that we do not delete our own session
|
||||
// before having successfully deleted all other sessions, the deletion of
|
||||
// our own session is then triggered by the last successful deletion of
|
||||
// another session (ownSessionDelete == true, len == global deleted)
|
||||
$.ajax({
|
||||
url: "/api/auth/session/" + id,
|
||||
method: "DELETE",
|
||||
})
|
||||
.done(function () {
|
||||
// Do not reload page when deleting multiple sessions
|
||||
if (++deleted < len) return;
|
||||
|
||||
// All other sessions have been deleted, now delete own session
|
||||
if (ownSessionDelete) deleteOneSession(ownSessionID, 1, false);
|
||||
|
||||
if (id !== ownSessionID) {
|
||||
// Reload table to remove session
|
||||
apiSessionsTable.ajax.reload();
|
||||
|
||||
@ -10,6 +10,36 @@
|
||||
var dnsRecordsTable = null;
|
||||
var customCNAMETable = null;
|
||||
|
||||
function hostsDomain(data) {
|
||||
// Split record in format IP NAME1 [NAME2 [NAME3 [NAME...]]]
|
||||
const name = data.substring(data.indexOf(" ") + 1);
|
||||
return name;
|
||||
}
|
||||
|
||||
function hostsIP(data) {
|
||||
// Split record in format IP NAME1 [NAME2 [NAME3 [NAME...]]]
|
||||
const ip = data.substring(0, data.indexOf(" "));
|
||||
return ip;
|
||||
}
|
||||
|
||||
function CNAMEdomain(data) {
|
||||
// Split record in format <cname>,<target>[,<TTL>]
|
||||
const CNAMEarr = data.split(",");
|
||||
return CNAMEarr[0];
|
||||
}
|
||||
|
||||
function CNAMEtarget(data) {
|
||||
// Split record in format <cname>,<target>[,<TTL>]
|
||||
const CNAMEarr = data.split(",");
|
||||
return CNAMEarr[1];
|
||||
}
|
||||
|
||||
function CNAMEttl(data) {
|
||||
// Split record in format <cname>,<target>[,<TTL>]
|
||||
const CNAMEarr = data.split(",");
|
||||
return CNAMEarr.length > 2 ? CNAMEarr[2] : "-";
|
||||
}
|
||||
|
||||
$(function () {
|
||||
dnsRecordsTable = $("#dnsRecordsTable").DataTable({
|
||||
ajax: {
|
||||
@ -19,8 +49,8 @@ $(function () {
|
||||
},
|
||||
order: [[0, "asc"]],
|
||||
columns: [
|
||||
{ data: null },
|
||||
{ data: null, type: "ip-address" },
|
||||
{ data: null, render: hostsDomain },
|
||||
{ data: null, type: "ip-address", render: hostsIP },
|
||||
{ data: null, width: "22px", orderable: false },
|
||||
],
|
||||
columnDefs: [
|
||||
@ -36,11 +66,6 @@ $(function () {
|
||||
$("body > .bootstrap-select.dropdown").remove();
|
||||
},
|
||||
rowCallback: function (row, data) {
|
||||
// Split record in format IP NAME1 [NAME2 [NAME3 [NAME...]]]
|
||||
var ip = data.substring(0, data.indexOf(" "));
|
||||
// The name can be multiple domains separated by spaces
|
||||
var name = data.substring(data.indexOf(" ") + 1);
|
||||
|
||||
$(row).attr("data-id", data);
|
||||
var button =
|
||||
'<button type="button" class="btn btn-danger btn-xs" id="deleteRecord' +
|
||||
@ -51,8 +76,6 @@ $(function () {
|
||||
'<span class="far fa-trash-alt"></span>' +
|
||||
"</button>";
|
||||
$("td:eq(2)", row).html(button);
|
||||
$("td:eq(0)", row).text(name);
|
||||
$("td:eq(1)", row).text(ip);
|
||||
},
|
||||
dom:
|
||||
"<'row'<'col-sm-6'l><'col-sm-6'f>>" +
|
||||
@ -91,9 +114,9 @@ $(function () {
|
||||
},
|
||||
order: [[0, "asc"]],
|
||||
columns: [
|
||||
{ data: null },
|
||||
{ data: null },
|
||||
{ data: null },
|
||||
{ data: null, render: CNAMEdomain },
|
||||
{ data: null, render: CNAMEtarget },
|
||||
{ data: null, render: CNAMEttl },
|
||||
{ data: null, width: "22px", orderable: false },
|
||||
],
|
||||
columnDefs: [
|
||||
@ -109,9 +132,6 @@ $(function () {
|
||||
$("body > .bootstrap-select.dropdown").remove();
|
||||
},
|
||||
rowCallback: function (row, data) {
|
||||
// Split record in format <cname>,<target>[,<TTL>]
|
||||
var CNAMEarr = data.split(",");
|
||||
|
||||
$(row).attr("data-id", data);
|
||||
var button =
|
||||
'<button type="button" class="btn btn-danger btn-xs" id="deleteCNAME' +
|
||||
@ -122,10 +142,6 @@ $(function () {
|
||||
'<span class="far fa-trash-alt"></span>' +
|
||||
"</button>";
|
||||
$("td:eq(3)", row).html(button);
|
||||
$("td:eq(0)", row).text(CNAMEarr[0]);
|
||||
$("td:eq(1)", row).text(CNAMEarr[1]);
|
||||
if (CNAMEarr.length > 2) $("td:eq(2)", row).text(CNAMEarr[2]);
|
||||
else $("td:eq(2)", row).text("-");
|
||||
},
|
||||
dom:
|
||||
"<'row'<'col-sm-6'l><'col-sm-6'f>>" +
|
||||
@ -230,15 +246,17 @@ function delCNAME(elem) {
|
||||
|
||||
$(document).ready(function () {
|
||||
$("#btnAdd-host").on("click", function () {
|
||||
utils.disableAll();
|
||||
const elem = $("#Hip").val() + " " + $("#Hdomain").val();
|
||||
const url = "/api/config/dns/hosts/" + encodeURIComponent(elem);
|
||||
utils.showAlert("info", "", "Adding DNS record...", elem);
|
||||
$.ajax({
|
||||
url: url,
|
||||
method: "PUT",
|
||||
})
|
||||
.done(function () {
|
||||
utils.enableAll();
|
||||
utils.showAlert("success", "far fa-plus", "Successfully added DNS record", "");
|
||||
utils.showAlert("success", "fas fa-plus", "Successfully added DNS record", elem);
|
||||
dnsRecordsTable.ajax.reload(null, false);
|
||||
})
|
||||
.fail(function (data, exception) {
|
||||
@ -250,17 +268,19 @@ $(document).ready(function () {
|
||||
});
|
||||
|
||||
$("#btnAdd-cname").on("click", function () {
|
||||
utils.disableAll();
|
||||
var elem = $("#Cdomain").val() + "," + $("#Ctarget").val();
|
||||
var ttlVal = parseInt($("#Cttl").val(), 10);
|
||||
if (isFinite(ttlVal) && ttlVal >= 0) elem += "," + ttlVal;
|
||||
const url = "/api/config/dns/cnameRecords/" + encodeURIComponent(elem);
|
||||
utils.showAlert("info", "", "Adding DNS record...", elem);
|
||||
$.ajax({
|
||||
url: url,
|
||||
method: "PUT",
|
||||
})
|
||||
.done(function () {
|
||||
utils.enableAll();
|
||||
utils.showAlert("success", "far fa-plus", "Successfully added CNAME record", "");
|
||||
utils.showAlert("success", "fas fa-plus", "Successfully added CNAME record", elem);
|
||||
dnsRecordsTable.ajax.reload(null, false);
|
||||
})
|
||||
.fail(function (data, exception) {
|
||||
|
||||
@ -33,6 +33,7 @@ function importZIP() {
|
||||
fetch("/api/teleporter", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
headers: { "X-CSRF-TOKEN": $('meta[name="csrf-token"]').attr("content") },
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
@ -70,3 +71,27 @@ function importZIP() {
|
||||
console.error(error); // eslint-disable-line no-console
|
||||
});
|
||||
}
|
||||
|
||||
// Inspired by https://stackoverflow.com/a/59576416/2087442
|
||||
$("#GETTeleporter").on("click", function () {
|
||||
$.ajax({
|
||||
url: "/api/teleporter",
|
||||
headers: { "X-CSRF-TOKEN": $('meta[name="csrf-token"]').attr("content") },
|
||||
method: "GET",
|
||||
xhrFields: {
|
||||
responseType: "blob",
|
||||
},
|
||||
success: function (data, status, xhr) {
|
||||
var a = document.createElement("a");
|
||||
// eslint-disable-next-line compat/compat
|
||||
var url = window.URL.createObjectURL(data);
|
||||
a.href = url;
|
||||
a.download = xhr.getResponseHeader("Content-Disposition").match(/filename="([^"]*)"/)[1];
|
||||
document.body.append(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
// eslint-disable-next-line compat/compat
|
||||
window.URL.revokeObjectURL(url);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@ -83,63 +83,51 @@ function padNumber(num) {
|
||||
return ("00" + num).substr(-2, 2);
|
||||
}
|
||||
|
||||
var info = null;
|
||||
var showAlertBox = null;
|
||||
function showAlert(type, icon, title, message) {
|
||||
var opts = {};
|
||||
title = " <strong>" + title + "</strong><br>";
|
||||
const options = {
|
||||
title: " <strong>" + title + "</strong><br>",
|
||||
message: message,
|
||||
},
|
||||
settings = {
|
||||
type: type,
|
||||
delay: 5000, // default value
|
||||
mouse_over: "pause",
|
||||
};
|
||||
switch (type) {
|
||||
case "info":
|
||||
opts = {
|
||||
type: "info",
|
||||
icon: "far fa-clock",
|
||||
title: title,
|
||||
message: message,
|
||||
};
|
||||
info = $.notify(opts);
|
||||
break;
|
||||
case "success":
|
||||
opts = {
|
||||
type: "success",
|
||||
icon: icon,
|
||||
title: title,
|
||||
message: message,
|
||||
};
|
||||
if (info) {
|
||||
info.update(opts);
|
||||
} else {
|
||||
$.notify(opts);
|
||||
}
|
||||
options.icon = icon !== null && icon.len > 0 ? icon : "far fa-clock";
|
||||
|
||||
break;
|
||||
case "success":
|
||||
break;
|
||||
case "warning":
|
||||
opts = {
|
||||
type: "warning",
|
||||
icon: "fas fa-exclamation-triangle",
|
||||
title: title,
|
||||
message: message,
|
||||
};
|
||||
if (info) {
|
||||
info.update(opts);
|
||||
} else {
|
||||
$.notify(opts);
|
||||
}
|
||||
options.icon = "fas fa-exclamation-triangle";
|
||||
settings.delay *= 2;
|
||||
|
||||
break;
|
||||
case "error":
|
||||
opts = {
|
||||
type: "danger",
|
||||
icon: "fas fa-times",
|
||||
title: " <strong>Error, something went wrong!</strong><br>",
|
||||
message: message,
|
||||
};
|
||||
if (info) {
|
||||
info.update(opts);
|
||||
} else {
|
||||
$.notify(opts);
|
||||
}
|
||||
options.icon = "fas fa-times";
|
||||
options.title = " <strong>Error, something went wrong!</strong><br>";
|
||||
settings.delay *= 2;
|
||||
|
||||
break;
|
||||
default:
|
||||
// Case not handled, do nothing
|
||||
console.log("Unknown alert type: " + type); // eslint-disable-line no-console
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === "info") {
|
||||
// Create a new notification for info boxes
|
||||
showAlertBox = $.notify(options, settings);
|
||||
} else if (showAlertBox !== null) {
|
||||
// Update existing notification for other boxes (if available)
|
||||
showAlertBox.update(options);
|
||||
showAlertBox.update(settings);
|
||||
} else {
|
||||
// Create a new notification for other boxes if no previous info box exists
|
||||
$.notify(options, settings);
|
||||
}
|
||||
}
|
||||
|
||||
@ -283,11 +271,6 @@ function stateLoadCallback(itemName) {
|
||||
return data;
|
||||
}
|
||||
|
||||
function getGraphType() {
|
||||
// Only return line if `barchart_chkbox` is explicitly set to false. Else return bar
|
||||
return localStorage && localStorage.getItem("barchart_chkbox") === "false" ? "line" : "bar";
|
||||
}
|
||||
|
||||
function addFromQueryLog(domain, list) {
|
||||
var alertModal = $("#alertModal");
|
||||
var alProcessing = alertModal.find(".alProcessing");
|
||||
@ -537,6 +520,81 @@ function hexDecode(string) {
|
||||
return back;
|
||||
}
|
||||
|
||||
function listAlert(type, items, data) {
|
||||
// Show simple success message if there is no "processed" object in "data" or
|
||||
// if all items were processed successfully
|
||||
if (data.processed === undefined || data.processed.success.length === items.length) {
|
||||
showAlert(
|
||||
"success",
|
||||
"fas fa-plus",
|
||||
"Successfully added " + type + (items.length !== 1 ? "s" : ""),
|
||||
items.join(", ")
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Show a more detailed message if there is a "processed" object in "data" and
|
||||
// not all items were processed successfully
|
||||
let message = "";
|
||||
|
||||
// Show a list of successful items if there are any
|
||||
if (data.processed.success.length > 0) {
|
||||
message +=
|
||||
"<strong>Successfully added " +
|
||||
data.processed.success.length +
|
||||
" " +
|
||||
type +
|
||||
(data.processed.success.length !== 1 ? "s" : "") +
|
||||
":</strong>";
|
||||
|
||||
// Loop over data.processed.success and print "item"
|
||||
for (const item in data.processed.success) {
|
||||
if (Object.prototype.hasOwnProperty.call(data.processed.success, item)) {
|
||||
message += "<br>- <strong>" + data.processed.success[item].item + "</strong>";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add a line break if there are both successful and failed items
|
||||
if (data.processed.success.length > 0 && data.processed.errors.length > 0) {
|
||||
message += "<br><br>";
|
||||
}
|
||||
|
||||
// Show a list of failed items if there are any
|
||||
if (data.processed.errors.length > 0) {
|
||||
message +=
|
||||
"<strong>Failed to add " +
|
||||
data.processed.errors.length +
|
||||
" " +
|
||||
type +
|
||||
(data.processed.errors.length !== 1 ? "s" : "") +
|
||||
":</strong>\n";
|
||||
|
||||
// Loop over data.processed.errors and print "item: error"
|
||||
for (const item in data.processed.errors) {
|
||||
if (Object.prototype.hasOwnProperty.call(data.processed.errors, item)) {
|
||||
let error = data.processed.errors[item].error;
|
||||
// Replace some error messages with a more user-friendly text
|
||||
if (error.indexOf("UNIQUE constraint failed") > -1) {
|
||||
error = "Already present";
|
||||
}
|
||||
|
||||
message += "<br>- <strong>" + data.processed.errors[item].item + "</strong>: " + error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Show the warning message
|
||||
const total = data.processed.success.length + data.processed.errors.length;
|
||||
const processed = "(" + total + " " + type + (total !== 1 ? "s" : "") + " processed)";
|
||||
showAlert(
|
||||
"warning",
|
||||
"fas fa-exclamation-triangle",
|
||||
"Some " + type + (items.length !== 1 ? "s" : "") + " could not be added " + processed,
|
||||
message
|
||||
);
|
||||
}
|
||||
|
||||
window.utils = (function () {
|
||||
return {
|
||||
escapeHtml: escapeHtml,
|
||||
@ -553,7 +611,6 @@ window.utils = (function () {
|
||||
setBsSelectDefaults: setBsSelectDefaults,
|
||||
stateSaveCallback: stateSaveCallback,
|
||||
stateLoadCallback: stateLoadCallback,
|
||||
getGraphType: getGraphType,
|
||||
validateMAC: validateMAC,
|
||||
validateHostname: validateHostname,
|
||||
addFromQueryLog: addFromQueryLog,
|
||||
@ -570,5 +627,6 @@ window.utils = (function () {
|
||||
parseQueryString: parseQueryString,
|
||||
hexEncode: hexEncode,
|
||||
hexDecode: hexDecode,
|
||||
listsAlert: listAlert,
|
||||
};
|
||||
})();
|
||||
|
||||
@ -13,8 +13,8 @@ mg.include('header.lp','r')
|
||||
<script src="<?=pihole.fileversion('scripts/vendor/datatables.min.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('scripts/vendor/datatables.select.min.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('scripts/vendor/datatables.buttons.min.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('scripts/vendor/chart.umd.min.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('scripts/vendor/moment.min.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('scripts/vendor/chart.min.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('scripts/vendor/chartjs-adapter-moment.js')?>"></script>
|
||||
</head>
|
||||
<body class="<?=theme.name?> hold-transition sidebar-mini <? if pihole.boxedlayout() then ?>layout-boxed<? end ?> logged-in">
|
||||
@ -34,7 +34,7 @@ mg.include('header.lp','r')
|
||||
<div class="wrapper">
|
||||
<header class="main-header">
|
||||
<!-- Logo -->
|
||||
<a href="index.lp" class="logo">
|
||||
<a href="<?=pihole.webhome()?>" class="logo">
|
||||
<!-- mini logo for sidebar mini 50x50 pixels -->
|
||||
<span class="logo-mini">P<strong>h</strong></span>
|
||||
<!-- logo for regular state and mobile devices -->
|
||||
|
||||
13
scripts/vendor/chart.min.js
vendored
13
scripts/vendor/chart.min.js
vendored
File diff suppressed because one or more lines are too long
1
scripts/vendor/chart.umd.js.map
vendored
Normal file
1
scripts/vendor/chart.umd.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
7
scripts/vendor/chart.umd.min.js
vendored
Normal file
7
scripts/vendor/chart.umd.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
5
scripts/vendor/chartjs-adapter-moment.js
vendored
5
scripts/vendor/chartjs-adapter-moment.js
vendored
@ -1,8 +1,7 @@
|
||||
/*!
|
||||
* chartjs-adapter-moment v1.0.0
|
||||
* chartjs-adapter-moment v1.0.1
|
||||
* https://www.chartjs.org
|
||||
* (c) 2021 chartjs-adapter-moment Contributors
|
||||
* (c) 2022 chartjs-adapter-moment Contributors
|
||||
* Released under the MIT license
|
||||
*/
|
||||
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(require("moment"),require("chart.js")):"function"==typeof define&&define.amd?define(["moment","chart.js"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).moment,e.Chart)}(this,(function(e,t){"use strict";function n(e){return e&&"object"==typeof e&&"default"in e?e:{default:e}}var f=n(e);const a={datetime:"MMM D, YYYY, h:mm:ss a",millisecond:"h:mm:ss.SSS a",second:"h:mm:ss a",minute:"h:mm a",hour:"hA",day:"MMM D",week:"ll",month:"MMM YYYY",quarter:"[Q]Q - YYYY",year:"YYYY"};t._adapters._date.override("function"==typeof f.default?{_id:"moment",formats:function(){return a},parse:function(e,t){return"string"==typeof e&&"string"==typeof t?e=f.default(e,t):e instanceof f.default||(e=f.default(e)),e.isValid()?e.valueOf():null},format:function(e,t){return f.default(e).format(t)},add:function(e,t,n){return f.default(e).add(t,n).valueOf()},diff:function(e,t,n){return f.default(e).diff(f.default(t),n)},startOf:function(e,t,n){return e=f.default(e),"isoWeek"===t?(n=Math.trunc(Math.min(Math.max(0,n),6)),e.isoWeekday(n).startOf("day").valueOf()):e.startOf(t).valueOf()},endOf:function(e,t){return f.default(e).endOf(t).valueOf()}}:{})}));
|
||||
//# sourceMappingURL=chartjs-adapter-moment.min.js.map
|
||||
|
||||
@ -123,7 +123,7 @@ mg.include('scripts/pi-hole/lua/settings_header.lp','r')
|
||||
<label for="webserver.api.temp.limit"><strong>Temperature limit for "hot":</strong></label>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<input type="number" id="webserver.api.temp.limit" data-key="webserver.api.temp.limit">
|
||||
<input type="number" data-type="integer" id="webserver.api.temp.limit" data-key="webserver.api.temp.limit">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
|
||||
@ -23,7 +23,7 @@ mg.include('scripts/pi-hole/lua/settings_header.lp','r')
|
||||
<p>Warning: This archive contains sensitive information about your Pi-hole installation, e.g. the API token and the 2FA-TOTP secret (if enabled). Please be careful with this file and do not share it with anyone even if they claim to help you.</p>
|
||||
<? if not is_secure then ?><p class='text-danger'>Warning: You are currently not using an end-to-end encryption. This means that your API token and 2FA-TOTP secret will be transmitted in plain text. We recommend to use HTTPS when exporting your configuration.</p><? end ?>
|
||||
<div class="pull-right">
|
||||
<a class="btn btn-app btn-success" href="/api/teleporter" target="_blank">
|
||||
<a class="btn btn-app btn-success" id="GETTeleporter" target="_blank">
|
||||
<i class="fa fa-save"></i><br>Export
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@ -80,13 +80,13 @@ td.lookatme {
|
||||
/* Optimize Queries-Table for small screens */
|
||||
/* Time column */
|
||||
#all-queries td:nth-of-type(1),
|
||||
/* Status column */
|
||||
#all-queries td:nth-of-type(5) {
|
||||
/* Reply time column */
|
||||
#all-queries td:nth-of-type(6) {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Domain column */
|
||||
#all-queries td:nth-of-type(3) {
|
||||
#all-queries td:nth-of-type(4) {
|
||||
min-width: 200px;
|
||||
word-break: break-all;
|
||||
white-space: pre-wrap;
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
/* Code courtesy of https://blog.jim-nielsen.com/2019/conditional-syntax-highlighting-in-dark-mode-with-css-imports/ */
|
||||
|
||||
/* Assume light mode by default */
|
||||
@import "default-light.css" screen;
|
||||
/* Supersede dark mode when applicable */
|
||||
/* Import light mode if color-scheme is different than dark (even not set) */
|
||||
@import "default-light.css" screen and not (prefers-color-scheme: dark);
|
||||
/* Import dark mode when applicable */
|
||||
@import "default-dark.css" screen and (prefers-color-scheme: dark);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user