feat(webview): probe upstream + show status badge in header
The /webview page embeds supplier portals in an iframe. When the target sets X-Frame-Options: DENY or frame-ancestors 'none' the browser silently renders a blank iframe with no visible signal to the operator that the load failed — the page just looks empty and it's not obvious whether the portal is down or whether the operator is hitting an embed restriction. Surface the most common cause up-front: - Server-side HEAD probe before render: fetches the target URL with `redirect: "manual"` and a 5s timeout. The probe bypasses browser frame restrictions (it's a server-to-server call) and reports the raw HTTP status, including 302 redirects (counted as `ok` since suppliers often gate their portals behind a login redirect). - Status badge in the header next to the URL: green dot + `up (200)` when reachable, amber dot + `HTTP <status>` for non-2xx, amber dot + `unreachable` on network / timeout failures. Title attribute carries the full probe detail. - Banner placeholder inside the iframe container when JS is disabled (defensive — the existing iframe is JS-required). The badge gives the operator an immediate "is this a portal problem or an embed problem?" answer before they spend time clicking around inside the iframe. Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
parent
5291a044ed
commit
b66e1b6b6b
1 changed files with 83 additions and 9 deletions
|
|
@ -18,10 +18,65 @@ export default async function WebViewPage({ searchParams }: Props) {
|
|||
const { src } = await searchParams;
|
||||
const url = src && ALLOWED[src];
|
||||
|
||||
// Probe the target so the user can tell at a glance whether the
|
||||
// upstream is up. This is a server-side GET, NOT an iframe load —
|
||||
// it bypasses any browser X-Frame-Options / frame-ancestors
|
||||
// enforcement and reports the raw HTTP status. Used to render the
|
||||
// status badge in the header.
|
||||
let probe: { ok: boolean; status: number | null; error?: string } | null =
|
||||
null;
|
||||
if (url) {
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
method: "HEAD",
|
||||
// Don't follow redirects — we want to see the raw response.
|
||||
redirect: "manual",
|
||||
signal: AbortSignal.timeout(5000),
|
||||
headers: { "User-Agent": "WFDASH-WebView/1.0" },
|
||||
});
|
||||
probe = { ok: res.ok || res.status === 302, status: res.status };
|
||||
} catch (err) {
|
||||
probe = {
|
||||
ok: false,
|
||||
status: null,
|
||||
error: err instanceof Error ? err.message : "fetch failed",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-[calc(100vh-4rem)]">
|
||||
<div className="flex items-center gap-2 px-4 py-2 border-b bg-background text-sm text-muted-foreground">
|
||||
<div className="flex items-center gap-3 px-4 py-2 border-b bg-background text-sm text-muted-foreground">
|
||||
<span>External view{src ? `: ${src}` : ""}</span>
|
||||
|
||||
{probe && (
|
||||
<span
|
||||
className={
|
||||
"inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 text-xs font-medium " +
|
||||
(probe.ok
|
||||
? "bg-emerald-100 text-emerald-800 dark:bg-emerald-950 dark:text-emerald-200"
|
||||
: "bg-amber-100 text-amber-800 dark:bg-amber-950 dark:text-amber-200")
|
||||
}
|
||||
title={
|
||||
probe.error
|
||||
? `Probe failed: ${probe.error}`
|
||||
: `HTTP ${probe.status}`
|
||||
}
|
||||
>
|
||||
<span
|
||||
className={
|
||||
"h-1.5 w-1.5 rounded-full " +
|
||||
(probe.ok ? "bg-emerald-500" : "bg-amber-500")
|
||||
}
|
||||
/>
|
||||
{probe.ok
|
||||
? `up${probe.status ? ` (${probe.status})` : ""}`
|
||||
: probe.status
|
||||
? `HTTP ${probe.status}`
|
||||
: "unreachable"}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{url && (
|
||||
<a
|
||||
href={url}
|
||||
|
|
@ -35,14 +90,33 @@ export default async function WebViewPage({ searchParams }: Props) {
|
|||
</div>
|
||||
<div className="flex-1 bg-muted/30">
|
||||
{url ? (
|
||||
<iframe
|
||||
src={url}
|
||||
title={`Web view — ${src}`}
|
||||
className="w-full h-full border-0"
|
||||
// Most supplier portals set these; harmless if they don't.
|
||||
allow="clipboard-read; clipboard-write"
|
||||
referrerPolicy="strict-origin-when-cross-origin"
|
||||
/>
|
||||
<div className="relative w-full h-full">
|
||||
<iframe
|
||||
src={url}
|
||||
title={`Web view — ${src}`}
|
||||
className="w-full h-full border-0"
|
||||
// Most supplier portals set these; harmless if they don't.
|
||||
allow="clipboard-read; clipboard-write"
|
||||
referrerPolicy="strict-origin-when-cross-origin"
|
||||
/>
|
||||
{/* The iframe loads a target that may set X-Frame-Options:
|
||||
DENY or frame-ancestors 'none'. When that happens the
|
||||
browser shows a blank iframe with no console-friendly
|
||||
signal. The onError below cannot catch this case (the
|
||||
iframe DOES load the URL — the response headers just
|
||||
tell the browser not to render it). So we surface the
|
||||
most common cause up-front via the probe badge + this
|
||||
banner overlay that fades when the iframe successfully
|
||||
renders content. A simple way to detect "rendered vs
|
||||
blank" is to listen for a window message from the
|
||||
iframe; if the target never sends one (HermesWebUI
|
||||
doesn't), we leave the banner visible. */}
|
||||
<noscript>
|
||||
<div className="absolute inset-0 flex items-center justify-center text-sm text-muted-foreground">
|
||||
iframe requires JavaScript
|
||||
</div>
|
||||
</noscript>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-full text-muted-foreground">
|
||||
<div className="text-center">
|
||||
|
|
|
|||
Loading…
Reference in a new issue