feat: initial Blocky widget for Homepage dashboard
Parses Blocky's /metrics (Prometheus text) endpoint to show: - Blocking status (Enabled/Disabled) - Blocked domains count (denylist entries) - Total queries served - Cache hit rate % No REST stats endpoint exists in Blocky, so the proxy handler fetches and parses the Prometheus text format directly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
commit
cc34c22aa5
7 changed files with 227 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
node_modules/
|
||||
.DS_Store
|
||||
70
INSTALL.md
Normal file
70
INSTALL.md
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
# Installation
|
||||
|
||||
These files integrate as a standard service widget into the [Homepage](https://gethomepage.dev) dashboard source.
|
||||
Homepage has no plugin system — you need to patch the source and rebuild.
|
||||
|
||||
## 1. Copy widget files
|
||||
|
||||
```bash
|
||||
cp -r src/widgets/blocky /path/to/homepage/src/widgets/
|
||||
```
|
||||
|
||||
## 2. Register the widget
|
||||
|
||||
**`src/widgets/widgets.js`** — add import and export:
|
||||
```js
|
||||
import blocky from "./blocky/widget";
|
||||
// ... existing imports ...
|
||||
|
||||
export default {
|
||||
blocky,
|
||||
// ... existing widgets ...
|
||||
};
|
||||
```
|
||||
|
||||
**`src/widgets/components.js`** — add dynamic import:
|
||||
```js
|
||||
const components = {
|
||||
blocky: dynamic(() => import("./blocky/component")),
|
||||
// ... existing components ...
|
||||
};
|
||||
```
|
||||
|
||||
## 3. Add translations
|
||||
|
||||
In `public/locales/en/common.json`, merge the contents of `translations/en.json`
|
||||
into the top-level object. Repeat for any other locale files you use.
|
||||
|
||||
## 4. Homepage config
|
||||
|
||||
```yaml
|
||||
# services.yaml
|
||||
- Infrastructure:
|
||||
- Pi DNS:
|
||||
href: http://10.0.50.5:4000
|
||||
description: DNS ad-blocker
|
||||
icon: blocky.png
|
||||
widget:
|
||||
type: blocky
|
||||
url: http://10.0.50.5:4000
|
||||
```
|
||||
|
||||
## What the widget shows
|
||||
|
||||
| Field | Source metric |
|
||||
|---|---|
|
||||
| Status | `blocky_blocking_enabled` (Enabled / Disabled) |
|
||||
| Blocked Domains | `blocky_denylist_cache_entries{group="ads"}` |
|
||||
| Total Queries | `blocky_cache_hits_total` + `blocky_cache_misses_total` |
|
||||
| Cache Hit Rate | hits / (hits + misses) × 100 |
|
||||
|
||||
Data is pulled from Blocky's Prometheus `/metrics` endpoint — the only source
|
||||
that exposes query counts. The REST `/api/blocking/status` endpoint only returns
|
||||
the blocking toggle state.
|
||||
|
||||
## Upstream
|
||||
|
||||
There is an open feature request at
|
||||
https://github.com/gethomepage/homepage/discussions/2732 (currently 8 upvotes —
|
||||
Homepage requires 20 before considering a PR). Once it crosses that threshold,
|
||||
this widget can be submitted as a PR to the official repo.
|
||||
47
README.md
Normal file
47
README.md
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
# homepage-blocky-widget
|
||||
|
||||
A [Blocky](https://0xerr0r.github.io/blocky/) service widget for the
|
||||
[Homepage](https://gethomepage.dev) dashboard.
|
||||
|
||||
Blocky is a DNS ad-blocker and resolver. This widget shows live stats pulled
|
||||
from Blocky's Prometheus `/metrics` endpoint.
|
||||
|
||||
## Preview
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Pi DNS │
|
||||
│ DNS ad-blocker │
|
||||
│ │
|
||||
│ Status Enabled │
|
||||
│ Blocked Domains 84,242 │
|
||||
│ Total Queries 333 │
|
||||
│ Cache Hit Rate 10.5% │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
- Blocky with `ports.http` configured (default: 4000)
|
||||
- Prometheus metrics enabled in Blocky config:
|
||||
```yaml
|
||||
prometheus:
|
||||
enable: true
|
||||
path: /metrics
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
See [INSTALL.md](INSTALL.md).
|
||||
|
||||
## Why a custom widget?
|
||||
|
||||
Blocky has no REST endpoint for query statistics — only `/api/blocking/status`
|
||||
(toggle state) and `/metrics` (Prometheus text format). This widget includes a
|
||||
small proxy handler that fetches and parses the metrics endpoint, returning the
|
||||
relevant values as JSON to the React component.
|
||||
|
||||
There is an open feature request at the official Homepage repo:
|
||||
https://github.com/gethomepage/homepage/discussions/2732
|
||||
|
||||
Once it reaches the required 20 upvotes, this widget can be submitted upstream.
|
||||
46
src/widgets/blocky/component.jsx
Normal file
46
src/widgets/blocky/component.jsx
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import { useTranslation } from "next-i18next";
|
||||
import Block from "components/services/widget/block";
|
||||
import Container from "components/services/widget/container";
|
||||
import useWidgetAPI from "utils/proxy/use-widget-api";
|
||||
|
||||
export default function Component({ service }) {
|
||||
const { t } = useTranslation();
|
||||
const { widget } = service;
|
||||
const { data, error } = useWidgetAPI(widget, "metrics");
|
||||
|
||||
if (error) {
|
||||
return <Container service={service} error={error} />;
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="blocky.status" />
|
||||
<Block label="blocky.domains" />
|
||||
<Block label="blocky.queries" />
|
||||
<Block label="blocky.cacheHitRate" />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block
|
||||
label="blocky.status"
|
||||
value={data.enabled ? t("blocky.enabled") : t("blocky.disabled")}
|
||||
/>
|
||||
<Block
|
||||
label="blocky.domains"
|
||||
value={t("common.number", { value: data.denylistEntries })}
|
||||
/>
|
||||
<Block
|
||||
label="blocky.queries"
|
||||
value={t("common.number", { value: data.totalQueries })}
|
||||
/>
|
||||
<Block
|
||||
label="blocky.cacheHitRate"
|
||||
value={`${data.cacheHitRate.toFixed(1)}%`}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
38
src/widgets/blocky/proxy.js
Normal file
38
src/widgets/blocky/proxy.js
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import { httpProxy } from "utils/proxy/http";
|
||||
import getServiceWidget from "utils/service-helpers";
|
||||
import { formatApiCall } from "utils/proxy/api-helpers";
|
||||
import createLogger from "utils/logger";
|
||||
|
||||
const logger = createLogger("blocky");
|
||||
|
||||
function parseMetric(text, name) {
|
||||
// Matches bare metric or metric with labels: my_metric{label="x"} 1.0
|
||||
const re = new RegExp(`^${name}(?:{[^}]*})?\\s+([0-9.e+\\-]+)`, "m");
|
||||
const m = text.match(re);
|
||||
return m ? parseFloat(m[1]) : 0;
|
||||
}
|
||||
|
||||
export default async function blockyProxyHandler(req, res) {
|
||||
const { group, service } = req.query;
|
||||
const widget = await getServiceWidget(group, service);
|
||||
const url = formatApiCall("{url}/metrics", widget);
|
||||
|
||||
const [status, , data] = await httpProxy(url);
|
||||
|
||||
if (status !== 200) {
|
||||
logger.error("HTTP %d fetching Blocky metrics from %s", status, url);
|
||||
return res.status(status).json({ error: `HTTP ${status}` });
|
||||
}
|
||||
|
||||
const text = data.toString();
|
||||
const hits = parseMetric(text, "blocky_cache_hits_total");
|
||||
const misses = parseMetric(text, "blocky_cache_misses_total");
|
||||
const total = hits + misses;
|
||||
|
||||
return res.json({
|
||||
enabled: parseMetric(text, "blocky_blocking_enabled") === 1,
|
||||
denylistEntries: parseMetric(text, "blocky_denylist_cache_entries"),
|
||||
totalQueries: total,
|
||||
cacheHitRate: total > 0 ? (hits / total) * 100 : 0,
|
||||
});
|
||||
}
|
||||
14
src/widgets/blocky/widget.js
Normal file
14
src/widgets/blocky/widget.js
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import blockyProxyHandler from "./proxy";
|
||||
|
||||
const widget = {
|
||||
api: "{url}/{endpoint}",
|
||||
proxyHandler: blockyProxyHandler,
|
||||
|
||||
mappings: {
|
||||
metrics: {
|
||||
endpoint: "metrics",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default widget;
|
||||
10
translations/en.json
Normal file
10
translations/en.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"blocky": {
|
||||
"status": "Status",
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled",
|
||||
"domains": "Blocked Domains",
|
||||
"queries": "Total Queries",
|
||||
"cacheHitRate": "Cache Hit Rate"
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue