desktop: don't embed Navidrome/GitDXD (servers send X-Frame-Options, so the windows were blank)

This commit is contained in:
Martin Dvořák 2026-09-25 08:09:10 +00:00
commit dbff352054
4 changed files with 22 additions and 15 deletions

View file

@ -35,10 +35,10 @@ heatmap**, a gym SBD panel and links to friends' sites.
hidden `ssh` easter egg. hidden `ssh` easter egg.
- **Live service status** — each self-hosted service is health-checked through the site's - **Live service status** — each self-hosted service is health-checked through the site's
own API; Minecraft is checked through mcstatus.io. own API; Minecraft is checked through mcstatus.io.
- **Embedded live services** — opening a self-hosted service (Llama.cpp/Open WebUI, - **Embedded live services** — opening a frameable self-hosted service (Llama.cpp/Open WebUI,
Jellyfin, Navidrome, GitDXD) from the desktop shows the real site inside the window (an Jellyfin) from the desktop shows the real site inside the window (an iframe). Services that
iframe). The service must allow framing; SMP (Discord) keeps a link card because Discord send `X-Frame-Options` / `frame-ancestors` can't be framed, so they keep a link card or their
sends `X-Frame-Options: DENY`. own app card instead (Navidrome sends `DENY`, Forgejo/GitDXD `SAMEORIGIN`, Discord `DENY`).
- **Minecraft-style SMP window** — the SMP shortcut opens a join menu modelled on - **Minecraft-style SMP window** — the SMP shortcut opens a join menu modelled on
Minecraft's multiplayer screen, with live status, player count, MOTD and version pulled Minecraft's multiplayer screen, with live status, player count, MOTD and version pulled
from mcstatus.io through the site's own ping API. from mcstatus.io through the site's own ping API.

View file

