Merge development into new/simple-dhcp-static-leases and address review feedback

Resolve merge conflict in style/pi-hole.css (keep both StaticDHCPTable
styles and DNSSEC query log styles).

Address outstanding reviewer feedback:
- Change save button icon from floppy-disk to checkmark to clarify it
  confirms the row edit, not a final save
- Update hint text to mention "Save & Apply" is still needed
- Add hostname validation on the hostname cell (rejects spaces, commas,
  and other characters invalid in DNS names)

Signed-off-by: Dominik <dl6er@dl6er.de>
This commit is contained in:
Dominik
2026-03-25 07:23:10 +01:00
28 changed files with 336 additions and 236 deletions

View File

@@ -26,21 +26,21 @@ jobs:
steps: steps:
- name: Clone repository - name: Clone repository
uses: actions/checkout@v4.2.2 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 #v5.0.0
with: with:
persist-credentials: false persist-credentials: false
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v3.29.2 uses: github/codeql-action/init@16140ae1a102900babc80a33c44059580f687047 #v4.30.9
with: with:
config-file: ./.github/codeql/codeql-config.yml config-file: ./.github/codeql/codeql-config.yml
languages: "javascript" languages: "javascript"
queries: +security-and-quality queries: +security-and-quality
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@v3.29.2 uses: github/codeql-action/autobuild@16140ae1a102900babc80a33c44059580f687047 #v4.30.9
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3.29.2 uses: github/codeql-action/analyze@16140ae1a102900babc80a33c44059580f687047 #v4.30.9
with: with:
category: "/language:javascript" category: "/language:javascript"

View File

@@ -13,12 +13,12 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Clone repository - name: Clone repository
uses: actions/checkout@v4.2.2 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 #v5.0.0
with: with:
persist-credentials: false persist-credentials: false
- name: Spell-Checking - name: Spell-Checking
uses: codespell-project/actions-codespell@master uses: codespell-project/actions-codespell@406322ec52dd7b488e48c1c4b82e2a8b3a1bf630 #v2.1
with: with:
ignore_words_file: .codespellignore ignore_words_file: .codespellignore
skip: ./vendor,./package.json,./package-lock.json skip: ./vendor,./package.json,./package-lock.json

View File

@@ -10,8 +10,8 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Clone repository - name: Clone repository
uses: actions/checkout@v4.2.2 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 #v5.0.0
with: with:
persist-credentials: false persist-credentials: false
- uses: editorconfig-checker/action-editorconfig-checker@main - uses: editorconfig-checker/action-editorconfig-checker@5ecdd656fe347c26f76b1b435b90e1d74fb5e787 # tag v2. is really out of date
- run: editorconfig-checker - run: editorconfig-checker

View File

@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check if PRs are have merge conflicts - name: Check if PRs are have merge conflicts
uses: eps1lon/actions-label-merge-conflict@v3.0.3 uses: eps1lon/actions-label-merge-conflict@1df065ebe6e3310545d4f4c4e862e43bdca146f0 #v3.0.3
with: with:
dirtyLabel: "Merge Conflicts" dirtyLabel: "Merge Conflicts"
repoToken: "${{ secrets.GITHUB_TOKEN }}" repoToken: "${{ secrets.GITHUB_TOKEN }}"

View File

@@ -17,7 +17,7 @@ jobs:
issues: write issues: write
steps: steps:
- uses: actions/stale@v9.1.0 - uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 #v10.1.0
with: with:
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: 30 days-before-stale: 30
@@ -41,7 +41,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Clone repository - name: Clone repository
uses: actions/checkout@v4.2.2 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 #v5.0.0
with: with:
persist-credentials: false persist-credentials: false
- name: Remove 'stale' label - name: Remove 'stale' label

View File

@@ -16,7 +16,7 @@ jobs:
pull-requests: write pull-requests: write
steps: steps:
- uses: actions/stale@v9.1.0 - uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 #v10.1.0
with: with:
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
# Do not automatically mark PR/issue as stale # Do not automatically mark PR/issue as stale

View File

@@ -11,7 +11,7 @@ jobs:
name: Syncing branches name: Syncing branches
steps: steps:
- name: Clone repository - name: Clone repository
uses: actions/checkout@v4.2.2 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 #v5.0.0
with: with:
persist-credentials: false persist-credentials: false
- name: Opening pull request - name: Opening pull request

View File

@@ -19,12 +19,12 @@ jobs:
steps: steps:
- name: Clone repository - name: Clone repository
uses: actions/checkout@v4.2.2 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 #v5.0.0
with: with:
persist-credentials: false persist-credentials: false
- name: Set up Node.js - name: Set up Node.js
uses: actions/setup-node@v4.4.0 uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 #v6.0.0
with: with:
node-version: "22.x" node-version: "22.x"
cache: npm cache: npm

View File

@@ -9,14 +9,15 @@
mg.include('scripts/lua/header.lp','r') mg.include('scripts/lua/header.lp','r')
?> ?>
<body class="hold-transition layout-boxed login-page page-<?=pihole.format_path(mg.request_info.request_uri)?>"> </head>
<body class="hold-transition layout-boxed login-page page-<?=pihole.format_path(scriptname)?>">
<div class="box login-box"> <div class="box login-box">
<section style="padding: 15px;"> <section style="padding: 15px;">
<h2 class="error-headline text-danger">403</h2> <h2 class="error-headline text-danger">403</h2>
<div class="error-content"> <div class="error-content">
<h3><i class="fa fa-times-circle text-danger"></i> Oops! Access denied.</h3> <h3><i class="fa fa-times-circle text-danger"></i> Oops! Access denied.</h3>
<p> <p>
You don't have permission to access <code><?=mg.request_info.request_uri?></code> on this server.<br> You don't have permission to access this URL.<br>
Did you mean to go to <a href="<?=pihole.webhome()?>">your Pi-hole's dashboard</a> instead? Did you mean to go to <a href="<?=pihole.webhome()?>">your Pi-hole's dashboard</a> instead?
</p> </p>
</div> </div>

View File

@@ -9,7 +9,8 @@
mg.include('scripts/lua/header.lp','r') mg.include('scripts/lua/header.lp','r')
?> ?>
<body class="hold-transition layout-boxed login-page page-<?=pihole.format_path(mg.request_info.request_uri)?>"> </head>
<body class="hold-transition layout-boxed login-page page-<?=pihole.format_path(scriptname)?>">
<div class="box login-box"> <div class="box login-box">
<section style="padding: 15px;"> <section style="padding: 15px;">
<h2 class="error-headline text-yellow">404</h2> <h2 class="error-headline text-yellow">404</h2>

View File

