Point the panel link at the installation, not at the browser's origin

Both links out of the dashboard editor were built root-relative, so a portal
serving the app under `/i/{id}` got a URL to itself: the hub has no route
there and answers a bare 404. That is what a device link and "open what a
wall panel sees" both landed on.

They want different answers. The view link is for the person already looking,
so it takes the router's basepath — `appPath` in `lib/portal` is the same
prefix the router applies to every `Link`, for the places that step outside
it. The device link is for a screen, which cannot go through the portal at
all: the shell is served only to a portal session, and the credential that
page carries is the portal's rather than the panel's. So the server now says
where it answers, and `FRONTEND_HOST` is that answer — the same setting the
password-reset links already use.

Also fixes the panel branch in the query error handler, which compared a raw
pathname and so never fired under a portal.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AHpLJHozysQXjsxAyU1WHj
This commit is contained in:
2026-08-20 17:24:20 +02:00
co-authored by Claude Opus 5
parent ffae24c16b
commit ce77262f81
10 changed files with 119 additions and 19 deletions
+26
View File
@@ -2054,6 +2054,32 @@ export const PanelsConfigSchema = {
description: 'Every panel this installation knows about.'
} as const;
export const PanelsPublicSchema = {
properties: {
panels: {
items: {
'$ref': '#/components/schemas/PanelDef'
},
type: 'array',
title: 'Panels'
},
frontend_host: {
type: 'string',
title: 'Frontend Host',
default: ''
}
},
type: 'object',
title: 'PanelsPublic',
description: `The panels, and the address a device should be pointed at.
The address is the server's own, because the browser's origin is not a
reliable answer to it: an admin working through the portal is on the
portal's origin, and a screen cannot be sent there — the portal serves a
page only to someone holding a portal session, and the credential it hands
that page is the portal's rather than the panel's.`
} as const;
export const PlacementSchema = {
properties: {
x: {