feat: add Gluetun integration with VPN status widget#5812
Conversation
Adds a Gluetun integration and accompanying dashboard widget surfacing VPN connection details from a running gluetun container. - GluetunIntegration calls the /v1/vpn/status, /v1/dns/status, /v1/publicip/ip and /v1/vpn/settings control endpoints in parallel and maps the results into a flat GluetunStatusInfo - Auth supports either an X-API-Key header or Basic auth from username/password secrets - Cached request handler with a 30s TTL feeds the widget through the gluetun tRPC router - Widget renders a single card for one VPN or a scroll list for many, with VPN/DNS status, public IP, city/country and provider/protocol - Tests: integration class (mocked HTTP), zod schema parsing, widget status-color helper Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
🚨 Preview Deployment Blocked - Security ProtectionYour pull request was blocked from triggering preview deployments Why was this blocked?
How to resolve this:Option 1: Get Collaborator Access (Recommended) Option 2: Request Permission Override For Repository Administrators:To disable this security check ( This security measure protects against malicious code execution in preview deployments. Only trusted collaborators should have the ability to trigger deployments. 🛡️ Learn more about this security featureThis protection prevents unauthorized users from:
Preview deployments are powerful but require trust. Only users with repository write access can trigger them. |
| getVpnInfo: publicProcedure.concat(createManyIntegrationMiddleware("query", "gluetun")).query(async ({ ctx }) => { | ||
| const results = await Promise.all( | ||
| ctx.integrations.map(async (integration) => { | ||
| const innerHandler = gluetunVPNStatusHandler.handler(integration, {}); | ||
| const result = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false }); | ||
| return result.data; | ||
| }), | ||
| ); | ||
|
|
||
| return results; | ||
| }), |
There was a problem hiding this comment.
This works when you refresh the page, but if the VPN goes down while you are on the page, the status will not be updated. Can you check the other integrations and widgets for example implementations on subscriptions?
| const [vpnStatus, dnsStatus, publicIp, vpnSettings] = await Promise.all([ | ||
| this.getVpnStatusAsync(authHeaders), | ||
| this.getDnsStatusAsync(authHeaders), | ||
| this.getPublicIpAsync(authHeaders), | ||
| this.getVpnSettingAsync(authHeaders), | ||
| ]); |
There was a problem hiding this comment.
This works, but is there no endpoint to get all data at once? If there is not, you can leave it as is.
There was a problem hiding this comment.
@manuel-rw we can't get all the details from one URL that's why it's like this. here is the documentation.
| const integrationInstance = await createIntegrationAsync(integration); | ||
| return await integrationInstance.getVpnDetailsAsync(); | ||
| }, | ||
| cacheDuration: dayjs.duration(30, "seconds"), |
There was a problem hiding this comment.
This cache duration is quite aggressive. I think 5 minutes would be fine.
|
|
||
| if (integrations.length === 0) { | ||
| return <EmptyState />; | ||
| } |
There was a problem hiding this comment.
Check the other widgets, you can throw an exception in this case.
There was a problem hiding this comment.
Remove this check as it can never happen due to an exception that is thrown in item-content.tsx
| if (integrations.length === 1) { | ||
| const [vpn] = integrations; | ||
|
|
||
| if (!vpn) return <EmptyState />; |
There was a problem hiding this comment.
The exception is thrown automatically, so no need to handle this. Just use something like
// It will always have at least one integration as otherwise the NoIntegrationSelectedError would be thrown in item-content.tsx
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const integration = integrations[0]!;
or similar
|
Just to be sure; this IP you posted isn't yours, correct? |
yeah not mine, just a placeholder for screnshot |
| iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/gluetun.svg", | ||
| category: ["gluetun"], | ||
| defaultUrl: "http://localhost:8001", | ||
| documentationUrl: createDocumentationLink("/docs/integrations/umami"), |
There was a problem hiding this comment.
Update documentation url and add @ts-expect-error until docs have been created and merged
| secretKinds: [["username", "password"], ["apiKey"]], | ||
| iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/gluetun.svg", | ||
| category: ["gluetun"], | ||
| defaultUrl: "http://localhost:8001", |
There was a problem hiding this comment.
We haven't specified defaultUrl for other self hosted integrations, therefore I would suggest to remove it here
| "tracearr", | ||
| "speedtestTracker", | ||
| "umami", | ||
| "gluetun", |
There was a problem hiding this comment.
Maybe it makes more sense to have a vpn widget? What do you guys think?
There was a problem hiding this comment.
I can work on that, but what else than gluetun would be a candidate?
There was a problem hiding this comment.
Not sure what's your opinion on this @manuel-rw, but I think the value of this test is very limited. Not sure if we can add e2e tests instead that test with a running docker container?
| export const gluetunPublicIpSchema = z.object({ | ||
| public_ip: z.string(), | ||
| region: z.string(), | ||
| country: z.string(), | ||
| city: z.string(), | ||
| location: z.string(), | ||
| organization: z.string(), | ||
| postal_code: z.string(), | ||
| timezone: z.string(), | ||
| }); |
There was a problem hiding this comment.
Where did you get this schema from? Based on the docs it only returns the public_ip
There was a problem hiding this comment.
If we don't use the fields it might make more sense to add a link to the docs and remove the property so if the field has another format or does not exist for some users, it does not break the integration for them
There was a problem hiding this comment.
Same for all other schemas below
There was a problem hiding this comment.
@Meierschlumpf the shemas come from this documentation . Since there was not a single endpoint i bunched them together to get the final schema
There was a problem hiding this comment.
Not sure if it is worth to have a test for one simple ternary operator


Adds a Gluetun integration and accompanying dashboard widget surfacing VPN connection details from a running gluetun container.
Homarr
Thank you for your contribution. Please ensure that your pull request meets the following pull request:
pnpm build, autofix withpnpm format:fix)devbranchx,y,ior any abbrevation)