@@ -9,7 +9,8 @@
mg.include('scripts/lua/header.lp','r') mg.include('scripts/lua/header.lp','r')
?> ?>
<body class="hold-transition layout-boxed login-page page-<?=pihole.format_path(mg.request_info.request_uri)?>" data-apiurl="<?=pihole.api_url()?>" data-webhome="<?=webhome?>"> </head>
<body class="hold-transition layout-boxed login-page page-<?=pihole.format_path(scriptname)?>" data-apiurl="<?=pihole.api_url()?>" data-webhome="<?=webhome?>">
<div class="box login-box" id="login-box"> <div class="box login-box" id="login-box">
<section style="padding: 15px;"> <section style="padding: 15px;">
<div class="login-logo"> <div class="login-logo">
@@ -50,9 +51,13 @@ mg.include('scripts/lua/header.lp','r')
</span> </span>
</div> </div>
</div> </div>
<div class="form-group has-feedback hidden" id="totp_input"> <div class="form-group hidden" id="totp_input">
<input type="text" id="totp" size="6" maxlen="6" class="form-control totp_token" placeholder="123456" value="" spellcheck="false" autofocus autocomplete="off"> <div class="input-group">
<span class="fa fa-clock-rotate-left pwd-field form-control-feedback"></span> <input type="text" id="totp" size="6" maxlen="6" class="form-control totp_token" placeholder="123456" value="" spellcheck="false" autofocus autocomplete="off">
<div class="input-group-addon" data-toggle="tooltip" data-placement="auto" title="TOTP verification code">
<i class="fa fa-clock-rotate-left"></i>
</div>
</div>
</div> </div>
<div class="form-group"> <div class="form-group">
<button type="submit" class="btn btn-primary form-control"><i class="fas fa-sign-in-alt"></i>&nbsp;&nbsp;&nbsp;Log in (uses cookie)</button> <button type="submit" class="btn btn-primary form-control"><i class="fas fa-sign-in-alt"></i>&nbsp;&nbsp;&nbsp;Log in (uses cookie)</button>

235
package-lock.json generated
View File

