🔀 Merge pull request #1892 from casmbu/master

Add v6 versions of Pi-Hole widgets and fix minor bug in Uptime Kuma widget
This commit is contained in:
Alicia Sykes 2025-08-30 21:02:11 +01:00 committed by GitHub
commit c1f27c64b6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 670 additions and 15 deletions

View file

@ -26,7 +26,7 @@
- **Getting Started** - **Getting Started**
- [🌈 Features](#features-) - [🌈 Features](#features-)
- [Demo](#demo-) - [ Demo](#demo-)
- [🚀 Getting Started](#getting-started-) - [🚀 Getting Started](#getting-started-)
- [🔧 Configuring](#configuring-) - [🔧 Configuring](#configuring-)
- **Feature Overview** - **Feature Overview**

View file

@ -47,9 +47,12 @@ Dashy has support for displaying dynamic content in the form of widgets. There a
- [CPU History](#cpu-history-netdata) - [CPU History](#cpu-history-netdata)
- [Memory History](#memory-history-netdata) - [Memory History](#memory-history-netdata)
- [System Load History](#load-history-netdata) - [System Load History](#load-history-netdata)
- [Pi Hole Stats](#pi-hole-stats) - [Pi-Hole Stats](#pi-hole-stats)
- [Pi Hole Queries](#pi-hole-queries) - [Pi-Hole Stats v6](#pi-hole-stats-v6)
- [Pi Hole Recent Traffic](#pi-hole-recent-traffic) - [Pi-Hole Queries](#pi-hole-queries)
- [Pi-Hole Queries v6](#pi-hole-queries-v6)
- [Pi-Hole Recent Traffic](#pi-hole-recent-traffic)
- [Pi-Hole Recent Traffic v6](#pi-hole-recent-traffic-v6)
- [Stat Ping Statuses](#stat-ping-statuses) - [Stat Ping Statuses](#stat-ping-statuses)
- [Synology Download Station](#synology-download-station) - [Synology Download Station](#synology-download-station)
- [AdGuard Home Block Stats](#adguard-home-block-stats) - [AdGuard Home Block Stats](#adguard-home-block-stats)
@ -1722,7 +1725,7 @@ Pull recent load usage in 1, 5 and 15 minute intervals, from NetData.
--- ---
### Pi Hole Stats ### Pi-Hole Stats
Displays the number of queries blocked by [Pi-Hole](https://pi-hole.net/). Displays the number of queries blocked by [Pi-Hole](https://pi-hole.net/).
@ -1763,12 +1766,58 @@ Displays the number of queries blocked by [Pi-Hole](https://pi-hole.net/).
- **CORS**: 🟢 Enabled - **CORS**: 🟢 Enabled
- **Auth**: 🔴 Required - **Auth**: 🔴 Required
- **Price**: 🟢 Free - **Price**: 🟢 Free
- **Host**: Self-Hosted (see [GitHub - Pi-hole](https://github.com/pi-hole/pi-hole)) - **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/)_ - **Privacy**: _See [Pi-Hole Privacy Guide](https://pi-hole.net/privacy/)_
--- ---
### Pi Hole Queries ### Pi-Hole Stats v6
Displays the number of queries blocked 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/zftCLJN/pi-hole-stats.png" /></p>
#### Options
**Field** | **Type** | **Required** | **Description**
--- | --- | --- | ---
**`hostname`** | `string` | Required | The URL to your Pi-Hole instance
**`hideStatus`** / **`hideChart`** / **`hideInfo`** | `boolean` | _Optional_ | Optionally hide any of the three parts of the widget
**`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-stats-v6
options:
hostname: http://192.168.130.1
apiKey: xxxxxxxxxxxxxxxxxxxxxxx
```
> [!TIP]
> In order to avoid leaking secret data, both `hostname` and `apiKey` can leverage environment variables. Simply pass the name of the variable, which MUST start with `VUE_APP_`.
```yaml
- type: pi-hole-stats-v6
options:
hostname: VUE_APP_pihole_ip
apiKey: VUE_APP_pihole_key
```
> [!IMPORTANT]
> You will need to restart the server (or the docker image) if adding/editing an env var for this to be refreshed.
#### 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/)_
---
### Pi-Hole Queries
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/).
@ -1801,7 +1850,40 @@ Shows top queries that were blocked and allowed by [Pi-Hole](https://pi-hole.net
--- ---
### Pi Hole Recent Traffic ### Pi-Hole Queries v6
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>
#### 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.
**`count`** | `number` | _Optional_ | The number of queries to display. Defaults to `10`
#### Example
```yaml
- type: pi-hole-top-queries-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/)_
---
### Pi-Hole Recent Traffic
Shows number of recent traffic, using allowed and blocked queries from [Pi-Hole](https://pi-hole.net/) Shows number of recent traffic, using allowed and blocked queries from [Pi-Hole](https://pi-hole.net/)
@ -1833,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 ### 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) Displays the current and recent uptime of your running services, via a self-hosted instance of [StatPing](https://github.com/statping/statping)

View file

@ -38,7 +38,7 @@ export default {
hostname() { hostname() {
const usersChoice = this.parseAsEnvVar(this.options.hostname); const usersChoice = this.parseAsEnvVar(this.options.hostname);
if (!usersChoice) this.error('You must specify the hostname for your Pi-Hole server'); if (!usersChoice) this.error('You must specify the hostname for your Pi-Hole server');
return usersChoice || 'http://pi.hole'; return usersChoice;
}, },
apiKey() { apiKey() {
const usersChoice = this.parseAsEnvVar(this.options.apiKey); const usersChoice = this.parseAsEnvVar(this.options.apiKey);
@ -53,9 +53,7 @@ export default {
hideInfo() { return this.options.hideInfo; }, hideInfo() { return this.options.hideInfo; },
}, },
filters: { filters: {
capitalize(str) { capitalize,
return capitalize(str);
},
}, },
methods: { methods: {
/* Make GET request to local pi-hole instance */ /* Make GET request to local pi-hole instance */

View file

@ -0,0 +1,252 @@
<template>
<div class="pi-hole-stats-v6-wrapper">
<!-- Current Status -->
<div v-if="status && !hideStatus" class="status">
<span class="status-lbl">{{ $t('widgets.pi-hole.status-heading') }}:</span>
<span :class="`status-val ${getStatusColor(status)}`">{{ status | capitalize }}</span>
</div>
<!-- Block Pie Chart -->
<p :id="chartId" class="block-pie"></p>
<!-- More Data -->
<div v-if="dataTable" class="data-table">
<div class="data-table-row" v-for="(row, inx) in dataTable" :key="inx">
<p class="row-label">{{ row.lbl }}</p>
<p class="row-value">{{ row.val }}</p>
</div>
</div>
</div>
</template>
<script>
import WidgetMixin from '@/mixins/WidgetMixin';
import ChartingMixin from '@/mixins/ChartingMixin';
import { capitalize } from '@/utils/MiscHelpers';
export default {
mixins: [WidgetMixin, ChartingMixin],
data() {
return {
status: null,
dataTable: null,
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;
},
hideStatus() { return this.options.hideStatus; },
hideChart() { return this.options.hideChart; },
hideInfo() { return this.options.hideInfo; },
authHeader() {
return {
'X-FTL-SID': this.sid,
'X-FTL-CSRF': this.csrfToken,
Accept: 'application/json',
};
},
authEndpoint() {
return `${this.hostname}/api/auth`;
},
blockingStatusEndpoint() {
return `${this.hostname}/api/dns/blocking`;
},
/* This is actually just the stats that are currently in memory, which amounts to 24hrs when the
service first boots up, but will drift to be a little more than 24hrs worth of data as the
server runs. If you need accurate stats to a particular timeframe, then the
/api/stats/database/summary endpoint is the way to go. */
statsEndpoint() {
return `${this.hostname}/api/stats/summary`;
},
statsDatabaseEndpoint() {
return `${this.hostname}/api/stats/database/summary`;
},
timestampTomorrowMidnight() {
const calcDate = new Date();
calcDate.setHours(0, 0, 0, 0);
calcDate.setDate(calcDate.getDate() + 1);
return parseInt(
String(calcDate.getTime()).substring(0, String(calcDate.getTime()).length - 3),
10,
);
},
timestamp24HoursAgo() {
const calcDate = new Date();
calcDate.setDate(calcDate.getDate() - 1);
return parseInt(
String(calcDate.getTime()).substring(0, String(calcDate.getTime()).length - 3),
10,
);
},
},
filters: {
capitalize,
},
methods: {
fetchData() {
this.makeRequest(
this.authEndpoint,
{ 'Content-Type': 'application/json' },
'POST',
{ password: this.apiKey },
)
.then(this.processAuthData)
.then(
() => {
if (!this.sid || !this.csrfToken) return;
Promise.all([
this.fetchBlockingStatus(),
this.fetchInMemoryStats(),
this.fetchTodayStats(),
this.fetchAllTimeStats(),
]).then(this.processData);
},
);
},
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;
}
}
},
fetchBlockingStatus() {
return this.makeRequest(this.blockingStatusEndpoint, this.authHeader);
},
fetchInMemoryStats() {
return this.makeRequest(this.statsEndpoint, this.authHeader);
},
fetchTodayStats() {
const url = new URL(this.statsDatabaseEndpoint);
url.searchParams.append('from', this.timestamp24HoursAgo);
// Future date because we're looking for "up to present".
url.searchParams.append('until', this.timestampTomorrowMidnight);
return this.makeRequest(url.toString(), this.authHeader);
},
fetchAllTimeStats() {
const url = new URL(this.statsDatabaseEndpoint);
url.searchParams.append('from', 1); // Errors out with 0.
url.searchParams.append('until', this.timestampTomorrowMidnight);
return this.makeRequest(url.toString(), this.authHeader);
},
processData([blockingStatus, inMemoryStats, todayStats, allTimeStats]) {
if (!this.hideStatus) {
this.status = blockingStatus.blocking || 'unknown';
}
if (!this.hideInfo) {
this.dataTable = [
{
lbl: 'Active Clients',
val: `${inMemoryStats.clients.active.toLocaleString('en-US')}/${allTimeStats.total_clients.toLocaleString('en-US')}`,
},
{ lbl: 'Ads Blocked Last 24 Hours', val: todayStats.sum_blocked.toLocaleString('en-US') },
{ lbl: 'DNS Queries Last 24 Hours', val: todayStats.sum_queries.toLocaleString('en-US') },
{ lbl: 'Total DNS Queries', val: allTimeStats.sum_queries.toLocaleString('en-US') },
{
lbl: 'Domains on Block List',
val: inMemoryStats.gravity.domains_being_blocked.toLocaleString('en-US'),
},
];
}
if (!this.hideChart) {
this.generateBlockPie(
Math.round(todayStats.percent_blocked * 10) / 10,
);
}
},
getStatusColor(status) {
if (status === 'enabled') return 'green';
if (status === 'disabled') return 'red';
return 'blue';
},
/* Generate pie chart showing the proportion of queries blocked */
generateBlockPie(blockedTodayPercentage) {
const chartData = {
labels: ['Blocked', 'Allowed'],
datasets: [{
values: [blockedTodayPercentage, 100 - blockedTodayPercentage],
}],
};
return new this.Chart(`#${this.chartId}`, {
title: 'Block Percent Last 24 Hours',
data: chartData,
type: 'donut',
height: 250,
strokeWidth: 18,
colors: ['#f80363', '#20e253'],
tooltipOptions: {
formatTooltipY: d => `${Math.round(d * 10) / 10}%`,
},
});
},
},
};
</script>
<style scoped lang="scss">
.pi-hole-stats-v6-wrapper {
display: flex;
flex-direction: column;
.status {
margin: 0.5rem 0;
.status-lbl {
color: var(--widget-text-color);
font-weight: bold;
}
.status-val {
margin-left: 0.5rem;
font-family: var(--font-monospace);
&.green { color: var(--success); }
&.red { color: var(--danger); }
&.blue { color: var(--info); }
}
}
img.block-percent-chart {
margin: 0.5rem auto;
max-width: 8rem;
width: 100%;
}
.block-pie {
margin: 0;
}
.data-table {
display: flex;
flex-direction: column;
.data-table-row {
display: flex;
justify-content: space-between;
p {
margin: 0.2rem 0;
color: var(--widget-text-color);
font-size: 0.9rem;
&.row-value {
font-family: var(--font-monospace);
}
}
&:not(:last-child) {
border-bottom: 1px dashed var(--widget-text-color);
}
}
}
}
</style>

View file

@ -103,5 +103,4 @@ export default {
} }
} }
} }
</style> </style>

View file

@ -0,0 +1,159 @@
<template>
<div class="pi-hole-queries-wrapper" v-if="results">
<div v-for="section in results" :key="section.id" class="query-section">
<p class="section-title">{{ section.title }}</p>
<div v-for="(query, i) in section.results" :key="i" class="query-row">
<p class="domain">{{ query.domain }}</p>
<p class="count">{{ query.count }}</p>
</div>
</div>
</div>
</template>
<script>
import WidgetMixin from '@/mixins/WidgetMixin';
import { showNumAsThousand } from '@/utils/MiscHelpers';
export default {
mixins: [WidgetMixin],
components: {},
data() {
return {
results: null,
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;
},
count() {
const usersChoice = this.options.count;
if (usersChoice && typeof usersChoice === 'number') return usersChoice;
return 10;
},
authHeader() {
return {
'X-FTL-SID': this.sid,
'X-FTL-CSRF': this.csrfToken,
Accept: 'application/json',
};
},
authEndpoint() {
return `${this.hostname}/api/auth`;
},
/* This is actually just the stats that are shown on the Pi-Hole dashboard, which amounts to
24hrs when the service first boots up, but will drift to be a little more than 24hrs worth of
data as the server runs. If you need accurate stats to a particular timeframe, then the
/api/stats/database/top_domains endpoint is the way to go. However, that endpoint does not
return sorted results, so you would have to get everything and sort it yourself, which presents
logistical problems. */
topDomainsEndpoint() {
return `${this.hostname}/api/stats/top_domains`;
},
},
methods: {
fetchData() {
this.makeRequest(
this.authEndpoint,
{ 'Content-Type': 'application/json' },
'POST',
{ password: this.apiKey },
)
.then(this.processAuthData)
.then(
() => {
if (!this.sid || !this.csrfToken) return;
Promise.all([
this.fetchTopAllowedDomains(),
this.fetchTopBlockedDomains(),
]).then(this.processData);
},
);
},
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;
}
}
},
fetchTopAllowedDomains() {
const url = new URL(this.topDomainsEndpoint);
url.searchParams.append('blocked', false);
url.searchParams.append('count', this.count);
return this.makeRequest(url.toString(), this.authHeader);
},
fetchTopBlockedDomains() {
const url = new URL(this.topDomainsEndpoint);
url.searchParams.append('blocked', true);
url.searchParams.append('count', this.count);
return this.makeRequest(url.toString(), this.authHeader);
},
processData([topAllowedDomains, topBlockedDomains]) {
const topAds = [];
topBlockedDomains.domains.forEach(({ domain, count }) => {
topAds.push({ domain, count: showNumAsThousand(count) });
});
const topQueries = [];
topAllowedDomains.domains.forEach(({ domain, count }) => {
topQueries.push({ domain, count: showNumAsThousand(count) });
});
this.results = [
{ id: '01', title: 'Top Ads Blocked', results: topAds },
{ id: '02', title: 'Top Queries', results: topQueries },
];
},
},
};
</script>
<style scoped lang="scss">
.pi-hole-queries-wrapper {
color: var(--widget-text-color);
.query-section {
display: inline-block;
width: 100%;
p.section-title {
margin: 0.75rem 0 0.25rem;
font-size: 1.2rem;
font-weight: bold;
}
.query-row {
display: flex;
justify-content: space-between;
margin: 0.25rem;
p.domain {
margin: 0.25rem 0;
overflow: hidden;
text-overflow: ellipsis;
}
p.count {
margin: 0.25rem 0;
font-family: var(--font-monospace);
}
&:not(:last-child) {
border-bottom: 1px dashed var(--widget-text-color);
}
}
}
}
</style>

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

@ -60,10 +60,10 @@ export default {
}, },
/* Create authorisation header for the instance from the apiKey */ /* Create authorisation header for the instance from the apiKey */
authHeaders() { authHeaders() {
if (!this.options.apiKey) { if (!this.apiKey) {
return {}; return {};
} }
const encoded = window.btoa(`:${this.options.apiKey}`); const encoded = window.btoa(`:${this.apiKey}`);
return { Authorization: `Basic ${encoded}` }; return { Authorization: `Basic ${encoded}` };
}, },
}, },

View file

@ -103,8 +103,11 @@ const COMPAT = {
'nextcloud-user': 'NextcloudUser', 'nextcloud-user': 'NextcloudUser',
'nextcloud-user-status': 'NextcloudUserStatus', 'nextcloud-user-status': 'NextcloudUserStatus',
'pi-hole-stats': 'PiHoleStats', 'pi-hole-stats': 'PiHoleStats',
'pi-hole-stats-v6': 'PiHoleStatsV6',
'pi-hole-top-queries': 'PiHoleTopQueries', 'pi-hole-top-queries': 'PiHoleTopQueries',
'pi-hole-top-queries-v6': 'PiHoleTopQueriesV6',
'pi-hole-traffic': 'PiHoleTraffic', 'pi-hole-traffic': 'PiHoleTraffic',
'pi-hole-traffic-v6': 'PiHoleTrafficV6',
'proxmox-lists': 'Proxmox', 'proxmox-lists': 'Proxmox',
'public-holidays': 'PublicHolidays', 'public-holidays': 'PublicHolidays',
'public-ip': 'PublicIp', 'public-ip': 'PublicIp',