From cc34c22aa51d6f4dd31a34b125396b3d2a816d9a Mon Sep 17 00:00:00 2001 From: Alkim Ake Gozen Date: Thu, 21 May 2026 11:59:09 +0900 Subject: [PATCH] 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 --- .gitignore | 2 + INSTALL.md | 70 ++++++++++++++++++++++++++++++++ README.md | 47 +++++++++++++++++++++ src/widgets/blocky/component.jsx | 46 +++++++++++++++++++++ src/widgets/blocky/proxy.js | 38 +++++++++++++++++ src/widgets/blocky/widget.js | 14 +++++++ translations/en.json | 10 +++++ 7 files changed, 227 insertions(+) create mode 100644 .gitignore create mode 100644 INSTALL.md create mode 100644 README.md create mode 100644 src/widgets/blocky/component.jsx create mode 100644 src/widgets/blocky/proxy.js create mode 100644 src/widgets/blocky/widget.js create mode 100644 translations/en.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2752eb9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +.DS_Store diff --git a/INSTALL.md b/INSTALL.md new file mode 100644 index 0000000..042011d --- /dev/null +++ b/INSTALL.md @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..42dfe23 --- /dev/null +++ b/README.md @@ -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. diff --git a/src/widgets/blocky/component.jsx b/src/widgets/blocky/component.jsx new file mode 100644 index 0000000..d156247 --- /dev/null +++ b/src/widgets/blocky/component.jsx @@ -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 ; + } + + if (!data) { + return ( + + + + + + + ); + } + + return ( + + + + + + + ); +} diff --git a/src/widgets/blocky/proxy.js b/src/widgets/blocky/proxy.js new file mode 100644 index 0000000..8c321c4 --- /dev/null +++ b/src/widgets/blocky/proxy.js @@ -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, + }); +} diff --git a/src/widgets/blocky/widget.js b/src/widgets/blocky/widget.js new file mode 100644 index 0000000..7e48c8a --- /dev/null +++ b/src/widgets/blocky/widget.js @@ -0,0 +1,14 @@ +import blockyProxyHandler from "./proxy"; + +const widget = { + api: "{url}/{endpoint}", + proxyHandler: blockyProxyHandler, + + mappings: { + metrics: { + endpoint: "metrics", + }, + }, +}; + +export default widget; diff --git a/translations/en.json b/translations/en.json new file mode 100644 index 0000000..def4c11 --- /dev/null +++ b/translations/en.json @@ -0,0 +1,10 @@ +{ + "blocky": { + "status": "Status", + "enabled": "Enabled", + "disabled": "Disabled", + "domains": "Blocked Domains", + "queries": "Total Queries", + "cacheHitRate": "Cache Hit Rate" + } +}