@@ -37,13 +37,13 @@
}, },
"devDependencies": { "devDependencies": {
"autoprefixer": "^10.4.21", "autoprefixer": "^10.4.21",
"eslint": "^9.31.0", "eslint": "^9.38.0",
"eslint-plugin-compat": "^6.0.2", "eslint-plugin-compat": "^6.0.2",
"globals": "^16.3.0", "globals": "^16.4.0",
"postcss": "^8.5.6", "postcss": "^8.5.6",
"postcss-cli": "^11.0.1", "postcss-cli": "^11.0.1",
"prettier": "^3.6.2", "prettier": "^3.6.2",
"xo": "^1.1.1" "xo": "^1.2.3"
} }
}, },
"node_modules/@babel/code-frame": { "node_modules/@babel/code-frame": {
@@ -126,9 +126,9 @@
} }
}, },
"node_modules/@eslint-community/eslint-utils": { "node_modules/@eslint-community/eslint-utils": {
"version": "4.7.0", "version": "4.9.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz",
"integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -168,13 +168,13 @@
} }
}, },
"node_modules/@eslint/config-array": { "node_modules/@eslint/config-array": {
"version": "0.21.0", "version": "0.21.1",
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz",
"integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@eslint/object-schema": "^2.1.6", "@eslint/object-schema": "^2.1.7",
"debug": "^4.3.1", "debug": "^4.3.1",
"minimatch": "^3.1.2" "minimatch": "^3.1.2"
}, },
@@ -183,19 +183,22 @@
} }
}, },
"node_modules/@eslint/config-helpers": { "node_modules/@eslint/config-helpers": {
"version": "0.3.0", "version": "0.4.1",
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.1.tgz",
"integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", "integrity": "sha512-csZAzkNhsgwb0I/UAV6/RGFTbiakPCf0ZrGmrIxQpYvGZ00PhTkSnyKNolphgIvmnJeGw6rcGVEXfTzUnFuEvw==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": {
"@eslint/core": "^0.16.0"
},
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
} }
}, },
"node_modules/@eslint/core": { "node_modules/@eslint/core": {
"version": "0.15.1", "version": "0.16.0",
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.16.0.tgz",
"integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", "integrity": "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
@@ -243,9 +246,9 @@
} }
}, },
"node_modules/@eslint/js": { "node_modules/@eslint/js": {
"version": "9.31.0", "version": "9.38.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.31.0.tgz", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.38.0.tgz",
"integrity": "sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==", "integrity": "sha512-UZ1VpFvXf9J06YG9xQBdnzU+kthors6KjhMAl6f4gH4usHyh31rUf2DLGInT8RFYIReYXNSydgPY0V2LuWgl7A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -256,9 +259,9 @@
} }
}, },
"node_modules/@eslint/object-schema": { "node_modules/@eslint/object-schema": {
"version": "2.1.6", "version": "2.1.7",
"resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz",
"integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"engines": { "engines": {
@@ -266,13 +269,13 @@
} }
}, },
"node_modules/@eslint/plugin-kit": { "node_modules/@eslint/plugin-kit": {
"version": "0.3.3", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.3.tgz", "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.0.tgz",
"integrity": "sha512-1+WqvgNMhmlAambTvT3KPtCl/Ibr68VldY2XY40SL1CE0ZXiakFR/cbTspaF5HsnpDMvcYYoJHfl4980NBjGag==", "integrity": "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@eslint/core": "^0.15.1", "@eslint/core": "^0.16.0",
"levn": "^0.4.1" "levn": "^0.4.1"
}, },
"engines": { "engines": {
@@ -563,17 +566,17 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@typescript-eslint/eslint-plugin": { "node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.35.0", "version": "8.37.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.35.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.37.0.tgz",
"integrity": "sha512-ijItUYaiWuce0N1SoSMrEd0b6b6lYkYt99pqCPfybd+HKVXtEvYhICfLdwp42MhiI5mp0oq7PKEL+g1cNiz/Eg==", "integrity": "sha512-jsuVWeIkb6ggzB+wPCsR4e6loj+rM72ohW6IBn2C+5NCvfUVY8s33iFPySSVXqtm5Hu29Ne/9bnA0JmyLmgenA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@eslint-community/regexpp": "^4.10.0", "@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "8.35.0", "@typescript-eslint/scope-manager": "8.37.0",
"@typescript-eslint/type-utils": "8.35.0", "@typescript-eslint/type-utils": "8.37.0",
"@typescript-eslint/utils": "8.35.0", "@typescript-eslint/utils": "8.37.0",
"@typescript-eslint/visitor-keys": "8.35.0", "@typescript-eslint/visitor-keys": "8.37.0",
"graphemer": "^1.4.0", "graphemer": "^1.4.0",
"ignore": "^7.0.0", "ignore": "^7.0.0",
"natural-compare": "^1.4.0", "natural-compare": "^1.4.0",
@@ -587,7 +590,7 @@
"url": "https://opencollective.com/typescript-eslint" "url": "https://opencollective.com/typescript-eslint"
}, },
"peerDependencies": { "peerDependencies": {
"@typescript-eslint/parser": "^8.35.0", "@typescript-eslint/parser": "^8.37.0",
"eslint": "^8.57.0 || ^9.0.0", "eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <5.9.0" "typescript": ">=4.8.4 <5.9.0"
} }
@@ -603,16 +606,16 @@
} }
}, },
"node_modules/@typescript-eslint/parser": { "node_modules/@typescript-eslint/parser": {
"version": "8.35.0", "version": "8.37.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.35.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.37.0.tgz",
"integrity": "sha512-6sMvZePQrnZH2/cJkwRpkT7DxoAWh+g6+GFRK6bV3YQo7ogi3SX5rgF6099r5Q53Ma5qeT7LGmOmuIutF4t3lA==", "integrity": "sha512-kVIaQE9vrN9RLCQMQ3iyRlVJpTiDUY6woHGb30JDkfJErqrQEmtdWH3gV0PBAfGZgQXoqzXOO0T3K6ioApbbAA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "8.35.0", "@typescript-eslint/scope-manager": "8.37.0",
"@typescript-eslint/types": "8.35.0", "@typescript-eslint/types": "8.37.0",
"@typescript-eslint/typescript-estree": "8.35.0", "@typescript-eslint/typescript-estree": "8.37.0",
"@typescript-eslint/visitor-keys": "8.35.0", "@typescript-eslint/visitor-keys": "8.37.0",
"debug": "^4.3.4" "debug": "^4.3.4"
}, },
"engines": { "engines": {
@@ -628,14 +631,14 @@
} }
}, },
"node_modules/@typescript-eslint/project-service": { "node_modules/@typescript-eslint/project-service": {
"version": "8.35.0", "version": "8.37.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.35.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.37.0.tgz",
"integrity": "sha512-41xatqRwWZuhUMF/aZm2fcUsOFKNcG28xqRSS6ZVr9BVJtGExosLAm5A1OxTjRMagx8nJqva+P5zNIGt8RIgbQ==", "integrity": "sha512-BIUXYsbkl5A1aJDdYJCBAo8rCEbAvdquQ8AnLb6z5Lp1u3x5PNgSSx9A/zqYc++Xnr/0DVpls8iQ2cJs/izTXA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.35.0", "@typescript-eslint/tsconfig-utils": "^8.37.0",
"@typescript-eslint/types": "^8.35.0", "@typescript-eslint/types": "^8.37.0",
"debug": "^4.3.4" "debug": "^4.3.4"
}, },
"engines": { "engines": {
@@ -650,14 +653,14 @@
} }
}, },
"node_modules/@typescript-eslint/scope-manager": { "node_modules/@typescript-eslint/scope-manager": {
"version": "8.35.0", "version": "8.37.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.35.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.37.0.tgz",
"integrity": "sha512-+AgL5+mcoLxl1vGjwNfiWq5fLDZM1TmTPYs2UkyHfFhgERxBbqHlNjRzhThJqz+ktBqTChRYY6zwbMwy0591AA==", "integrity": "sha512-0vGq0yiU1gbjKob2q691ybTg9JX6ShiVXAAfm2jGf3q0hdP6/BruaFjL/ManAR/lj05AvYCH+5bbVo0VtzmjOA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "8.35.0", "@typescript-eslint/types": "8.37.0",
"@typescript-eslint/visitor-keys": "8.35.0" "@typescript-eslint/visitor-keys": "8.37.0"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -668,9 +671,9 @@
} }
}, },
"node_modules/@typescript-eslint/tsconfig-utils": { "node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.35.0", "version": "8.37.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.35.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.37.0.tgz",
"integrity": "sha512-04k/7247kZzFraweuEirmvUj+W3bJLI9fX6fbo1Qm2YykuBvEhRTPl8tcxlYO8kZZW+HIXfkZNoasVb8EV4jpA==", "integrity": "sha512-1/YHvAVTimMM9mmlPvTec9NP4bobA1RkDbMydxG8omqwJJLEW/Iy2C4adsAESIXU3WGLXFHSZUU+C9EoFWl4Zg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -685,14 +688,15 @@
} }
}, },
"node_modules/@typescript-eslint/type-utils": { "node_modules/@typescript-eslint/type-utils": {
"version": "8.35.0", "version": "8.37.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.35.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.37.0.tgz",
"integrity": "sha512-ceNNttjfmSEoM9PW87bWLDEIaLAyR+E6BoYJQ5PfaDau37UGca9Nyq3lBk8Bw2ad0AKvYabz6wxc7DMTO2jnNA==", "integrity": "sha512-SPkXWIkVZxhgwSwVq9rqj/4VFo7MnWwVaRNznfQDc/xPYHjXnPfLWn+4L6FF1cAz6e7dsqBeMawgl7QjUMj4Ow==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/typescript-estree": "8.35.0", "@typescript-eslint/types": "8.37.0",
"@typescript-eslint/utils": "8.35.0", "@typescript-eslint/typescript-estree": "8.37.0",
"@typescript-eslint/utils": "8.37.0",
"debug": "^4.3.4", "debug": "^4.3.4",
"ts-api-utils": "^2.1.0" "ts-api-utils": "^2.1.0"
}, },
@@ -709,9 +713,9 @@
} }
}, },
"node_modules/@typescript-eslint/types": { "node_modules/@typescript-eslint/types": {
"version": "8.35.0", "version": "8.37.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.35.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.37.0.tgz",
"integrity": "sha512-0mYH3emanku0vHw2aRLNGqe7EXh9WHEhi7kZzscrMDf6IIRUQ5Jk4wp1QrledE/36KtdZrVfKnE32eZCf/vaVQ==", "integrity": "sha512-ax0nv7PUF9NOVPs+lmQ7yIE7IQmAf8LGcXbMvHX5Gm+YJUYNAl340XkGnrimxZ0elXyoQJuN5sbg6C4evKA4SQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -723,16 +727,16 @@
} }
}, },
"node_modules/@typescript-eslint/typescript-estree": { "node_modules/@typescript-eslint/typescript-estree": {
"version": "8.35.0", "version": "8.37.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.35.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.37.0.tgz",
"integrity": "sha512-F+BhnaBemgu1Qf8oHrxyw14wq6vbL8xwWKKMwTMwYIRmFFY/1n/9T/jpbobZL8vp7QyEUcC6xGrnAO4ua8Kp7w==", "integrity": "sha512-zuWDMDuzMRbQOM+bHyU4/slw27bAUEcKSKKs3hcv2aNnc/tvE/h7w60dwVw8vnal2Pub6RT1T7BI8tFZ1fE+yg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/project-service": "8.35.0", "@typescript-eslint/project-service": "8.37.0",
"@typescript-eslint/tsconfig-utils": "8.35.0", "@typescript-eslint/tsconfig-utils": "8.37.0",
"@typescript-eslint/types": "8.35.0", "@typescript-eslint/types": "8.37.0",
"@typescript-eslint/visitor-keys": "8.35.0", "@typescript-eslint/visitor-keys": "8.37.0",
"debug": "^4.3.4", "debug": "^4.3.4",
"fast-glob": "^3.3.2", "fast-glob": "^3.3.2",
"is-glob": "^4.0.3", "is-glob": "^4.0.3",
@@ -778,16 +782,16 @@
} }
}, },
"node_modules/@typescript-eslint/utils": { "node_modules/@typescript-eslint/utils": {
"version": "8.35.0", "version": "8.37.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.35.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.37.0.tgz",
"integrity": "sha512-nqoMu7WWM7ki5tPgLVsmPM8CkqtoPUG6xXGeefM5t4x3XumOEKMoUZPdi+7F+/EotukN4R9OWdmDxN80fqoZeg==", "integrity": "sha512-TSFvkIW6gGjN2p6zbXo20FzCABbyUAuq6tBvNRGsKdsSQ6a7rnV6ADfZ7f4iI3lIiXc4F4WWvtUfDw9CJ9pO5A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.7.0", "@eslint-community/eslint-utils": "^4.7.0",
"@typescript-eslint/scope-manager": "8.35.0", "@typescript-eslint/scope-manager": "8.37.0",
"@typescript-eslint/types": "8.35.0", "@typescript-eslint/types": "8.37.0",
"@typescript-eslint/typescript-estree": "8.35.0" "@typescript-eslint/typescript-estree": "8.37.0"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -802,13 +806,13 @@
} }
}, },
"node_modules/@typescript-eslint/visitor-keys": { "node_modules/@typescript-eslint/visitor-keys": {
"version": "8.35.0", "version": "8.37.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.35.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.37.0.tgz",
"integrity": "sha512-zTh2+1Y8ZpmeQaQVIc/ZZxsx8UzgKJyNg1PTvjzC7WMhPSVS8bfDX34k1SrwOf016qd5RU3az2UxUNue3IfQ5g==", "integrity": "sha512-YzfhzcTnZVPiLfP/oeKtDp2evwvHLMe0LOy7oe+hb9KKIumLNohYS9Hgp1ifwpu42YWxhZE8yieggz6JpqO/1w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "8.35.0", "@typescript-eslint/types": "8.37.0",
"eslint-visitor-keys": "^4.2.1" "eslint-visitor-keys": "^4.2.1"
}, },
"engines": { "engines": {
@@ -3161,25 +3165,24 @@
} }
}, },
"node_modules/eslint": { "node_modules/eslint": {
"version": "9.31.0", "version": "9.38.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.31.0.tgz", "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.38.0.tgz",
"integrity": "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==", "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1", "@eslint-community/regexpp": "^4.12.1",
"@eslint/config-array": "^0.21.0", "@eslint/config-array": "^0.21.1",
"@eslint/config-helpers": "^0.3.0", "@eslint/config-helpers": "^0.4.1",
"@eslint/core": "^0.15.0", "@eslint/core": "^0.16.0",
"@eslint/eslintrc": "^3.3.1", "@eslint/eslintrc": "^3.3.1",
"@eslint/js": "9.31.0", "@eslint/js": "9.38.0",
"@eslint/plugin-kit": "^0.3.1", "@eslint/plugin-kit": "^0.4.0",
"@humanfs/node": "^0.16.6", "@humanfs/node": "^0.16.6",
"@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/module-importer": "^1.0.1",
"@humanwhocodes/retry": "^0.4.2", "@humanwhocodes/retry": "^0.4.2",
"@types/estree": "^1.0.6", "@types/estree": "^1.0.6",
"@types/json-schema": "^7.0.15",
"ajv": "^6.12.4", "ajv": "^6.12.4",
"chalk": "^4.0.0", "chalk": "^4.0.0",
"cross-spawn": "^7.0.6", "cross-spawn": "^7.0.6",
@@ -3632,14 +3635,13 @@
} }
}, },
"node_modules/eslint-plugin-n": { "node_modules/eslint-plugin-n": {
"version": "17.20.0", "version": "17.21.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-17.20.0.tgz", "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-17.21.0.tgz",
"integrity": "sha512-IRSoatgB/NQJZG5EeTbv/iAx1byOGdbbyhQrNvWdCfTnmPxUT0ao9/eGOeG7ljD8wJBsxwE8f6tES5Db0FRKEw==", "integrity": "sha512-1+iZ8We4ZlwVMtb/DcHG3y5/bZOdazIpa/4TySo22MLKdwrLcfrX0hbadnCvykSQCCmkAnWmIP8jZVb2AAq29A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.5.0", "@eslint-community/eslint-utils": "^4.5.0",
"@typescript-eslint/utils": "^8.26.1",
"enhanced-resolve": "^5.17.1", "enhanced-resolve": "^5.17.1",
"eslint-plugin-es-x": "^7.8.0", "eslint-plugin-es-x": "^7.8.0",
"get-tsconfig": "^4.8.1", "get-tsconfig": "^4.8.1",
@@ -4623,9 +4625,9 @@
} }
}, },
"node_modules/globals": { "node_modules/globals": {
"version": "16.3.0", "version": "16.4.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-16.3.0.tgz", "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz",
"integrity": "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==", "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -8315,9 +8317,9 @@
} }
}, },
"node_modules/ts-declaration-location/node_modules/picomatch": { "node_modules/ts-declaration-location/node_modules/picomatch": {
"version": "4.0.2", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -8466,15 +8468,16 @@
} }
}, },
"node_modules/typescript-eslint": { "node_modules/typescript-eslint": {
"version": "8.35.0", "version": "8.37.0",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.35.0.tgz", "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.37.0.tgz",
"integrity": "sha512-uEnz70b7kBz6eg/j0Czy6K5NivaYopgxRjsnAJ2Fx5oTLo3wefTHIbL7AkQr1+7tJCRVpTs/wiM8JR/11Loq9A==", "integrity": "sha512-TnbEjzkE9EmcO0Q2zM+GE8NQLItNAJpMmED1BdgoBMYNdqMhzlbqfdSwiRlAzEK2pA9UzVW0gzaaIzXWg2BjfA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/eslint-plugin": "8.35.0", "@typescript-eslint/eslint-plugin": "8.37.0",
"@typescript-eslint/parser": "8.35.0", "@typescript-eslint/parser": "8.37.0",
"@typescript-eslint/utils": "8.35.0" "@typescript-eslint/typescript-estree": "8.37.0",
"@typescript-eslint/utils": "8.37.0"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -8876,43 +8879,43 @@
"license": "ISC" "license": "ISC"
}, },
"node_modules/xo": { "node_modules/xo": {
"version": "1.1.1", "version": "1.2.3",
"resolved": "https://registry.npmjs.org/xo/-/xo-1.1.1.tgz", "resolved": "https://registry.npmjs.org/xo/-/xo-1.2.3.tgz",
"integrity": "sha512-CMuUby6vSP+OQyOQRbhqWWIfc1dg06M6MFdtIQh/EMNE+ONcBzDhzNnbtDt/qD9gGRdg5qZ9YK16BBGwOfA2BQ==", "integrity": "sha512-ykvWr88620CwealQwr7nWcPwolE6RMAVsCSBIdF3JnVdQUBAllnBJypSPsu0YYFzWTrJjQfNgH82lnWMPVTXnA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@eslint-community/eslint-plugin-eslint-comments": "^4.5.0", "@eslint-community/eslint-plugin-eslint-comments": "^4.5.0",
"@sindresorhus/tsconfig": "^7.0.0", "@sindresorhus/tsconfig": "^7.0.0",
"@stylistic/eslint-plugin": "^4.2.0", "@stylistic/eslint-plugin": "^4.2.0",
"@typescript-eslint/parser": "^8.32.1", "@typescript-eslint/parser": "^8.37.0",
"arrify": "^3.0.0", "arrify": "^3.0.0",
"cosmiconfig": "^9.0.0", "cosmiconfig": "^9.0.0",
"define-lazy-prop": "^3.0.0", "define-lazy-prop": "^3.0.0",
"eslint": "^9.27.0", "eslint": "^9.31.0",
"eslint-config-prettier": "^10.1.5", "eslint-config-prettier": "^10.1.5",
"eslint-config-xo-react": "^0.28.0", "eslint-config-xo-react": "^0.28.0",
"eslint-config-xo-typescript": "^7.0.0", "eslint-config-xo-typescript": "^7.0.0",
"eslint-formatter-pretty": "^6.0.1", "eslint-formatter-pretty": "^6.0.1",
"eslint-plugin-ava": "^15.0.1", "eslint-plugin-ava": "^15.0.1",
"eslint-plugin-import-x": "^4.12.2", "eslint-plugin-import-x": "^4.16.1",
"eslint-plugin-n": "^17.18.0", "eslint-plugin-n": "^17.21.0",
"eslint-plugin-no-use-extend-native": "^0.7.2", "eslint-plugin-no-use-extend-native": "^0.7.2",
"eslint-plugin-prettier": "^5.4.0", "eslint-plugin-prettier": "^5.5.1",
"eslint-plugin-promise": "^7.2.1", "eslint-plugin-promise": "^7.2.1",
"eslint-plugin-unicorn": "^59.0.1", "eslint-plugin-unicorn": "^59.0.1",
"find-cache-directory": "^6.0.0", "find-cache-directory": "^6.0.0",
"get-stdin": "^9.0.0", "get-stdin": "^9.0.0",
"get-tsconfig": "^4.10.1", "get-tsconfig": "^4.10.1",
"globals": "^16.1.0", "globals": "^16.3.0",
"globby": "^14.1.0", "globby": "^14.1.0",
"meow": "^13.2.0", "meow": "^13.2.0",
"micromatch": "^4.0.8", "micromatch": "^4.0.8",
"open-editor": "^5.1.0", "open-editor": "^5.1.0",
"path-exists": "^5.0.0", "path-exists": "^5.0.0",
"prettier": "^3.5.3", "prettier": "^3.6.2",
"type-fest": "^4.41.0", "type-fest": "^4.41.0",
"typescript-eslint": "^8.32.1" "typescript-eslint": "^8.37.0"
}, },
"bin": { "bin": {
"xo": "dist/cli.js" "xo": "dist/cli.js"

View File

@@ -53,13 +53,13 @@
}, },
"devDependencies": { "devDependencies": {
"autoprefixer": "^10.4.21", "autoprefixer": "^10.4.21",
"eslint": "^9.31.0", "eslint": "^9.38.0",
"eslint-plugin-compat": "^6.0.2", "eslint-plugin-compat": "^6.0.2",
"globals": "^16.3.0", "globals": "^16.4.0",
"postcss": "^8.5.6", "postcss": "^8.5.6",
"postcss-cli": "^11.0.1", "postcss-cli": "^11.0.1",
"prettier": "^3.6.2", "prettier": "^3.6.2",
"xo": "^1.1.1" "xo": "^1.2.3"
}, },
"browserslist": [ "browserslist": [
">= 0.5%", ">= 0.5%",

View File

@@ -138,7 +138,7 @@ mg.include('scripts/lua/header_authenticated.lp','r')
</div> </div>
<!-- /.box-body --> <!-- /.box-body -->
<div class="box-footer clearfix"> <div class="box-footer clearfix">
<span class="pull-left">* These input fields allow manual input as well. Use <code>*</code> for wildcard search.</span> <span class="pull-left">* These input fields allow manual input as well. Use <code>*</code> for wildcard search. An <code>_</code> can be used as a single-character wildcard. If you want to search for <code>_</code> explicitly, then you must escape it like <code>\_</code></span>
<span class="pull-right">Click "Refresh" below to apply.</span> <span class="pull-right">Click "Refresh" below to apply.</span>
</div> </div>
<!-- /.box --> <!-- /.box -->

View File

@@ -5,7 +5,7 @@
* This file is copyright under the latest version of the EUPL. * This file is copyright under the latest version of the EUPL.
* Please see LICENSE file for your rights under this license. */ * Please see LICENSE file for your rights under this license. */
/* global upstreams:false */ /* global upstreamIPs:false */
"use strict"; "use strict";
@@ -63,6 +63,23 @@ globalThis.htmlLegendPlugin = {
for (const item of items) { for (const item of items) {
const li = document.createElement("li"); const li = document.createElement("li");
// Select the corresponding "slice" of the chart when the mouse is over a legend item
li.addEventListener("mouseover", () => {
chart.setActiveElements([
{
datasetIndex: 0,
index: item.index,
},
]);
chart.update();
});
// Deselect all "slices"
li.addEventListener("mouseout", () => {
chart.setActiveElements([]);
chart.update();
});
// Color checkbox (toggle visibility) // Color checkbox (toggle visibility)
const boxSpan = document.createElement("span"); const boxSpan = document.createElement("span");
boxSpan.title = "Toggle visibility"; boxSpan.title = "Toggle visibility";
@@ -96,9 +113,19 @@ globalThis.htmlLegendPlugin = {
if (isQueryTypeChart) { if (isQueryTypeChart) {
link.href = `queries?type=${item.text}`; link.href = `queries?type=${item.text}`;
} else if (isForwardDestinationChart) { } else {
// Encode the forward destination as it may contain an "#" character // Encode the forward destination as it may contain an "#" character
link.href = `queries?upstream=${encodeURIComponent(upstreams[item.text])}`; link.href = `queries?upstream=${encodeURIComponent(upstreamIPs[item.index])}`;
// If server name and IP are different:
if (item.text !== upstreamIPs[item.index]) {
// replace the title tooltip to include the upstream IP to the text ...
link.title = `List ${item.text} (${upstreamIPs[item.index]}) queries`;
// ... and include the server name (without port) to the querystring, to match
// the text used on the SELECT element (sent by suggestions API endpoint)
link.href += ` (${item.text.split("#")[0]})`;
}
} }
} else { } else {
// no clickable links in other charts // no clickable links in other charts
@@ -233,6 +260,10 @@ function positionTooltip(tooltipEl, tooltip, context) {
const arrowMinIndent = 2 * tooltip.options.cornerRadius; const arrowMinIndent = 2 * tooltip.options.cornerRadius;
const arrowSize = 5; const arrowSize = 5;
// Check if this is a queryOverTimeChart or clientsChart - these should stick to x-axis
const canvasId = context.chart.canvas.id;
const isTimelineChart = canvasId === "queryOverTimeChart" || canvasId === "clientsChart";
let tooltipX = offsetX + caretX; let tooltipX = offsetX + caretX;
let arrowX; let arrowX;
@@ -289,27 +320,37 @@ function positionTooltip(tooltipEl, tooltip, context) {
arrowX = offsetX + caretX - tooltipX; arrowX = offsetX + caretX - tooltipX;
} }
let tooltipY = offsetY + caretY; let tooltipY;
// Compute Y position if (isTimelineChart) {
switch (tooltip.yAlign) { // For timeline charts, always position tooltip below the chart with caret pointing to x-axis
case "top": { const chartArea = context.chart.chartArea;
tooltipY += arrowSize + caretPadding; const canvasBottom = chartArea.bottom;
break; tooltipY = offsetY + canvasBottom + arrowSize + caretPadding;
}
case "center": { // Ensure the arrow points to the correct X position
tooltipY -= tooltipHeight / 2; arrowX = tooltip.caretX - (tooltipX - offsetX);
if (tooltip.xAlign === "left") tooltipX += arrowSize; } else {
if (tooltip.xAlign === "right") tooltipX -= arrowSize; tooltipY = offsetY + caretY;
break; switch (tooltip.yAlign) {
} case "top": {
tooltipY += arrowSize + caretPadding;
break;
}
case "bottom": { case "center": {
tooltipY -= tooltipHeight + arrowSize + caretPadding; tooltipY -= tooltipHeight / 2;
break; if (tooltip.xAlign === "left") tooltipX += arrowSize;
if (tooltip.xAlign === "right") tooltipX -= arrowSize;
break;
}
case "bottom": {
tooltipY -= tooltipHeight + arrowSize + caretPadding;
break;
}
// No default
} }
// No default
} }
// Position tooltip and display // Position tooltip and display

View File

@@ -241,28 +241,26 @@ function updateFtlInfo() {
$("#num_lists").text(intl.format(database.lists)); $("#num_lists").text(intl.format(database.lists));
$("#num_gravity").text(intl.format(database.gravity)); $("#num_gravity").text(intl.format(database.gravity));
$("#num_allowed") $("#num_allowed")
.text(intl.format(database.domains.allowed + database.regex.allowed)) .text(intl.format(database.domains.allowed.enabled + database.regex.allowed.enabled))
.attr( .attr(
"title", "title",
"Allowed: " + "Allowed: " +
intl.format(database.domains.allowed) + intl.format(database.domains.allowed.enabled) +
" exact domains and " + " exact domains and " +
intl.format(database.regex.allowed) + intl.format(database.regex.allowed.enabled) +
" regex filters are enabled" " regex filters are enabled"
); );
$("#num_denied") $("#num_denied")
.text(intl.format(database.domains.denied + database.regex.denied)) .text(intl.format(database.domains.denied.enabled + database.regex.denied.enabled))
.attr( .attr(
"title", "title",
"Denied: " + "Denied: " +
intl.format(database.domains.denied) + intl.format(database.domains.denied.enabled) +
" exact domains and " + " exact domains and " +
intl.format(database.regex.denied) + intl.format(database.regex.denied.enabled) +
" regex filters are enabled" " regex filters are enabled"
); );
updateQueryFrequency(intl, ftl.query_frequency); updateQueryFrequency(intl, ftl.query_frequency);
$("#sysinfo-cpu-ftl").text("(" + ftl["%cpu"].toFixed(1) + "% used by FTL)");
$("#sysinfo-ram-ftl").text("(" + ftl["%mem"].toFixed(1) + "% used by FTL)");
$("#sysinfo-pid-ftl").text(ftl.pid); $("#sysinfo-pid-ftl").text(ftl.pid);
const startdate = moment() const startdate = moment()
.subtract(ftl.uptime, "milliseconds") .subtract(ftl.uptime, "milliseconds")
@@ -346,15 +344,17 @@ function updateSystemInfo() {
); );
$("#cpu").prop( $("#cpu").prop(
"title", "title",
"Load averages for the past 1, 5, and 15 minutes\non a system with " + "CPU usage: " +
system.cpu["%cpu"].toFixed(1) +
"%\nLoad averages for the past 1, 5, and 15 minutes\non a system with " +
system.cpu.nprocs + system.cpu.nprocs +
" core" + " core" +
(system.cpu.nprocs > 1 ? "s" : "") + (system.cpu.nprocs > 1 ? "s" : "") +
" running " + " running " +
system.procs + system.procs +
" processes " + " processes" +
(system.cpu.load.raw[0] > system.cpu.nprocs (system.cpu.load.raw[0] > system.cpu.nprocs
? " (load is higher than the number of cores)" ? "\n(load is higher than the number of cores)"
: "") : "")
); );
$("#sysinfo-cpu").text( $("#sysinfo-cpu").text(
@@ -368,6 +368,9 @@ function updateSystemInfo() {
" processes" " processes"
); );
$("#sysinfo-cpu-ftl").text("(" + system.ftl["%cpu"].toFixed(1) + "% used by FTL)");
$("#sysinfo-ram-ftl").text("(" + system.ftl["%mem"].toFixed(1) + "% used by FTL)");
const startdate = moment() const startdate = moment()
.subtract(system.uptime, "seconds") .subtract(system.uptime, "seconds")
.format("dddd, MMMM Do YYYY, HH:mm:ss"); .format("dddd, MMMM Do YYYY, HH:mm:ss");

View File

@@ -5,7 +5,7 @@
* This file is copyright under the latest version of the EUPL. * This file is copyright under the latest version of the EUPL.
* Please see LICENSE file for your rights under this license. */ * Please see LICENSE file for your rights under this license. */
/* global apiFailure:false */ /* global apiFailure:false, utils:false */
"use strict"; "use strict";
@@ -89,9 +89,11 @@ function parseLines(outputElement, text) {
const lines = text.split(/(?=\r)/g); const lines = text.split(/(?=\r)/g);
for (let line of lines) { for (let line of lines) {
// Escape HTML to prevent XSS attacks (both in adlist URL and non-domain entries)
line = utils.escapeHtml(line);
if (line[0] === "\r") { if (line[0] === "\r") {
// This line starts with the "OVER" sequence. Replace them with "\n" before print // This line starts with the "OVER" sequence. Replace them with "\n" before print
line = line.replaceAll("\r", "\n").replaceAll("\r", "\n"); line = line.replaceAll("\r\u001B[K", "\n").replaceAll("\r", "\n");
// Last line from the textarea will be overwritten, so we remove it // Last line from the textarea will be overwritten, so we remove it
const lastLineIndex = outputElement.innerHTML.lastIndexOf("\n"); const lastLineIndex = outputElement.innerHTML.lastIndexOf("\n");
@@ -138,7 +140,7 @@ function parseLines(outputElement, text) {
}); });
// Append the new text to the end of the output // Append the new text to the end of the output
outputElement.append(line); outputElement.innerHTML += line;
} }
} }

View File

@@ -234,23 +234,20 @@ function initTable() {
if (data.address.startsWith("file://")) { if (data.address.startsWith("file://")) {
// Local files cannot be downloaded from a distant client so don't show // Local files cannot be downloaded from a distant client so don't show
// a link to such a list here // a link to such a list here
$("td:eq(3)", row).html( const codeElem = document.createElement("code");
'<code id="address_' + codeElem.id = "address_" + dataId;
dataId + codeElem.className = "breakall";
'" class="breakall">' + codeElem.textContent = data.address;
utils.escapeHtml(data.address) + $("td:eq(3)", row).empty().append(codeElem);
"</code>"
);
} else { } else {
$("td:eq(3)", row).html( const aElem = document.createElement("a");
'<a id="address_' + aElem.id = "address_" + dataId;
dataId + aElem.className = "breakall";
'" class="breakall" href="' + aElem.href = data.address;
encodeURI(data.address) + aElem.target = "_blank";
'" target="_blank" rel="noopener noreferrer">' + aElem.rel = "noopener noreferrer";
utils.escapeHtml(data.address) + aElem.textContent = data.address;
"</a>" $("td:eq(3)", row).empty().append(aElem);
);
} }
$("td:eq(4)", row).html( $("td:eq(4)", row).html(
@@ -518,12 +515,12 @@ function addList(event) {
} }
$.ajax({ $.ajax({
url: document.body.dataset.apiurl + "/lists", url: document.body.dataset.apiurl + "/lists?type=" + encodeURIComponent(type),
method: "post", method: "post",
dataType: "json", dataType: "json",
processData: false, processData: false,
contentType: "application/json; charset=utf-8", contentType: "application/json; charset=utf-8",
data: JSON.stringify({ address: addresses, comment, type, groups: group }), data: JSON.stringify({ address: addresses, comment, groups: group }),
success(data) { success(data) {
utils.enableAll(); utils.enableAll();
utils.listsAlert(type + "list", addresses, data); utils.listsAlert(type + "list", addresses, data);

View File

@@ -226,7 +226,7 @@ function updateClientsOverTime() {
}); });
} }
const upstreams = {}; const upstreamIPs = [];
function updateForwardDestinationsPie() { function updateForwardDestinationsPie() {
$.getJSON(document.body.dataset.apiurl + "/stats/upstreams", data => { $.getJSON(document.body.dataset.apiurl + "/stats/upstreams", data => {
const v = []; const v = [];
@@ -248,11 +248,8 @@ function updateForwardDestinationsPie() {
label += "#" + item.port; label += "#" + item.port;
} }
// Store upstreams for generating links to the Query Log // Store upstreams IPs for generating links to the Query Log
upstreams[label] = item.ip; upstreamIPs.push(item.port > 0 ? item.ip + "#" + item.port : item.ip);
if (item.port > 0) {
upstreams[label] += "#" + item.port;
}
const percent = (100 * item.count) / sum; const percent = (100 * item.count) / sum;
values.push([label, percent, THEME_COLORS[i++ % THEME_COLORS.length]]); values.push([label, percent, THEME_COLORS[i++ % THEME_COLORS.length]]);
@@ -521,8 +518,8 @@ function labelWithPercentage(tooltipLabel, skipZero = false) {
// Sum all queries for the current time by iterating over all keys in the // Sum all queries for the current time by iterating over all keys in the
// current dataset // current dataset
let sum = 0; let sum = 0;
for (const value of Object.values(tooltipLabel.parsed._stacks.y)) { for (const [key, value] of Object.entries(tooltipLabel.parsed._stacks.y)) {
if (value === undefined) continue; if (key.startsWith("_") || value === undefined) continue;
const num = Number.parseInt(value, 10); const num = Number.parseInt(value, 10);
if (num) sum += num; if (num) sum += num;
} }
@@ -639,9 +636,11 @@ $(() => {
display: false, display: false,
}, },
tooltip: { tooltip: {
enabled: true, // Disable the on-canvas tooltip
enabled: false,
intersect: false, intersect: false,
yAlign: "bottom", external: customTooltips,
yAlign: "top",
itemSort(a, b) { itemSort(a, b) {
return b.datasetIndex - a.datasetIndex; return b.datasetIndex - a.datasetIndex;
}, },
@@ -656,7 +655,7 @@ $(() => {
return "Queries from " + from + " to " + to; return "Queries from " + from + " to " + to;
}, },
label(tooltipLabel) { label(tooltipLabel) {
return labelWithPercentage(tooltipLabel); return labelWithPercentage(tooltipLabel, true);
}, },
}, },
}, },
@@ -893,6 +892,8 @@ $(() => {
elements: { elements: {
arc: { arc: {
borderColor: $(".box").css("background-color"), borderColor: $(".box").css("background-color"),
hoverBorderColor: $(".box").css("background-color"),
hoverOffset: 10,
}, },
}, },
plugins: { plugins: {
@@ -917,6 +918,9 @@ $(() => {
animation: { animation: {
duration: 750, duration: 750,
}, },
layout: {
padding: 10,
},
}, },
}); });
@@ -939,6 +943,8 @@ $(() => {
elements: { elements: {
arc: { arc: {
borderColor: $(".box").css("background-color"), borderColor: $(".box").css("background-color"),
hoverBorderColor: $(".box").css("background-color"),
hoverOffset: 10,
}, },
}, },
plugins: { plugins: {
@@ -963,6 +969,9 @@ $(() => {
animation: { animation: {
duration: 750, duration: 750,
}, },
layout: {
padding: 10,
},
}, },
}); });

View File

@@ -28,6 +28,14 @@ const filters = [
"reply", "reply",
"dnssec", "dnssec",
]; ];
let doDNSSEC = false;
// Check if pihole is validiting DNSSEC
function getDnssecConfig() {
$.getJSON(document.body.dataset.apiurl + "/config/dns/dnssec", data => {
doDNSSEC = data.config.dns.dnssec;
});
}
function initDateRangePicker() { function initDateRangePicker() {
$("#querytime").daterangepicker( $("#querytime").daterangepicker(
@@ -480,6 +488,9 @@ function liveUpdate() {
} }
$(() => { $(() => {
// Do we want to show DNSSEC icons?
getDnssecConfig();
// Do we want to filter queries? // Do we want to filter queries?
const GETDict = utils.parseQueryString(); const GETDict = utils.parseQueryString();
@@ -561,11 +572,13 @@ $(() => {
utils.stateSaveCallback("query_log_table", data); utils.stateSaveCallback("query_log_table", data);
}, },
stateLoadCallback() { stateLoadCallback() {
return utils.stateLoadCallback("query_log_table"); const state = utils.stateLoadCallback("query_log_table");
// Default to 25 entries if "All" was previously selected
if (state) state.length = state.length === -1 ? 25 : state.length;
return state;
}, },
rowCallback(row, data) { rowCallback(row, data) {
const querystatus = parseQueryStatus(data); const querystatus = parseQueryStatus(data);
const dnssec = parseDNSSEC(data);
if (querystatus.icon !== false) { if (querystatus.icon !== false) {
$("td:eq(1)", row).html( $("td:eq(1)", row).html(
@@ -589,14 +602,17 @@ $(() => {
// Prefix colored DNSSEC icon to domain text // Prefix colored DNSSEC icon to domain text
let dnssecIcon = ""; let dnssecIcon = "";
dnssecIcon = if (doDNSSEC === true) {
'<i class="mr-2 fa fa-fw ' + const dnssec = parseDNSSEC(data);
dnssec.icon + dnssecIcon =
" " + '<i class="mr-2 fa fa-fw ' +
dnssec.color + dnssec.icon +
'" title="DNSSEC: ' + " " +
dnssec.text + dnssec.color +
'"></i>'; '" title="DNSSEC: ' +
dnssec.text +
'"></i>';
}
// Escape HTML in domain // Escape HTML in domain
domain = dnssecIcon + utils.escapeHtml(domain); domain = dnssecIcon + utils.escapeHtml(domain);

View File

@@ -361,7 +361,7 @@ $(document).on("focus input", "#StaticDHCPTable td[contenteditable]", function (
if (!row.next().hasClass("edit-hint-row")) { if (!row.next().hasClass("edit-hint-row")) {
row.next(".edit-hint-row").remove(); // Remove any existing hint row.next(".edit-hint-row").remove(); // Remove any existing hint
row.after( row.after(
'<tr class="edit-hint-row"><td colspan="4" class="text-info" style="font-style:italic;">Please save this line before editing another or leaving the page, otherwise your changes will be lost.</td></tr>' '<tr class="edit-hint-row"><td colspan="4" class="text-info" style="font-style:italic;">Please confirm changes using the green button, then click "Save &amp; Apply" before leaving the page.</td></tr>'
); );
} }
}); });
@@ -398,10 +398,10 @@ function renderStaticDHCPTable() {
$("<td></td>") $("<td></td>")
.append( .append(
$( $(
'<button type="button" class="btn btn-success btn-xs save-static-row"><i class="fa fa-fw fa-floppy-disk"></i></button>' '<button type="button" class="btn btn-success btn-xs save-static-row"><i class="fa fa-fw fa-check"></i></button>'
) )
.attr("data-row", idx) .attr("data-row", idx)
.attr("title", "Save changes to this line") .attr("title", "Confirm changes to this line")
.attr("data-toggle", "tooltip") .attr("data-toggle", "tooltip")
) )
.append(" ") .append(" ")
@@ -518,3 +518,17 @@ $(document).on("input blur paste", "#StaticDHCPTable td.static-ipaddr", function
$(this).attr("title", ""); $(this).attr("title", "");
} }
}); });
$(document).on("input blur paste", "#StaticDHCPTable td.static-hostname", function () {
const val = $(this).text().trim();
// Hostnames must not contain spaces, commas, or characters invalid in DNS names
const hostnameValidator = /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?)*$/v;
if (val && !hostnameValidator.test(val)) {
$(this).addClass("table-danger");
$(this).removeClass("table-success");
$(this).attr("title", "Invalid hostname: only letters, digits, hyphens, and dots allowed");
} else {
$(this).removeClass("table-danger table-success");
$(this).attr("title", "");
}
});

View File

@@ -215,7 +215,7 @@ $(() => {
$("#btnAdd-host").on("click", () => { $("#btnAdd-host").on("click", () => {
utils.disableAll(); utils.disableAll();
const elem = $("#Hip").val() + " " + $("#Hdomain").val(); const elem = $("#Hip").val().trim() + " " + $("#Hdomain").val().trim();
const url = document.body.dataset.apiurl + "/config/dns/hosts/" + encodeURIComponent(elem); const url = document.body.dataset.apiurl + "/config/dns/hosts/" + encodeURIComponent(elem);
utils.showAlert("info", "", "Adding DNS record...", elem); utils.showAlert("info", "", "Adding DNS record...", elem);
$.ajax({ $.ajax({
@@ -239,7 +239,7 @@ $(() => {
$("#btnAdd-cname").on("click", () => { $("#btnAdd-cname").on("click", () => {
utils.disableAll(); utils.disableAll();
let elem = $("#Cdomain").val() + "," + $("#Ctarget").val(); let elem = $("#Cdomain").val().trim() + "," + $("#Ctarget").val().trim();
const ttlVal = Number.parseInt($("#Cttl").val(), 10); const ttlVal = Number.parseInt($("#Cttl").val(), 10);
// TODO Fix eslint // TODO Fix eslint
// eslint-disable-next-line unicorn/prefer-number-properties // eslint-disable-next-line unicorn/prefer-number-properties

View File

@@ -267,7 +267,7 @@ $(".confirm-flusharp").confirm({
title: "Confirmation required", title: "Confirmation required",
confirm() { confirm() {
$.ajax({ $.ajax({
url: document.body.dataset.apiurl + "/action/flush/arp", url: document.body.dataset.apiurl + "/action/flush/network",
type: "POST", type: "POST",
}).fail(data => { }).fail(data => {
apiFailure(data); apiFailure(data);

View File

@@ -24,7 +24,7 @@ mg.include('header.lp','r')
<script src="<?=pihole.fileversion('vendor/waitMe-js/modernized-waitme-min.js')?>"></script> <script src="<?=pihole.fileversion('vendor/waitMe-js/modernized-waitme-min.js')?>"></script>
<script src="<?=pihole.fileversion('scripts/js/logout.js')?>"></script> <script src="<?=pihole.fileversion('scripts/js/logout.js')?>"></script>
</head> </head>
<body class="<?=theme.name?> hold-transition sidebar-mini <? if pihole.boxedlayout() then ?>layout-boxed<? end ?> logged-in page-<?=pihole.format_path(mg.request_info.request_uri)?>" data-apiurl="<?=pihole.api_url()?>" data-webhome="<?=webhome?>"> <body class="<?=theme.name?> hold-transition sidebar-mini <? if pihole.boxedlayout() then ?>layout-boxed<? end ?> logged-in page-<?=pihole.format_path(scriptname)?>" data-apiurl="<?=pihole.api_url()?>" data-webhome="<?=webhome?>">
<noscript> <noscript>
<!-- JS Warning --> <!-- JS Warning -->
<div> <div>

View File

@@ -180,7 +180,7 @@ mg.include('scripts/lua/settings_header.lp','r')
<h4 class="modal-title">Enable two-factor authentication</h4> <h4 class="modal-title">Enable two-factor authentication</h4>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<p>Use a phone app like Google Authenticator, 2FA Authenticator, FreeOTP or BitWarden, etc. to get 2FA codes when prompted during login.</p> <p>Use a phone app like Google Authenticator, 2FA Authenticator, FreeOTP or Bitwarden, etc. to get 2FA codes when prompted during login.</p>
<p>1. Scan the QR code below with your app or enter the secret manually.</p> <p>1. Scan the QR code below with your app or enter the secret manually.</p>
<div class="text-center"> <div class="text-center">
<i id="qrcode-spinner" class="fas fa-spinner fa-pulse fa-5x"></i> <i id="qrcode-spinner" class="fas fa-spinner fa-pulse fa-5x"></i>
@@ -192,11 +192,15 @@ mg.include('scripts/lua/settings_header.lp','r')
<p>2. Enter the 2FA code from your app below to confirm that you have set up 2FA correctly.</p> <p>2. Enter the 2FA code from your app below to confirm that you have set up 2FA correctly.</p>
<div class="row"> <div class="row">
<div class="col-md-6 col-md-offset-3"> <div class="col-md-6 col-md-offset-3">
<div id="totp_div" class="has-feedback has-error"> <div id="totp_div">
<div class="pwd-field form-group"> <div class="pwd-field form-group has-error">
<input type="text" size="6" maxlength="6" class="form-control totp_token" id="totp_code" placeholder=""> <div class="input-group">
<input type="text" size="6" maxlength="6" class="form-control totp_token" id="totp_code" placeholder="">
<div class="input-group-addon" data-toggle="tooltip" data-placement="auto" title="TOTP verification code">
<i class="fa fa-clock-rotate-left"></i>
</div>
</div>
</div> </div>
<i class="fa-solid fa-clock-rotate-left pwd-field form-control-feedback"></i>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -59,7 +59,7 @@ mg.include('scripts/lua/settings_header.lp','r')
</div> </div>
<div class="box box-warning settings-level-expert d-none"> <div class="box box-warning settings-level-expert d-none">
<div class="box-header with-border"> <div class="box-header with-border">
<h3 class="box-title" data-configkeys="dns.domain dns.expandHosts">DNS domain settings</h3> <h3 class="box-title" data-configkeys="dns.domain.name dns.expandHosts">DNS domain settings</h3>
</div> </div>
<div class="box-body"> <div class="box-body">
<div class="row"> <div class="row">
@@ -68,7 +68,7 @@ mg.include('scripts/lua/settings_header.lp','r')
<div class="form-group"> <div class="form-group">
<div class="input-group"> <div class="input-group">
<div class="input-group-addon">Domain</div> <div class="input-group-addon">Domain</div>
<input type="text" class="form-control" id="dns.domain" data-key="dns.domain" value=""> <input type="text" class="form-control" id="dns.domain.name" data-key="dns.domain.name" value="">
</div> </div>
</div> </div>
<p>The DNS domains for your Pi-hole. This DNS domain is purely local. FTL may answer queries from its local cache and configuration but *never* forwards any requests upstream *unless* you have configured a dns.revServer exactly for this domain. If no domain is specified and you are using Pi-hole's DHCP server, then any hostnames with a domain part (i.e., with a period) will be disallowed. If a domain is specified, then hostnames with a domain parts matching the domain here are allowed. In addition, when a suffix is set then hostnames without a domain part have the suffix added as an optional domain part.</p> <p>The DNS domains for your Pi-hole. This DNS domain is purely local. FTL may answer queries from its local cache and configuration but *never* forwards any requests upstream *unless* you have configured a dns.revServer exactly for this domain. If no domain is specified and you are using Pi-hole's DHCP server, then any hostnames with a domain part (i.e., with a period) will be disallowed. If a domain is specified, then hostnames with a domain parts matching the domain here are allowed. In addition, when a suffix is set then hostnames without a domain part have the suffix added as an optional domain part.</p>

View File

@@ -44,7 +44,7 @@ mg.include('scripts/lua/settings_header.lp','r')
<th scope="row">Kernel:</th> <th scope="row">Kernel:</th>
<td><span id="sysinfo-kernel"></span></td> <td><span id="sysinfo-kernel"></span></td>
</tr> </tr>
<tr> <tr title="The machine uptime. For containers, this will match the host uptime">
<th scope="row">Uptime:</th> <th scope="row">Uptime:</th>
<td><span id="sysinfo-uptime"></span></td> <td><span id="sysinfo-uptime"></span></td>
</tr> </tr>

View File

@@ -594,12 +594,6 @@ td.details-control {
border: none; border: none;
} }
.form-control-feedback {
right: 0.5em;
width: 16px;
height: 100%;
}
.loginpage-logo { .loginpage-logo {
margin: 0 0 10px; margin: 0 0 10px;
} }
@@ -1650,3 +1644,13 @@ div.dhcp-hosts-wrapper {
background-color: #599900; background-color: #599900;
color: #fff; color: #fff;
} }
/* Used in query log page */
td.dnssec {
padding-inline-start: 2.25em !important;
text-indent: -1.25em;
}
td.dnssec i {
text-indent: 0;
margin-left: -0.5rem;
}