feat: add pi-hole-traffic-v6 widget and docs

This commit is contained in:
casmbu 2025-08-02 17:13:39 -04:00
parent f738cd2726
commit 8a0ec73ca7
3 changed files with 164 additions and 1 deletions

View File

@ -1852,7 +1852,7 @@ Shows top queries that were blocked and allowed by [Pi-Hole](https://pi-hole.net
### Pi Hole Queries v6
Shows top queries that were blocked and allowed by [Pi-Hole](https://pi-hole.net/).
Shows top queries that were blocked and allowed by [Pi-Hole](https://pi-hole.net/). Use this version of the widget if you have a v6+ Pi-Hole instance.
<p align="center"><img width="400" src="https://i.ibb.co/pXR0bdQ/pi-hole-queries.png" /></p>
@ -1915,6 +1915,38 @@ Shows number of recent traffic, using allowed and blocked queries from [Pi-Hole]
---
### Pi Hole Recent Traffic v6
Shows number of recent traffic, using allowed and blocked queries from [Pi-Hole](https://pi-hole.net/). Use this version of the widget if you have a v6+ Pi-Hole instance.
<p align="center"><img width="500" src="https://i.ibb.co/7kdxxwx/pi-hole-recent-queries.png" /></p>
#### Options
**Field** | **Type** | **Required** | **Description**
--- | --- | --- | ---
**`hostname`** | `string` | Required | The URL to your Pi-Hole instance
**`apiKey`** | `string` | Required | Your Pi-Hole web password or application password. It **IS** your Pi-Hole admin interface password **UNLESS** you have 2FA turned on (in contrast to the old widget). If you have 2FA turned on you will need to create an application password. Refer to Pi-Hole documentation for how to create an application password.
#### Example
```yaml
- type: pi-hole-traffic-v6
options:
hostname: https://pi-hole.local
apiKey: xxxxxxxxxxxxxxxxxxxxxxx
```
#### Info
- **CORS**: 🟢 Enabled
- **Auth**: 🔴 Required
- **Price**: 🟢 Free
- **Host**: Self-Hosted (see [GitHub - Pi-hole](https://github.com/pi-hole/pi-hole))
- **Privacy**: _See [Pi-Hole Privacy Guide](https://pi-hole.net/privacy/)_
---
### Stat Ping Statuses
Displays the current and recent uptime of your running services, via a self-hosted instance of [StatPing](https://github.com/statping/statping)

View File

@ -0,0 +1,130 @@
<template>
<div :id="chartId" class="pi-hole-traffic"></div>
</template>
<script>
import WidgetMixin from '@/mixins/WidgetMixin';
import ChartingMixin from '@/mixins/ChartingMixin';
export default {
mixins: [WidgetMixin, ChartingMixin],
components: {},
data() {
return {
csrfToken: null,
sid: null,
};
},
computed: {
/* Let user select which comic to display: random, latest or a specific number */
hostname() {
const usersChoice = this.parseAsEnvVar(this.options.hostname);
if (!usersChoice) this.error('You must specify the hostname for your Pi-Hole server');
return usersChoice;
},
apiKey() {
const usersChoice = this.parseAsEnvVar(this.options.apiKey);
if (!usersChoice) this.error('App Password is required, please see the docs');
return usersChoice;
},
authHeader() {
return {
'X-FTL-SID': this.sid,
'X-FTL-CSRF': this.csrfToken,
Accept: 'application/json',
};
},
authEndpoint() {
return `${this.hostname}/api/auth`;
},
historyEndpoint() {
return `${this.hostname}/api/history`;
},
},
methods: {
fetchData() {
this.makeRequest(
this.authEndpoint,
{ 'Content-Type': 'application/json' },
'POST',
{ password: this.apiKey },
)
.then(this.processAuthData)
.then(
() => {
if (!this.sid || !this.csrfToken) return;
this.fetchHistory().then((response) => {
if (this.validate(response)) {
this.processData(response);
}
});
},
);
},
processAuthData({ session }) {
if (!session) {
this.error('Missing session info in auth response');
} else if (session.valid !== true) {
this.error('Authentication failed: Invalid credentials or 2FA token required');
} else {
const { sid, csrf } = session;
if (!sid || !csrf) {
this.error('No CSRF token or SID received');
} else {
this.sid = sid;
this.csrfToken = csrf;
}
}
},
fetchHistory() {
return this.makeRequest(this.historyEndpoint, this.authHeader);
},
validate(data) {
if (!data || !Array.isArray(data['history'])) {
this.error('Got success, but found no results, possible authorization error');
} else if (data.history.length < 1) {
this.error('Request completed succesfully, but no data in Pi-Hole yet');
return false;
}
return true;
},
processData({ history }) {
const timeData = [];
const domainsData = [];
const adsData = [];
history.forEach(({ timestamp, total, blocked }) => {
timeData.push(this.formatTime(timestamp * 1000));
domainsData.push(total - blocked);
adsData.push(blocked);
});
const chartData = {
labels: timeData,
datasets: [
{ name: 'Queries', type: 'bar', values: domainsData },
{ name: 'Ads Blocked', type: 'bar', values: adsData },
],
};
this.generateChart(chartData);
},
generateChart(chartData) {
return new this.Chart(`#${this.chartId}`, {
title: 'Recent Queries & Ads',
data: chartData,
type: 'axis-mixed',
height: this.chartHeight,
colors: ['#20e253', '#f80363'],
truncateLegends: true,
lineOptions: {
regionFill: 1,
hideDots: 1,
},
axisOptions: {
xIsSeries: true,
xAxisMode: 'tick',
},
});
},
},
};
</script>

View File

@ -107,6 +107,7 @@ const COMPAT = {
'pi-hole-top-queries': 'PiHoleTopQueries',
'pi-hole-top-queries-v6': 'PiHoleTopQueriesV6',
'pi-hole-traffic': 'PiHoleTraffic',
'pi-hole-traffic-v6': 'PiHoleTrafficV6',
'proxmox-lists': 'Proxmox',
'public-holidays': 'PublicHolidays',
'public-ip': 'PublicIp',