@ -267,14 +267,16 @@ list reordering, not free placement. Don't confuse the two.
redirects. **Adding a service or friend host means adding it to redirects. **Adding a service or friend host means adding it to
`ALLOWED_HOSTS` in `api/ping/route.ts`**, otherwise its status dot reads `ALLOWED_HOSTS` in `api/ping/route.ts`**, otherwise its status dot reads
"Dead". Do not turn this route back into an open fetch proxy. "Dead". Do not turn this route back into an open fetch proxy.
- Service shortcuts show the real site in an iframe via - Service shortcuts can show the real site in an iframe via
`createWebsiteApp({ embed: true })` (the shortcut's `embed` flag). It only `createWebsiteApp({ embed: true })` (the shortcut's `embed` flag), but the
works for sites that don't send `X-Frame-Options` / a `frame-ancestors` CSP. browser refuses to frame a site that sends `X-Frame-Options` /
`embed` is set on Llama.cpp (Open WebUI), Jellyfin, Navidrome and GitDXD; SMP `frame-ancestors`, leaving a blank window. `embed` is only set on Llama.cpp
(Discord) is excluded because Discord sends `DENY`. Navidrome (`DENY`) and (Open WebUI) and Jellyfin, which allow framing. Navidrome (`DENY`) and
GitDXD (`SAMEORIGIN`) still need those headers stripped at the edge (a Forgejo/GitDXD (`SAMEORIGIN`) are deliberately **not** embedded: Navidrome
Cloudflare "Modify Response Header" rule or nginx) before their frames render. falls back to the link card and GitDXD opens its dedicated app card. To embed
Always verify headers before enabling `embed` on a new service. them live you must strip those headers at the edge (Cloudflare "Modify
Response Header" or nginx) and accept the clickjacking risk. Always verify
headers before enabling `embed` on a new service.
- Reduced motion is handled with a near-zero `animation-duration` (not - Reduced motion is handled with a near-zero `animation-duration` (not
`animation: none`) so `animationend` still fires and windows unmount. `animation: none`) so `animationend` still fires and windows unmount.

View file

@ -39,13 +39,18 @@ interface IconEntry {
onOpen: () => void; onOpen: () => void;
} }
// `embed: true` renders the real site in an iframe, which the browser refuses
// (blank window) when the server sends X-Frame-Options / frame-ancestors.
// Llama.cpp and Jellyfin allow framing; Navidrome sends `X-Frame-Options: DENY`
// and Forgejo (GitDXD) sends `SAMEORIGIN`, so those two use their link card or
// the dedicated GitDXD app instead.
const SERVICE_SHORTCUTS: Shortcut[] = [ const SERVICE_SHORTCUTS: Shortcut[] = [
{ id: "svc-proxmox", icon: "🖥️", label: "Proxmox", appId: "alacritty", color: "#1257a0", desc: "Server Management" }, { id: "svc-proxmox", icon: "🖥️", label: "Proxmox", appId: "alacritty", color: "#1257a0", desc: "Server Management" },
{ id: "svc-ai", icon: "🦾", label: "Llama.cpp", url: "https://ai.dvorakmortonxd.dev/", color: "#157347", desc: "Local AI Server", embed: true }, { id: "svc-ai", icon: "🦾", label: "Llama.cpp", url: "https://ai.dvorakmortonxd.dev/", color: "#157347", desc: "Local AI Server", embed: true },
{ id: "svc-jellyfin", icon: "🎬", label: "Jellyfin", url: "https://jelly.dvorakmortonxd.dev/", color: "#6a4fb3", desc: "Local Media Server", embed: true }, { id: "svc-jellyfin", icon: "🎬", label: "Jellyfin", url: "https://jelly.dvorakmortonxd.dev/", color: "#6a4fb3", desc: "Local Media Server", embed: true },
{ id: "svc-smp", icon: "🎮", label: "SMP", appId: "smp", color: "#c0392b", desc: "My Survival Minecraft Server" }, { id: "svc-smp", icon: "🎮", label: "SMP", appId: "smp", color: "#c0392b", desc: "My Survival Minecraft Server" },
{ id: "svc-navidrome", icon: "🎸", label: "Navidrome", url: "https://navidrome.dvorakmortonxd.dev/", color: "#b3005f", desc: "Music Streaming", embed: true }, { id: "svc-navidrome", icon: "🎸", label: "Navidrome", url: "https://navidrome.dvorakmortonxd.dev/", color: "#b3005f", desc: "Music Streaming" },
{ id: "svc-gitdxd", icon: "🐙", label: "GitDXD", url: "https://git.dvorakmortonxd.dev", color: "#6a4fb3", desc: "Best Git Hosting OAT", embed: true }, { id: "svc-gitdxd", icon: "🐙", label: "GitDXD", appId: "gitdxd", color: "#6a4fb3", desc: "Best Git Hosting OAT" },
]; ];
const FRIEND_SHORTCUTS: Shortcut[] = friends.map((f, i) => ({ const FRIEND_SHORTCUTS: Shortcut[] = friends.map((f, i) => ({

View file

@ -875,7 +875,7 @@ export const APPS: AppDef[] = [
{ id: "projects", title: "Projects", icon: "🎮", color: "#b3005f", width: 860, height: 560, content: <ProjectsApp /> }, { id: "projects", title: "Projects", icon: "🎮", color: "#b3005f", width: 860, height: 560, content: <ProjectsApp /> },
{ id: "hardware", title: "Hardware", icon: "💻", color: "#1257a0", width: 860, height: 560, content: <HardwareApp /> }, { id: "hardware", title: "Hardware", icon: "💻", color: "#1257a0", width: 860, height: 560, content: <HardwareApp /> },
{ id: "friends", title: "Friends", icon: "🫂", color: "#c0392b", width: 780, height: 560, content: <FriendsApp /> }, { id: "friends", title: "Friends", icon: "🫂", color: "#c0392b", width: 780, height: 560, content: <FriendsApp /> },
{ id: "gitdxd", title: "GitDXD", icon: "🐙", color: "#333333", width: 780, height: 560, content: <GitdxdApp /> }, { id: "gitdxd", title: "GitDXD", icon: "🐙", color: "#333333", width: 780, height: 560, url: "https://git.dvorakmortonxd.dev", content: <GitdxdApp /> },
{ id: "contributions", title: "Contributions", icon: "📊", color: "#157347", width: 900, height: 470, content: <ContributionsApp /> }, { id: "contributions", title: "Contributions", icon: "📊", color: "#157347", width: 900, height: 470, content: <ContributionsApp /> },
{ id: "gym", title: "Gym SBD", icon: "🏋️", color: "#c0392b", width: 620, height: 380, content: <GymApp /> }, { id: "gym", title: "Gym SBD", icon: "🏋️", color: "#c0392b", width: 620, height: 380, content: <GymApp /> },
{ id: "revival", title: "Revival Inc.", icon: "🧟", color: "#6a4fb3", width: 740, height: 560, content: <RevivalApp /> }, { id: "revival", title: "Revival Inc.", icon: "🧟", color: "#6a4fb3", width: 740, height: 560, content: <RevivalApp /> },