mirror of
https://github.com/pi-hole/web.git
synced 2024-12-06 19:36:21 +01:00
Merge branch 'development-v6' into new/error_pages
Signed-off-by: DL6ER <dl6er@dl6er.de>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
+2
-1
@@ -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;
|
||||
|
||||
@@ -501,8 +501,10 @@ function updateVersionInfo() {
|
||||
|
||||
versions.forEach(function (v) {
|
||||
if (v.local !== null) {
|
||||
// reset update status for each component
|
||||
var updateComponentAvailable = false;
|
||||
var localVersion = v.local;
|
||||
if (v.branch !== null && v.hash !== null)
|
||||
if (v.branch !== null && v.hash !== null) {
|
||||
if (v.branch === "master") {
|
||||
localVersion = v.local.split("-")[0];
|
||||
localVersion =
|
||||
@@ -515,32 +517,37 @@ function updateVersionInfo() {
|
||||
"</a>";
|
||||
if (versionCompare(v.local, v.remote) === -1) {
|
||||
// Update available
|
||||
updateAvailable = true;
|
||||
updateComponentAvailable = true;
|
||||
}
|
||||
} else {
|
||||
// non-master branch
|
||||
localVersion = "vDev (" + v.branch + ", " + v.hash + ")";
|
||||
if (v.hash !== v.hash_remote) {
|
||||
if (v.hash_remote && v.hash !== v.hash_remote) {
|
||||
// hash differ > Update available
|
||||
updateAvailable = true;
|
||||
updateComponentAvailable = true;
|
||||
// link to the commit history instead of release page
|
||||
v.url = v.url.replace("releases", "commits/" + v.branch);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (v.name === "Docker Tag" && versionCompare(v.local, v.remote) === -1) {
|
||||
updateAvailable = true;
|
||||
updateComponentAvailable = true;
|
||||
dockerUpdate = true;
|
||||
}
|
||||
|
||||
if (updateAvailable) {
|
||||
if (updateComponentAvailable) {
|
||||
$("#versions").append(
|
||||
"<li><strong>" +
|
||||
v.name +
|
||||
"</strong> " +
|
||||
localVersion +
|
||||
'· <a class="lookatme" lookatme-text="Update available!" href="' +
|
||||
' · <a class="lookatme" lookatme-text="Update available!" href="' +
|
||||
v.url +
|
||||
'" rel="noopener" target="_blank">Update available!</a></li>'
|
||||
);
|
||||
// if at least one component can be updated, display the update-hint footer
|
||||
updateAvailable = true;
|
||||
} else {
|
||||
$("#versions").append("<li><strong>" + v.name + "</strong> " + localVersion + "</li>");
|
||||
}
|
||||
@@ -548,11 +555,11 @@ function updateVersionInfo() {
|
||||
});
|
||||
|
||||
if (dockerUpdate)
|
||||
$("update-hint").html(
|
||||
$("#update-hint").html(
|
||||
'To install updates, <a href="https://github.com/pi-hole/docker-pi-hole#upgrading-persistence-and-customizations" rel="noopener" target="_blank">replace this old container with a fresh upgraded image</a>.'
|
||||
);
|
||||
else if (updateAvailable)
|
||||
$("update-hint").html(
|
||||
$("#update-hint").html(
|
||||
'To install updates, run <code><a href="https://docs.pi-hole.net/main/update/" rel="noopener" target="_blank">pihole -up</a></code>.'
|
||||
);
|
||||
|
||||
|
||||
@@ -38,7 +38,6 @@ function eventsource() {
|
||||
return reader.read().then(({ done, value }) => {
|
||||
// When no more data needs to be consumed, close the stream
|
||||
if (done) {
|
||||
ta.append("Done.");
|
||||
controller.close();
|
||||
alInfo.hide();
|
||||
$("#gravityBtn").prop("disabled", false);
|
||||
@@ -50,7 +49,7 @@ function eventsource() {
|
||||
var string = new TextDecoder().decode(value);
|
||||
parseLines(ta, string);
|
||||
|
||||
if (string.indexOf("Pi-hole blocking is") !== -1) {
|
||||
if (string.indexOf("Done.") !== -1) {
|
||||
alSuccess.show();
|
||||
}
|
||||
|
||||
|
||||
@@ -85,7 +85,7 @@ function format(data) {
|
||||
: "N/A") +
|
||||
(data.abp_entries !== null && parseInt(data.abp_entries, 10) > 0 && numbers === true
|
||||
? " (out of which " + parseInt(data.abp_entries, 10).toLocaleString() + " are in ABP-style)"
|
||||
: "N/A") +
|
||||
: "") +
|
||||
'</td></tr><tr class="dataTables-child"' +
|
||||
"><td>Number of non-domains on this list: </td>" +
|
||||
"<td>" +
|
||||
@@ -359,7 +359,7 @@ function initTable() {
|
||||
var ids = [];
|
||||
$("tr.selected").each(function () {
|
||||
// ... add the row identified by "data-id".
|
||||
ids.push($(this).attr("data-id"), 10);
|
||||
ids.push($(this).attr("data-id"));
|
||||
});
|
||||
// Delete all selected rows at once
|
||||
delItems(ids);
|
||||
@@ -499,13 +499,21 @@ function delItems(ids) {
|
||||
|
||||
function addAdlist(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 addAdlist(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();
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
+19
-11
@@ -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);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -48,16 +48,19 @@ function eventsource(partial) {
|
||||
(numDomains > 0 ? ":" : ".") +
|
||||
"<br><br>";
|
||||
for (const domain of res.domains) {
|
||||
const color = domain.type === "deny" ? "red" : "green";
|
||||
result +=
|
||||
" - <a href='groups-domains.lp?domainid=" +
|
||||
domain.id +
|
||||
"' target='_blank'><strong class='text-green'>" +
|
||||
"' target='_blank'><strong>" +
|
||||
utils.escapeHtml(domain.domain) +
|
||||
"</strong></a><br> " +
|
||||
"</strong></a><br> <strong class='text-" +
|
||||
color +
|
||||
"'>" +
|
||||
domain.kind +
|
||||
" " +
|
||||
domain.type +
|
||||
" domain<br> added: " +
|
||||
" domain</strong><br> added: " +
|
||||
utils.renderTimestamp(domain.date_added, "display") +
|
||||
"<br> last modified: " +
|
||||
utils.renderTimestamp(domain.date_modified, "display") +
|
||||
@@ -88,7 +91,7 @@ function eventsource(partial) {
|
||||
result +=
|
||||
"Found " +
|
||||
numAdlists +
|
||||
" adlist" +
|
||||
" list" +
|
||||
(numAdlists !== 1 ? "s" : "") +
|
||||
" <em>" +
|
||||
verb +
|
||||
@@ -99,12 +102,18 @@ function eventsource(partial) {
|
||||
"<br><br>";
|
||||
for (const address of Object.keys(grouped)) {
|
||||
const adlist = grouped[address][0];
|
||||
const color = adlist.type === "block" ? "red" : "green";
|
||||
result +=
|
||||
" - <a href='groups-adlists.lp?adlistid=" +
|
||||
adlist.id +
|
||||
"' target='_blank'>" +
|
||||
utils.escapeHtml(address) +
|
||||
"</a><br> added: " +
|
||||
"</a><br> <strong class='text-" +
|
||||
color +
|
||||
"'>" +
|
||||
adlist.type +
|
||||
" list</strong>" +
|
||||
"<br> added: " +
|
||||
utils.renderTimestamp(adlist.date_added, "display") +
|
||||
"<br> last modified: " +
|
||||
utils.renderTimestamp(adlist.date_modified, "display") +
|
||||
@@ -122,18 +131,50 @@ function eventsource(partial) {
|
||||
(adlist.comment !== null && adlist.comment.length > 0
|
||||
? '<br> comment: "' + utils.escapeHtml(adlist.comment) + '"'
|
||||
: "<br> no comment") +
|
||||
"<br>";
|
||||
"<br> matching entries:<br>";
|
||||
for (const adlists of grouped[address]) {
|
||||
result +=
|
||||
" - <strong class='text-green'>" +
|
||||
utils.escapeHtml(adlists.domain) +
|
||||
"</strong><br>";
|
||||
" - <strong class='text-blue'>" + utils.escapeHtml(adlists.domain) + "</strong><br>";
|
||||
}
|
||||
|
||||
result += "<br>";
|
||||
}
|
||||
|
||||
result += "Search took " + (1000 * data.took).toFixed(1) + " ms";
|
||||
result += "Number of results per type:<br>";
|
||||
result +=
|
||||
" - <strong class='text-blue'>" +
|
||||
data.search.results.domains.exact +
|
||||
"</strong> exact domain matches<br>";
|
||||
result +=
|
||||
" - <strong class='text-blue'>" +
|
||||
data.search.results.domains.regex +
|
||||
"</strong> regex domain matches<br>";
|
||||
result +=
|
||||
" - <strong class='text-blue'>" +
|
||||
data.search.results.gravity.allow +
|
||||
"</strong> allowlist (antigravity) matches<br>";
|
||||
result +=
|
||||
" - <strong class='text-blue'>" +
|
||||
data.search.results.gravity.block +
|
||||
"</strong> blocklist (gravity) matches<br>";
|
||||
|
||||
if (
|
||||
data.search.results.gravity.allow > data.search.parameters.N ||
|
||||
data.search.results.gravity.block > data.search.parameters.N ||
|
||||
data.search.results.domains.exact > data.search.parameters.N ||
|
||||
data.search.results.domains.regex > data.search.parameters.N
|
||||
) {
|
||||
result +=
|
||||
"<br><br><strong class='text-green'>Note:</strong> " +
|
||||
"The number of results to return per type is limited to " +
|
||||
data.search.parameters.N +
|
||||
" entries.<br> There are " +
|
||||
data.search.results.total +
|
||||
" matching entries in total.<br> Consider " +
|
||||
"using a more specific search term or increase N.<br>";
|
||||
}
|
||||
|
||||
result += "<br>Search took " + (1000 * data.took).toFixed(1) + " ms";
|
||||
|
||||
ta.append(result);
|
||||
})
|
||||
|
||||
@@ -41,11 +41,17 @@ function generateRow(topic, key, value) {
|
||||
'<h3 class="box-title">' +
|
||||
key +
|
||||
(value.modified
|
||||
? ' <i class="far fa-edit text-light-blue" title="Modified"></i>'
|
||||
? ' <i class="far fa-edit text-light-blue" title="Modified from default"></i>'
|
||||
: "") +
|
||||
(value.flags.advanced
|
||||
? ' <i class="fas fa-cogs text-yellow" title="Expert-level setting"></i>'
|
||||
: "") +
|
||||
(value.flags.restart_dnsmasq
|
||||
? ' <i class="fas fa-redo text-orange" title="Setting requires FTL restart on change"></i>'
|
||||
: "") +
|
||||
(value.flags.env_var
|
||||
? ' <i class="fas fa-lock text-orange" title="Settings overwritten by an environmental variable are read-only"></i>'
|
||||
: "") +
|
||||
"</h3>" +
|
||||
"<p>" +
|
||||
utils.escapeHtml(value.description).replace("\n", "<br>") +
|
||||
@@ -84,6 +90,11 @@ function generateRow(topic, key, value) {
|
||||
}
|
||||
}
|
||||
|
||||
let extraAttributes = "";
|
||||
if (value.flags.env_var) {
|
||||
extraAttributes = " disabled";
|
||||
}
|
||||
|
||||
switch (value.type) {
|
||||
case "IPv4 address":
|
||||
case "IPv6 address":
|
||||
@@ -95,7 +106,9 @@ function generateRow(topic, key, value) {
|
||||
value.value +
|
||||
'" data-key="' +
|
||||
key +
|
||||
'"> ' +
|
||||
'"' +
|
||||
extraAttributes +
|
||||
"> " +
|
||||
defaultValueHint +
|
||||
addAllowedValues(value.allowed) +
|
||||
"</div>";
|
||||
@@ -111,7 +124,9 @@ function generateRow(topic, key, value) {
|
||||
key +
|
||||
'-checkbox" data-key="' +
|
||||
key +
|
||||
'"><label for="' +
|
||||
'"' +
|
||||
extraAttributes +
|
||||
'><label for="' +
|
||||
key +
|
||||
'-checkbox">Enabled ' +
|
||||
defaultValueHint +
|
||||
@@ -129,7 +144,9 @@ function generateRow(topic, key, value) {
|
||||
value.value +
|
||||
'" data-key="' +
|
||||
key +
|
||||
'" data-type="float"> ' +
|
||||
'" data-type="float"' +
|
||||
extraAttributes +
|
||||
"> " +
|
||||
defaultValueHint +
|
||||
"</div>";
|
||||
|
||||
@@ -144,7 +161,9 @@ function generateRow(topic, key, value) {
|
||||
value.value +
|
||||
'" data-key="' +
|
||||
key +
|
||||
'" data-type="integer"> ' +
|
||||
'" data-type="integer"' +
|
||||
extraAttributes +
|
||||
"> " +
|
||||
defaultValueHint +
|
||||
"</div>";
|
||||
|
||||
@@ -159,7 +178,9 @@ function generateRow(topic, key, value) {
|
||||
value.value +
|
||||
'" data-key="' +
|
||||
key +
|
||||
'" data-type="integer"> ' +
|
||||
'" data-type="integer"' +
|
||||
extraAttributes +
|
||||
"> " +
|
||||
defaultValueHint +
|
||||
"</div>";
|
||||
|
||||
@@ -174,7 +195,9 @@ function generateRow(topic, key, value) {
|
||||
value.value +
|
||||
'" data-key="' +
|
||||
key +
|
||||
'" data-type="integer"> ' +
|
||||
'" data-type="integer"' +
|
||||
extraAttributes +
|
||||
"> " +
|
||||
defaultValueHint +
|
||||
"</div>";
|
||||
|
||||
@@ -187,7 +210,9 @@ function generateRow(topic, key, value) {
|
||||
'<div class="col-sm-7">' +
|
||||
'<textarea class="form-control" data-key="' +
|
||||
key +
|
||||
'">' +
|
||||
'"' +
|
||||
extraAttributes +
|
||||
">" +
|
||||
value.value.join("\n") +
|
||||
"</textarea> " +
|
||||
defaultValueHint +
|
||||
@@ -204,7 +229,9 @@ function generateRow(topic, key, value) {
|
||||
'<div class="col-sm-8">' +
|
||||
'<select class="form-control" data-key="' +
|
||||
key +
|
||||
'">';
|
||||
'"' +
|
||||
extraAttributes +
|
||||
">";
|
||||
value.allowed.forEach(function (option) {
|
||||
box +=
|
||||
'<option value="' +
|
||||
@@ -234,7 +261,9 @@ function generateRow(topic, key, value) {
|
||||
value.value +
|
||||
'" data-key="' +
|
||||
key +
|
||||
'"> ' +
|
||||
'"' +
|
||||
extraAttributes +
|
||||
"> " +
|
||||
defaultValueHint +
|
||||
addAllowedValues(value.allowed) +
|
||||
"</div>";
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
var apiSessionsTable = null;
|
||||
var ownSessionID = null;
|
||||
var deleted = 0;
|
||||
var TOTPdata = null;
|
||||
|
||||
function renderBool(data, type) {
|
||||
@@ -39,6 +40,7 @@ $(function () {
|
||||
{ data: "id" },
|
||||
{ data: "valid", render: renderBool },
|
||||
{ data: null },
|
||||
{ data: "app", render: renderBool },
|
||||
{ data: "login_at", render: utils.renderTimestamp },
|
||||
{ data: "valid_until", render: utils.renderTimestamp },
|
||||
{ data: "remote_addr", type: "ip-address" },
|
||||
@@ -60,7 +62,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 +72,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 +
|
||||
@@ -83,10 +85,10 @@ $(function () {
|
||||
'">' +
|
||||
'<span class="far fa-trash-alt"></span>' +
|
||||
"</button>";
|
||||
$("td:eq(8)", row).html(button);
|
||||
$("td:eq(9)", row).html(button);
|
||||
if (data.current_session) {
|
||||
ownSessionID = data.id;
|
||||
$("td:eq(6)", row).html(
|
||||
$("td:eq(7)", row).html(
|
||||
'<strong title="This is the session you are currently using for the web interface">' +
|
||||
data.remote_addr +
|
||||
"</strong>"
|
||||
@@ -148,7 +150,7 @@ $(function () {
|
||||
ids.push(parseInt($(this).attr("data-id"), 10));
|
||||
});
|
||||
// Delete all selected rows at once
|
||||
delSession(ids);
|
||||
deleteMultipleSessions(ids);
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -186,13 +188,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 +213,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 +227,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();
|
||||
@@ -297,6 +312,69 @@ $("#modal-totp").on("shown.bs.modal", function () {
|
||||
});
|
||||
});
|
||||
|
||||
var apppwhash = null;
|
||||
$("#modal-apppw").on("shown.bs.modal", function () {
|
||||
$.ajax({
|
||||
url: "/api/auth/app",
|
||||
})
|
||||
.done(function (data) {
|
||||
apppwhash = data.app.hash;
|
||||
$("#password_code").text(data.app.password);
|
||||
$("#password_display").removeClass("hidden");
|
||||
$("#password-spinner").hide();
|
||||
})
|
||||
.fail(function (data) {
|
||||
apiFailure(data);
|
||||
});
|
||||
});
|
||||
|
||||
$("#apppw_submit").on("click", function () {
|
||||
// Enable app password
|
||||
setAppPassword();
|
||||
});
|
||||
|
||||
$("#apppw_clear").on("click", function () {
|
||||
// Disable app password
|
||||
apppwhash = "";
|
||||
setAppPassword();
|
||||
});
|
||||
|
||||
function setAppPassword() {
|
||||
$.ajax({
|
||||
url: "/api/config",
|
||||
type: "PATCH",
|
||||
dataType: "json",
|
||||
processData: false,
|
||||
data: JSON.stringify({ config: { webserver: { api: { app_pwhash: apppwhash } } } }),
|
||||
contentType: "application/json",
|
||||
})
|
||||
.done(function () {
|
||||
$("#modal-apppw").modal("hide");
|
||||
const verb = apppwhash.length > 0 ? "enabled" : "disabled";
|
||||
const verb2 = apppwhash.length > 0 ? "will" : "may";
|
||||
alert(
|
||||
"App password has been " +
|
||||
verb +
|
||||
", you " +
|
||||
verb2 +
|
||||
" need to re-login to continue using the web interface."
|
||||
);
|
||||
location.reload();
|
||||
})
|
||||
.fail(function (data) {
|
||||
apiFailure(data);
|
||||
});
|
||||
}
|
||||
|
||||
// Remove class "password_background" from the password input field with ID
|
||||
// password_code when the user hovers over it or if it is focused
|
||||
$("#password_code").on("mouseover focus", function () {
|
||||
$(this).removeClass("password_background");
|
||||
});
|
||||
$("#password_code").on("mouseout blur", function () {
|
||||
$(this).addClass("password_background");
|
||||
});
|
||||
|
||||
// Trigger keyup event when pasting into the TOTP code input field
|
||||
$("#totp_code").on("paste", function (e) {
|
||||
$(e.target).keyup();
|
||||
|
||||
@@ -7,161 +7,141 @@
|
||||
|
||||
/* global utils: false, apiFailure:false */
|
||||
|
||||
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 populateDataTable(endpoint) {
|
||||
var columns = "";
|
||||
if (endpoint === "hosts") {
|
||||
columns = [
|
||||
{ data: null, render: hostsDomain },
|
||||
{ data: null, type: "ip-address", render: hostsIP },
|
||||
{ data: null, width: "22px", orderable: false },
|
||||
];
|
||||
} else {
|
||||
columns = [
|
||||
{ data: null, render: CNAMEdomain },
|
||||
{ data: null, render: CNAMEtarget },
|
||||
{ data: null, render: CNAMEttl },
|
||||
{ data: null, width: "22px", orderable: false },
|
||||
];
|
||||
}
|
||||
|
||||
var setByEnv = false;
|
||||
$.ajax({
|
||||
url: `/api/config/dns/${endpoint}?detailed=true`,
|
||||
}).done(function (data) {
|
||||
setByEnv = data.config.dns[endpoint].flags.env_var;
|
||||
|
||||
if (setByEnv) {
|
||||
$(`#title-${endpoint}`).append(
|
||||
`<span class="env-warning"> <i class="fas fa-lock text-orange env-warning" title="Settings overwritten by an environmental variable are read-only"></i></span>`
|
||||
);
|
||||
$(`.${endpoint}`).prop("disabled", true);
|
||||
}
|
||||
});
|
||||
|
||||
$(`#${endpoint}-Table`).DataTable({
|
||||
ajax: {
|
||||
url: `/api/config/dns/${endpoint}`,
|
||||
type: "GET",
|
||||
dataSrc: `config.dns.${endpoint}`,
|
||||
},
|
||||
columns: columns,
|
||||
columnDefs: [
|
||||
{
|
||||
targets: "_all",
|
||||
render: $.fn.dataTable.render.text(),
|
||||
},
|
||||
],
|
||||
drawCallback: function () {
|
||||
$(`button[id^="delete${endpoint}"]`).on("click", deleteRecord);
|
||||
|
||||
// Remove visible dropdown to prevent orphaning
|
||||
$("body > .bootstrap-select.dropdown").remove();
|
||||
},
|
||||
rowCallback: function (row, data) {
|
||||
$(row).attr("data-id", data);
|
||||
var button = `<button type="button"
|
||||
class="btn btn-danger btn-xs"
|
||||
id="delete${endpoint}${utils.hexEncode(data)}"
|
||||
data-tag="${data}"
|
||||
data-type="${endpoint}"
|
||||
${setByEnv ? "disabled" : ""}>
|
||||
<span class="far fa-trash-alt"></span>
|
||||
</button>`;
|
||||
$(`td:eq(${endpoint === "hosts" ? 2 : 3})`, row).html(button);
|
||||
},
|
||||
dom:
|
||||
"<'row'<'col-sm-6'l><'col-sm-6'f>>" +
|
||||
"<'row'<'col-sm-12'<'table-responsive'tr>>>" +
|
||||
"<'row'<'col-sm-12'i>>",
|
||||
lengthMenu: [
|
||||
[10, 25, 50, 100, -1],
|
||||
[10, 25, 50, 100, "All"],
|
||||
],
|
||||
language: {
|
||||
emptyTable: function () {
|
||||
return endpoint === "hosts"
|
||||
? "No local DNS records defined."
|
||||
: "No local CNAME records defined.";
|
||||
},
|
||||
},
|
||||
stateSave: true,
|
||||
stateDuration: 0,
|
||||
processing: true,
|
||||
stateSaveCallback: function (settings, data) {
|
||||
utils.stateSaveCallback(`${endpoint}-records-table`, data);
|
||||
},
|
||||
stateLoadCallback: function () {
|
||||
var data = utils.stateLoadCallback(`${endpoint}-records-table`);
|
||||
// Return if not available
|
||||
if (data === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Apply loaded state to table
|
||||
return data;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
$(function () {
|
||||
dnsRecordsTable = $("#dnsRecordsTable").DataTable({
|
||||
ajax: {
|
||||
url: "/api/config/dns/hosts",
|
||||
type: "GET",
|
||||
dataSrc: "config.dns.hosts",
|
||||
},
|
||||
order: [[0, "asc"]],
|
||||
columns: [
|
||||
{ data: null },
|
||||
{ data: null, type: "ip-address" },
|
||||
{ data: null, width: "22px", orderable: false },
|
||||
],
|
||||
columnDefs: [
|
||||
{
|
||||
targets: "_all",
|
||||
render: $.fn.dataTable.render.text(),
|
||||
},
|
||||
],
|
||||
drawCallback: function () {
|
||||
$('button[id^="deleteRecord"]').on("click", deleteRecord);
|
||||
|
||||
// Remove visible dropdown to prevent orphaning
|
||||
$("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' +
|
||||
utils.hexEncode(data) +
|
||||
'" data-tag="' +
|
||||
data +
|
||||
'" data-type="hosts">' +
|
||||
'<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>>" +
|
||||
"<'row'<'col-sm-12'<'table-responsive'tr>>>" +
|
||||
"<'row'<'col-sm-12'i>>",
|
||||
lengthMenu: [
|
||||
[10, 25, 50, 100, -1],
|
||||
[10, 25, 50, 100, "All"],
|
||||
],
|
||||
language: {
|
||||
emptyTable: "No local DNS records defined.",
|
||||
},
|
||||
stateSave: true,
|
||||
stateDuration: 0,
|
||||
processing: true,
|
||||
stateSaveCallback: function (settings, data) {
|
||||
utils.stateSaveCallback("dns-records-table", data);
|
||||
},
|
||||
stateLoadCallback: function () {
|
||||
var data = utils.stateLoadCallback("dns-records-table");
|
||||
// Return if not available
|
||||
if (data === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Apply loaded state to table
|
||||
return data;
|
||||
},
|
||||
});
|
||||
|
||||
dnsRecordsTable = $("#customCNAMETable").DataTable({
|
||||
ajax: {
|
||||
url: "/api/config/dns/cnameRecords",
|
||||
type: "GET",
|
||||
dataSrc: "config.dns.cnameRecords",
|
||||
},
|
||||
order: [[0, "asc"]],
|
||||
columns: [
|
||||
{ data: null },
|
||||
{ data: null },
|
||||
{ data: null },
|
||||
{ data: null, width: "22px", orderable: false },
|
||||
],
|
||||
columnDefs: [
|
||||
{
|
||||
targets: "_all",
|
||||
render: $.fn.dataTable.render.text(),
|
||||
},
|
||||
],
|
||||
drawCallback: function () {
|
||||
$('button[id^="deleteCNAME"]').on("click", deleteRecord);
|
||||
|
||||
// Remove visible dropdown to prevent orphaning
|
||||
$("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' +
|
||||
utils.hexEncode(data) +
|
||||
'" data-tag="' +
|
||||
data +
|
||||
'" data-type="cname">' +
|
||||
'<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>>" +
|
||||
"<'row'<'col-sm-12'<'table-responsive'tr>>>" +
|
||||
"<'row'<'col-sm-12'i>>",
|
||||
lengthMenu: [
|
||||
[10, 25, 50, 100, -1],
|
||||
[10, 25, 50, 100, "All"],
|
||||
],
|
||||
language: {
|
||||
emptyTable: "No local CNAME records defined.",
|
||||
},
|
||||
stateSave: true,
|
||||
stateDuration: 0,
|
||||
processing: true,
|
||||
stateSaveCallback: function (settings, data) {
|
||||
utils.stateSaveCallback("dns-cname-records-table", data);
|
||||
},
|
||||
stateLoadCallback: function () {
|
||||
var data = utils.stateLoadCallback("dns-cname-records-table");
|
||||
// Return if not available
|
||||
if (data === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Apply loaded state to table
|
||||
return data;
|
||||
},
|
||||
});
|
||||
populateDataTable("hosts");
|
||||
populateDataTable("cnameRecords");
|
||||
});
|
||||
|
||||
function deleteRecord() {
|
||||
// Get the tags
|
||||
var tags = [$(this).attr("data-tag")];
|
||||
var types = [$(this).attr("data-type")];
|
||||
|
||||
// Check input validity
|
||||
if (!Array.isArray(tags)) return;
|
||||
|
||||
@@ -186,7 +166,7 @@ function delHosts(elem) {
|
||||
.done(function () {
|
||||
utils.enableAll();
|
||||
utils.showAlert("success", "far fa-trash-alt", "Successfully deleted DNS record", "");
|
||||
dnsRecordsTable.ajax.reload(null, false);
|
||||
$("#hosts-Table").DataTable().ajax.reload(null, false);
|
||||
})
|
||||
.fail(function (data, exception) {
|
||||
utils.enableAll();
|
||||
@@ -213,7 +193,7 @@ function delCNAME(elem) {
|
||||
.done(function () {
|
||||
utils.enableAll();
|
||||
utils.showAlert("success", "far fa-trash-alt", "Successfully deleted local CNAME record", "");
|
||||
customCNAMETable.ajax.reload(null, false);
|
||||
$("#cnameRecords-Table").DataTable().ajax.reload(null, false);
|
||||
})
|
||||
.fail(function (data, exception) {
|
||||
utils.enableAll();
|
||||
@@ -230,8 +210,10 @@ 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",
|
||||
@@ -239,7 +221,7 @@ $(document).ready(function () {
|
||||
.done(function () {
|
||||
utils.enableAll();
|
||||
utils.showAlert("success", "far fa-plus", "Successfully added DNS record", "");
|
||||
dnsRecordsTable.ajax.reload(null, false);
|
||||
$("#hosts-Table").DataTable().ajax.reload(null, false);
|
||||
})
|
||||
.fail(function (data, exception) {
|
||||
utils.enableAll();
|
||||
@@ -250,10 +232,12 @@ $(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",
|
||||
@@ -261,7 +245,7 @@ $(document).ready(function () {
|
||||
.done(function () {
|
||||
utils.enableAll();
|
||||
utils.showAlert("success", "far fa-plus", "Successfully added CNAME record", "");
|
||||
dnsRecordsTable.ajax.reload(null, false);
|
||||
$("#cnameRecords-Table").DataTable().ajax.reload(null, false);
|
||||
})
|
||||
.fail(function (data, exception) {
|
||||
utils.enableAll();
|
||||
|
||||
@@ -18,6 +18,12 @@ function removeFromArray(arr, what) {
|
||||
}
|
||||
|
||||
function fillDNSupstreams(value, servers) {
|
||||
var disabledStr = "";
|
||||
if (value.flags.env_var === true) {
|
||||
$("#DNSupstreamsTextfield").prop("disabled", true);
|
||||
disabledStr = 'disabled="Disabled"';
|
||||
}
|
||||
|
||||
var i = 0;
|
||||
var customServers = value.value.length;
|
||||
servers.forEach(element => {
|
||||
@@ -30,21 +36,21 @@ function fillDNSupstreams(value, servers) {
|
||||
// Loop over available addresses (up to 2)
|
||||
for (let index = 0; index < 2; index++) {
|
||||
if (address.length > index) {
|
||||
row +=
|
||||
'<td title="' +
|
||||
address[index] +
|
||||
'"><div><input type="checkbox" id="DNSupstreams-' +
|
||||
i +
|
||||
'"';
|
||||
var checkedStr = "";
|
||||
if (
|
||||
value.value.includes(address[index]) ||
|
||||
value.value.includes(address[index] + "#53")
|
||||
) {
|
||||
row += " checked";
|
||||
checkedStr = "checked";
|
||||
customServers--;
|
||||
}
|
||||
|
||||
row += '><label for="DNSupstreams-' + i++ + '"></label></div></td>';
|
||||
row += `<td title="${address[index]}">
|
||||
<div>
|
||||
<input type="checkbox" id="DNSupstreams-${i}" ${disabledStr} ${checkedStr}>
|
||||
<label for="DNSupstreams-${i++}"></label>
|
||||
</div>
|
||||
</td>`;
|
||||
} else {
|
||||
row += "<td></td>";
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -37,6 +37,31 @@ function setConfigValues(topic, key, value) {
|
||||
|
||||
// else: we have a setting we can set
|
||||
var escapedKey = key.replaceAll(".", "\\.");
|
||||
var envTitle = $(`.${escapedKey}`);
|
||||
|
||||
if (value.flags.advanced && envTitle.find(".advanced-warning").length === 0) {
|
||||
envTitle.append(
|
||||
`<span class="advanced-warning"> <i class="fas fa-wrench" title="This is an advanced level setting"></i></span>`
|
||||
);
|
||||
}
|
||||
|
||||
if (value.flags.restart_dnsmasq && envTitle.find(".restart-warning").length === 0) {
|
||||
envTitle.append(
|
||||
`<span class="restart-warning"> <i class="fas fa-redo text-orange" title="Setting requires FTL restart on change"></i></span>`
|
||||
);
|
||||
}
|
||||
|
||||
if (value.flags.env_var) {
|
||||
// If this setting has been set by environment variable, display a padlock in the section title
|
||||
if (envTitle.find(".env-warning").length === 0) {
|
||||
envTitle.append(
|
||||
`<span class="env-warning"> <i class="fas fa-lock text-orange env-warning" title="Settings overwritten by an environmental variable are read-only"></i></span>`
|
||||
);
|
||||
}
|
||||
|
||||
$(`#${escapedKey}`).prop("disabled", "disabled");
|
||||
}
|
||||
|
||||
switch (value.type) {
|
||||
case "enum (unsigned integer)": // fallthrough
|
||||
case "enum (string)": {
|
||||
@@ -44,6 +69,7 @@ function setConfigValues(topic, key, value) {
|
||||
$("#" + escapedKey + " option").remove();
|
||||
// Add allowed select items (if available)
|
||||
value.allowed.forEach(function (allowedValue) {
|
||||
$("#" + escapedKey + "-" + allowedValue.item).prop("disabled", value.flags.env_var);
|
||||
var newopt = $("<option></option>")
|
||||
.attr("value", allowedValue.item)
|
||||
.text(allowedValue.description);
|
||||
@@ -70,7 +96,6 @@ function setConfigValues(topic, key, value) {
|
||||
case "string array": {
|
||||
// Set input field values from array (if available)
|
||||
$("#" + escapedKey).val(value.value.join("\n"));
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
+122
-50
@@ -83,63 +83,65 @@ 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;
|
||||
|
||||
// If the message is an API object, nicely format the error message
|
||||
// Try to parse message as JSON
|
||||
try {
|
||||
var data = JSON.parse(message);
|
||||
console.log(data); // eslint-disable-line no-console
|
||||
if (data.error !== undefined) {
|
||||
options.title = " <strong>" + data.error.message + "</strong><br>";
|
||||
|
||||
if (data.error.hint !== null) options.message = data.error.hint;
|
||||
}
|
||||
} catch {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
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 +285,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 +534,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 +625,6 @@ window.utils = (function () {
|
||||
setBsSelectDefaults: setBsSelectDefaults,
|
||||
stateSaveCallback: stateSaveCallback,
|
||||
stateLoadCallback: stateLoadCallback,
|
||||
getGraphType: getGraphType,
|
||||
validateMAC: validateMAC,
|
||||
validateHostname: validateHostname,
|
||||
addFromQueryLog: addFromQueryLog,
|
||||
@@ -570,5 +641,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 -->
|
||||
|
||||
@@ -230,7 +230,7 @@
|
||||
<!-- Query Lists -->
|
||||
<li class="<? if scriptname == 'search' then ?> active<? end ?>">
|
||||
<a href="<?=webhome?>search">
|
||||
<i class="fa fa-fw menu-icon fa-search"></i> <span>Search Adlists</span>
|
||||
<i class="fa fa-fw menu-icon fa-search"></i> <span>Search Lists</span>
|
||||
</a>
|
||||
</li>
|
||||
<!-- Network -->
|
||||
|
||||
Vendored
-13
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+7
File diff suppressed because one or more lines are too long
+2
-3
@@ -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
|
||||
|
||||
@@ -11,7 +11,7 @@ mg.include('scripts/pi-hole/lua/header_authenticated.lp','r')
|
||||
?>
|
||||
<!-- Title -->
|
||||
<div class="page-header">
|
||||
<h1>Find Blocked Domain In Lists</h1>
|
||||
<h1>Find Domains In Lists</h1>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
|
||||
+42
-5
@@ -19,7 +19,7 @@ mg.include('scripts/pi-hole/lua/settings_header.lp','r')
|
||||
<div class="col-md-12 settings-level-1">
|
||||
<div class="box box-warning">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">Exclusions</h3>
|
||||
<h3 class="box-title webserver.api.excludeDomains webserver.api.excludeClients">Exclusions</h3>
|
||||
</div>
|
||||
<div class="box-body">
|
||||
<div class="row">
|
||||
@@ -40,7 +40,7 @@ mg.include('scripts/pi-hole/lua/settings_header.lp','r')
|
||||
<div class="col-md-12 settings-level-0">
|
||||
<div class="box box-warning">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">Theme settings</h3>
|
||||
<h3 class="box-title webserver.interface.theme webserver.interface.boxed">Theme settings</h3>
|
||||
</div>
|
||||
<div class="box-body">
|
||||
<div class="row">
|
||||
@@ -65,7 +65,7 @@ mg.include('scripts/pi-hole/lua/settings_header.lp','r')
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div><!--
|
||||
</div><!-- TODO: Why is this commented out?
|
||||
<div class="col-md-6">
|
||||
<div class="box box-warning">
|
||||
<div class="box-header with-border">
|
||||
@@ -92,7 +92,7 @@ mg.include('scripts/pi-hole/lua/settings_header.lp','r')
|
||||
<div class="col-md-6 settings-level-1">
|
||||
<div class="box box-warning">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">Advanced Settings</h3>
|
||||
<h3 class="box-title webserver.api.localAPIauth webserver.api.prettyJSON webserver.api.allow_destructive webserver.api.temp.limit webserver.api.temp.unit">Advanced Settings</h3>
|
||||
</div>
|
||||
<div class="box-body">
|
||||
<div class="row">
|
||||
@@ -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">
|
||||
@@ -136,8 +136,11 @@ mg.include('scripts/pi-hole/lua/settings_header.lp','r')
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-12 pt-3">
|
||||
<button type="button" id="button-enable-totp" class="btn btn-success hidden" data-toggle="modal" data-target="#modal-totp">Enable 2FA</button>
|
||||
<button type="button" id="button-disable-totp" class="btn btn-danger hidden" data-toggle="modal" data-target="#modal-totp-disable">Disable 2FA</button>
|
||||
<button type="button" id="button-apppw" class="btn btn-default pull-right" data-toggle="modal" data-target="#modal-apppw">Configure app password</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -158,6 +161,7 @@ mg.include('scripts/pi-hole/lua/settings_header.lp','r')
|
||||
<th title="Session ID used internally by our Pi-hole">ID</th>
|
||||
<th title="Session is still valid (neither expired nor closed)">Valid</th>
|
||||
<th title="Connection between client and Pi-hole is end-to-end encrypted">TLS</th>
|
||||
<th title="Session used application password during authentication"><i class="fas fa-robot"></i></th>
|
||||
<th title="Time at which the client created the session">Login at</th>
|
||||
<th title="Time at which the session expires (if not prolonged)">Valid until</th>
|
||||
<th title="The client that created this session (the current connection is highlighted in bold-face)">Client IP</th>
|
||||
@@ -217,6 +221,39 @@ mg.include('scripts/pi-hole/lua/settings_header.lp','r')
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="modal-apppw" style="display: none;">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span></button>
|
||||
<h4 class="modal-title">Configure application password</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>After you turn on two-factor (2FA) verification and set up an
|
||||
Authenticator app, you may run into issues if you use apps or
|
||||
other services that don't support two-step verification. In this
|
||||
case, you can create and use an app password to sign in.</p>
|
||||
<p>An app password is a long, randomly generated password that
|
||||
can be used instead of your regular password + 2FA token when
|
||||
signing in to the API. You can revoke the app password at any
|
||||
time. The app password can also be used when 2FA is not enabled.</p>
|
||||
<div class="text-center">
|
||||
<i id="password-spinner" class="fas fa-spinner fa-pulse fa-5x"></i>
|
||||
</div>
|
||||
<p id="password_display" class="text-center hidden">🔐<br><strong>Your new app password is:</strong><br><code class="password_background m-5" id="password_code"></code></p>
|
||||
<p>IMPORTANT: The app password generated here will only be shown
|
||||
once and cannot be recovered. Make sure to store it in a safe
|
||||
place!</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" id="apppw_clear" class="btn btn-default btn-danger pull-left">Remove currently set app password</button>
|
||||
<button type="button" id="apppw_submit" class="btn btn-default btn-success">Enable new app password</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="<?=pihole.fileversion('scripts/vendor/jquery.confirm.min.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('scripts/vendor/qrious.min.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('scripts/pi-hole/js/settings-api.js')?>"></script>
|
||||
|
||||
+3
-3
@@ -18,7 +18,7 @@ mg.include('scripts/pi-hole/lua/settings_header.lp','r')
|
||||
<div class="col-md-6 settings-level-0">
|
||||
<div class="box box-warning">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">DHCP Settings</h3>
|
||||
<h3 class="box-title dhcp.active dhcp.start dhcp.end dhcp.router dhcp.ipv6">DHCP Settings</h3>
|
||||
</div>
|
||||
<div class="box-body">
|
||||
<div class="row">
|
||||
@@ -71,7 +71,7 @@ mg.include('scripts/pi-hole/lua/settings_header.lp','r')
|
||||
<div class="col-md-6 settings-level-1">
|
||||
<div class="box box-warning">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">Advanced DHCP Settings</h3>
|
||||
<h3 class="box-title dhcp.domain dhcp.leaseTime dhcp.rapidCommit chdp.multiDNS">Advanced DHCP Settings</h3>
|
||||
</div>
|
||||
<div class="box-body">
|
||||
<div class="row">
|
||||
@@ -146,7 +146,7 @@ mg.include('scripts/pi-hole/lua/settings_header.lp','r')
|
||||
<div class="col-md-12 settings-level-2">
|
||||
<div class="box box-warning">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">Static DHCP configuration <i class="fas fa-wrench" title="This is an advanced-level setting"></i></h3>
|
||||
<h3 class="box-title">Static DHCP configuration</h3>
|
||||
</div>
|
||||
<div class="box-body">
|
||||
<div class="row">
|
||||
|
||||
+5
-5
@@ -17,7 +17,7 @@ mg.include('scripts/pi-hole/lua/settings_header.lp','r')
|
||||
<div class="col-lg-6">
|
||||
<div class="box box-warning">
|
||||
<div class="box-header with-border">
|
||||
<h1 class="box-title">Upstream DNS Servers</h1>
|
||||
<h1 class="box-title dns.upstreams" id="lblDNSupstreams">Upstream DNS Servers</h1>
|
||||
</div>
|
||||
<div class="box-body">
|
||||
<div class="row">
|
||||
@@ -59,7 +59,7 @@ mg.include('scripts/pi-hole/lua/settings_header.lp','r')
|
||||
</div>
|
||||
<div class="box box-warning settings-level-1">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">Conditional forwarding <i class="fas fa-wrench" title="This is an advanced-level setting"></i></h3>
|
||||
<h3 class="box-title dns.revServer.active dns.revServer.cidr dns.revServer.target dns.revServer.domain">Conditional forwarding</h3>
|
||||
</div>
|
||||
<div class="box-body">
|
||||
<div class="row">
|
||||
@@ -125,7 +125,7 @@ mg.include('scripts/pi-hole/lua/settings_header.lp','r')
|
||||
<div class="col-lg-6 settings-level-1">
|
||||
<div class="box box-warning">
|
||||
<div class="box-header with-border">
|
||||
<h1 class="box-title">Interface settings <i class="fas fa-wrench" title="This is an advanced-level setting"></i></h1>
|
||||
<h1 class="box-title dns.listeningMode">Interface settings</h1>
|
||||
</div>
|
||||
<div class="box-body">
|
||||
<div class="row">
|
||||
@@ -166,7 +166,7 @@ mg.include('scripts/pi-hole/lua/settings_header.lp','r')
|
||||
</div>
|
||||
<div class="box box-warning">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">Advanced DNS settings</h3>
|
||||
<h3 class="box-title dns.domainNeeded dns.bogusPriv dns.dnssec">Advanced DNS settings</h3>
|
||||
</div>
|
||||
<div class="box-body">
|
||||
<div class="row">
|
||||
@@ -212,7 +212,7 @@ mg.include('scripts/pi-hole/lua/settings_header.lp','r')
|
||||
</div>
|
||||
<div class="box box-warning settings-level-2">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">Rate-limiting <i class="fas fa-wrench" title="This is an advanced-level setting"></i></h3>
|
||||
<h3 class="box-title dns.rateLimit.count dns.rateLimit.interval">Rate-limiting</h3>
|
||||
</div>
|
||||
<div class="box-body">
|
||||
<div class="row">
|
||||
|
||||
+13
-13
@@ -17,7 +17,7 @@ mg.include('scripts/pi-hole/lua/settings_header.lp','r')
|
||||
<div class="col-md-6 settings-level-1">
|
||||
<div class="box">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">Local DNS records</h3>
|
||||
<h3 class="box-title" id="title-hosts">Local DNS records<span class="restart-warning"> <i class="fas fa-redo text-orange" title="Setting requires FTL restart on change"></i></span></h3>
|
||||
</div>
|
||||
<div class="box-body">
|
||||
<div class="row">
|
||||
@@ -29,7 +29,7 @@ mg.include('scripts/pi-hole/lua/settings_header.lp','r')
|
||||
Add new DNS record
|
||||
</h3>
|
||||
<div class="box-tools pull-right">
|
||||
<button type="button" class="btn btn-box-tool" data-widget="collapse"><i class="fa fa-plus"></i></button>
|
||||
<button type="button" class="btn btn-box-tool hosts" data-widget="collapse"><i class="fa fa-plus"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /.box-header -->
|
||||
@@ -37,11 +37,11 @@ mg.include('scripts/pi-hole/lua/settings_header.lp','r')
|
||||
<div class="row">
|
||||
<div class="form-group col-md-6">
|
||||
<label for="domain">Domain:</label>
|
||||
<input id="Hdomain" type="url" class="form-control" placeholder="Add a domain (example.com or sub.example.com)" autocomplete="off" spellcheck="false" autocapitalize="none" autocorrect="off">
|
||||
<input id="Hdomain" type="url" class="form-control hosts" placeholder="Add a domain (example.com or sub.example.com)" autocomplete="off" spellcheck="false" autocapitalize="none" autocorrect="off">
|
||||
</div>
|
||||
<div class="form-group col-md-6">
|
||||
<label for="target">IP address:</label>
|
||||
<input id="Hip" type="text" class="form-control" placeholder="Associated IP address" autocomplete="off" spellcheck="false" autocapitalize="none" autocorrect="off">
|
||||
<input id="Hip" type="text" class="form-control hosts" placeholder="Associated IP address" autocomplete="off" spellcheck="false" autocapitalize="none" autocorrect="off">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -51,14 +51,14 @@ mg.include('scripts/pi-hole/lua/settings_header.lp','r')
|
||||
<p>The reason for this is that Pi-hole will not send additional queries upstream when serving <code>CNAME</code> replies. As consequence, if you set a target that isn't already known, the reply to the client may be incomplete. Pi-hole just returns the information it knows at the time of the query. This results in certain limitations for <code>CNAME</code> targets,
|
||||
for instance, only <i>active</i> DHCP leases work as targets - mere DHCP <i>leases</i> aren't sufficient as they aren't (yet) valid DNS records.</p>
|
||||
<p>Additionally, you can't <code>CNAME</code> external domains (<code>bing.com</code> to <code>google.com</code>) successfully as this could result in invalid SSL certificate errors when the target server does not serve content for the requested domain.</p>
|
||||
<button type="button" id="btnAdd-host" class="btn btn-primary pull-right">Add</button>
|
||||
<button type="button" id="btnAdd-host" class="btn btn-primary pull-right hosts">Add</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<h3 class="box-title">List of local DNS records</h3>
|
||||
<!-- /.box-header -->
|
||||
<table id="dnsRecordsTable" class="table table-striped table-bordered" width="100%">
|
||||
<table id="hosts-Table" class="table table-striped table-bordered" width="100%">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Domain</th>
|
||||
@@ -76,7 +76,7 @@ mg.include('scripts/pi-hole/lua/settings_header.lp','r')
|
||||
<div class="col-md-6 settings-level-1">
|
||||
<div class="box">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">Local CNAME records records</h3>
|
||||
<h3 class="box-title" id="title-cnameRecords">Local CNAME records records<span class="restart-warning"> <i class="fas fa-redo text-orange" title="Setting requires FTL restart on change"></i></span></h3>
|
||||
</div>
|
||||
<div class="box-body">
|
||||
<div class="row">
|
||||
@@ -88,7 +88,7 @@ mg.include('scripts/pi-hole/lua/settings_header.lp','r')
|
||||
Add new CNAME record
|
||||
</h3>
|
||||
<div class="box-tools pull-right">
|
||||
<button type="button" class="btn btn-box-tool" data-widget="collapse"><i class="fa fa-plus"></i></button>
|
||||
<button type="button" class="btn btn-box-tool cnameRecords" data-widget="collapse"><i class="fa fa-plus"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /.box-header -->
|
||||
@@ -96,27 +96,27 @@ mg.include('scripts/pi-hole/lua/settings_header.lp','r')
|
||||
<div class="row">
|
||||
<div class="form-group col-md-5">
|
||||
<label for="domain">Domain:</label>
|
||||
<input id="Cdomain" type="url" class="form-control" placeholder="Add a domain (example.com or sub.example.com)" autocomplete="off" spellcheck="false" autocapitalize="none" autocorrect="off">
|
||||
<input id="Cdomain" type="url" class="form-control cnameRecords" placeholder="Add a domain (example.com or sub.example.com)" autocomplete="off" spellcheck="false" autocapitalize="none" autocorrect="off">
|
||||
</div>
|
||||
<div class="form-group col-md-5">
|
||||
<label for="target">Target Domain:</label>
|
||||
<input id="Ctarget" type="url" class="form-control" placeholder="Associated Target Domain" autocomplete="off" spellcheck="false" autocapitalize="none" autocorrect="off">
|
||||
<input id="Ctarget" type="url" class="form-control cnameRecords" placeholder="Associated Target Domain" autocomplete="off" spellcheck="false" autocapitalize="none" autocorrect="off">
|
||||
</div>
|
||||
<div class="form-group col-md-2">
|
||||
<label for="target">TTL (optional):</label>
|
||||
<input id="Cttl" type="numeric" class="form-control" placeholder="TTL in seconds" autocomplete="off" spellcheck="false" autocapitalize="none" autocorrect="off">
|
||||
<input id="Cttl" type="numeric" class="form-control cnameRecords" placeholder="TTL in seconds" autocomplete="off" spellcheck="false" autocapitalize="none" autocorrect="off">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-footer clearfix">
|
||||
<button type="button" id="btnAdd-cname" class="btn btn-primary pull-right">Add</button>
|
||||
<button type="button" id="btnAdd-cname" class="btn btn-primary pull-right cnameRecords">Add</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<h3 class="box-title">List of local CNAME records</h3>
|
||||
<!-- /.box-header -->
|
||||
<table id="customCNAMETable" class="table table-striped table-bordered" width="100%">
|
||||
<table id="cnameRecords-Table" class="table table-striped table-bordered" width="100%">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Domain</th>
|
||||
|
||||
+3
-3
@@ -19,7 +19,7 @@ mg.include('scripts/pi-hole/lua/settings_header.lp','r')
|
||||
<div class="col-md-12">
|
||||
<div class="box box-warning">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">Query Logging</h3>
|
||||
<h3 class="box-title dns.queryLogging">Query Logging</h3>
|
||||
</div>
|
||||
<div class="box-body">
|
||||
<div class="row">
|
||||
@@ -37,7 +37,7 @@ mg.include('scripts/pi-hole/lua/settings_header.lp','r')
|
||||
<div class="col-md-12 settings-level-2">
|
||||
<div class="box box-warning">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">Privacy-related database settings</h3>
|
||||
<h3 class="box-title database.DBimport database.DBexport database.maxDBdays database.network.expire">Privacy-related database settings</h3>
|
||||
</div>
|
||||
<div class="box-body">
|
||||
<div class="row">
|
||||
@@ -70,7 +70,7 @@ mg.include('scripts/pi-hole/lua/settings_header.lp','r')
|
||||
<div class="col-md-6 settings-level-1">
|
||||
<div class="box box-warning">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">Query Anonymization ("Privacy Level")</h3>
|
||||
<h3 class="box-title misc.privacylevel">Query Anonymization ("Privacy Level")</h3>
|
||||
</div>
|
||||
<div class="box-body">
|
||||
<div class="row">
|
||||
|
||||
+1
-1
@@ -282,7 +282,7 @@ mg.include('scripts/pi-hole/lua/settings_header.lp','r')
|
||||
<div class="col-md-12 settings-level-1">
|
||||
<div class="box box-warning">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">Actions<span id="actions-title"></span> <i class="fas fa-wrench" title="This is an advanced-level setting"></i></h3>
|
||||
<h3 class="box-title">Actions<span id="actions-title"></span></h3>
|
||||
</div>
|
||||
<div class="box-body">
|
||||
<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>
|
||||
|
||||
+220
-3
@@ -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;
|
||||
@@ -1181,3 +1181,220 @@ table.dataTable tbody > tr > .selected {
|
||||
font-weight: 500;
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
.m-0 {
|
||||
margin: 0 !important;
|
||||
}
|
||||
.m-1 {
|
||||
margin: 0.25rem !important;
|
||||
}
|
||||
.m-2 {
|
||||
margin: 0.5rem !important;
|
||||
}
|
||||
.m-3 {
|
||||
margin: 1rem !important;
|
||||
}
|
||||
.m-4 {
|
||||
margin: 1.5rem !important;
|
||||
}
|
||||
.m-5 {
|
||||
margin: 3rem !important;
|
||||
}
|
||||
|
||||
.mt-0 {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
.mr-0 {
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
.mb-0 {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
.ml-0 {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
.mx-0 {
|
||||
margin-left: 0 !important;
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
.my-0 {
|
||||
margin-top: 0 !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.mt-1 {
|
||||
margin-top: 0.25rem !important;
|
||||
}
|
||||
.mr-1 {
|
||||
margin-right: 0.25rem !important;
|
||||
}
|
||||
.mb-1 {
|
||||
margin-bottom: 0.25rem !important;
|
||||
}
|
||||
.ml-1 {
|
||||
margin-left: 0.25rem !important;
|
||||
}
|
||||
.mx-1 {
|
||||
margin-left: 0.25rem !important;
|
||||
margin-right: 0.25rem !important;
|
||||
}
|
||||
.my-1 {
|
||||
margin-top: 0.25rem !important;
|
||||
margin-bottom: 0.25rem !important;
|
||||
}
|
||||
|
||||
.mt-2 {
|
||||
margin-top: 0.5rem !important;
|
||||
}
|
||||
.mr-2 {
|
||||
margin-right: 0.5rem !important;
|
||||
}
|
||||
.mb-2 {
|
||||
margin-bottom: 0.5rem !important;
|
||||
}
|
||||
.ml-2 {
|
||||
margin-left: 0.5rem !important;
|
||||
}
|
||||
.mx-2 {
|
||||
margin-right: 0.5rem !important;
|
||||
margin-left: 0.5rem !important;
|
||||
}
|
||||
.my-2 {
|
||||
margin-top: 0.5rem !important;
|
||||
margin-bottom: 0.5rem !important;
|
||||
}
|
||||
|
||||
.mt-3 {
|
||||
margin-top: 1rem !important;
|
||||
}
|
||||
.mr-3 {
|
||||
margin-right: 1rem !important;
|
||||
}
|
||||
.mb-3 {
|
||||
margin-bottom: 1rem !important;
|
||||
}
|
||||
.ml-3 {
|
||||
margin-left: 1rem !important;
|
||||
}
|
||||
.mx-3 {
|
||||
margin-right: 1rem !important;
|
||||
margin-left: 1rem !important;
|
||||
}
|
||||
.my-3 {
|
||||
margin-bottom: 1rem !important;
|
||||
margin-top: 1rem !important;
|
||||
}
|
||||
|
||||
.p-0 {
|
||||
padding: 0 !important;
|
||||
}
|
||||
.p-1 {
|
||||
padding: 0.25rem !important;
|
||||
}
|
||||
.p-2 {
|
||||
padding: 0.5rem !important;
|
||||
}
|
||||
.p-3 {
|
||||
padding: 1rem !important;
|
||||
}
|
||||
.p-4 {
|
||||
padding: 1.5rem !important;
|
||||
}
|
||||
.p-5 {
|
||||
padding: 3rem !important;
|
||||
}
|
||||
|
||||
.pt-0 {
|
||||
padding-top: 0 !important;
|
||||
}
|
||||
.pr-0 {
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
.pb-0 {
|
||||
padding-bottom: 0 !important;
|
||||
}
|
||||
.pl-0 {
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
.px-0 {
|
||||
padding-left: 0 !important;
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
.py-0 {
|
||||
padding-top: 0 !important;
|
||||
padding-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.pt-1 {
|
||||
padding-top: 0.25rem !important;
|
||||
}
|
||||
.pr-1 {
|
||||
padding-right: 0.25rem !important;
|
||||
}
|
||||
.pb-1 {
|
||||
padding-bottom: 0.25rem !important;
|
||||
}
|
||||
.pl-1 {
|
||||
padding-left: 0.25rem !important;
|
||||
}
|
||||
.px-1 {
|
||||
padding-left: 0.25rem !important;
|
||||
padding-right: 0.25rem !important;
|
||||
}
|
||||
.py-1 {
|
||||
padding-top: 0.25rem !important;
|
||||
padding-bottom: 0.25rem !important;
|
||||
}
|
||||
|
||||
.pt-2 {
|
||||
padding-top: 0.5rem !important;
|
||||
}
|
||||
.pr-2 {
|
||||
padding-right: 0.5rem !important;
|
||||
}
|
||||
.pb-2 {
|
||||
padding-bottom: 0.5rem !important;
|
||||
}
|
||||
.pl-2 {
|
||||
padding-left: 0.5rem !important;
|
||||
}
|
||||
.px-2 {
|
||||
padding-right: 0.5rem !important;
|
||||
padding-left: 0.5rem !important;
|
||||
}
|
||||
.py-2 {
|
||||
padding-top: 0.5rem !important;
|
||||
padding-bottom: 0.5rem !important;
|
||||
}
|
||||
|
||||
.pt-3 {
|
||||
padding-top: 1rem !important;
|
||||
}
|
||||
.pr-3 {
|
||||
padding-right: 1rem !important;
|
||||
}
|
||||
.pb-3 {
|
||||
padding-bottom: 1rem !important;
|
||||
}
|
||||
.pl-3 {
|
||||
padding-left: 1rem !important;
|
||||
}
|
||||
.px-3 {
|
||||
padding-right: 1rem !important;
|
||||
padding-left: 1rem !important;
|
||||
}
|
||||
.py-3 {
|
||||
padding-bottom: 1rem !important;
|
||||
padding-top: 1rem !important;
|
||||
}
|
||||
|
||||
.password_background {
|
||||
background-image: repeating-linear-gradient(
|
||||
45deg,
|
||||
white 0%,
|
||||
white 2%,
|
||||
rgb(0, 0, 0) 2%,
|
||||
rgb(0, 0, 0) 4%,
|
||||
white 4%
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user