diff --git a/README.md b/README.md
index e902dab..8551080 100644
--- a/README.md
+++ b/README.md
@@ -35,10 +35,10 @@ heatmap**, a gym SBD panel and links to friends' sites.
hidden `ssh` easter egg.
- **Live service status** — each self-hosted service is health-checked through the site's
own API; Minecraft is checked through mcstatus.io.
-- **Embedded live services** — opening a self-hosted service (Llama.cpp/Open WebUI,
- Jellyfin, Navidrome, GitDXD) from the desktop shows the real site inside the window (an
- iframe). The service must allow framing; SMP (Discord) keeps a link card because Discord
- sends `X-Frame-Options: DENY`.
+- **Embedded live services** — opening a frameable self-hosted service (Llama.cpp/Open WebUI,
+ Jellyfin) from the desktop shows the real site inside the window (an iframe). Services that
+ send `X-Frame-Options` / `frame-ancestors` can't be framed, so they keep a link card or their
+ 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's multiplayer screen, with live status, player count, MOTD and version pulled
from mcstatus.io through the site's own ping API.
diff --git a/opencode.md b/opencode.md
index 2d335fb..29e4b62 100644
--- a/opencode.md
+++ b/opencode.md
@@ -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
`ALLOWED_HOSTS` in `api/ping/route.ts`**, otherwise its status dot reads
"Dead". Do not turn this route back into an open fetch proxy.
-- Service shortcuts show the real site in an iframe via
- `createWebsiteApp({ embed: true })` (the shortcut's `embed` flag). It only
- works for sites that don't send `X-Frame-Options` / a `frame-ancestors` CSP.
- `embed` is set on Llama.cpp (Open WebUI), Jellyfin, Navidrome and GitDXD; SMP
- (Discord) is excluded because Discord sends `DENY`. Navidrome (`DENY`) and
- GitDXD (`SAMEORIGIN`) still need those headers stripped at the edge (a
- Cloudflare "Modify Response Header" rule or nginx) before their frames render.
- Always verify headers before enabling `embed` on a new service.
+- Service shortcuts can show the real site in an iframe via
+ `createWebsiteApp({ embed: true })` (the shortcut's `embed` flag), but the
+ browser refuses to frame a site that sends `X-Frame-Options` /
+ `frame-ancestors`, leaving a blank window. `embed` is only set on Llama.cpp
+ (Open WebUI) and Jellyfin, which allow framing. Navidrome (`DENY`) and
+ Forgejo/GitDXD (`SAMEORIGIN`) are deliberately **not** embedded: Navidrome
+ falls back to the link card and GitDXD opens its dedicated app card. To embed
+ 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
`animation: none`) so `animationend` still fires and windows unmount.
diff --git a/src/app/components/WindowsDesktop.tsx b/src/app/components/WindowsDesktop.tsx
index db82b06..586eb1b 100644
--- a/src/app/components/WindowsDesktop.tsx
+++ b/src/app/components/WindowsDesktop.tsx
@@ -39,13 +39,18 @@ interface IconEntry {
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[] = [
{ 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-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-navidrome", icon: "🎸", label: "Navidrome", url: "https://navidrome.dvorakmortonxd.dev/", color: "#b3005f", desc: "Music Streaming", embed: true },
- { id: "svc-gitdxd", icon: "🐙", label: "GitDXD", url: "https://git.dvorakmortonxd.dev", color: "#6a4fb3", desc: "Best Git Hosting OAT", embed: true },
+ { id: "svc-navidrome", icon: "🎸", label: "Navidrome", url: "https://navidrome.dvorakmortonxd.dev/", color: "#b3005f", desc: "Music Streaming" },
+ { id: "svc-gitdxd", icon: "🐙", label: "GitDXD", appId: "gitdxd", color: "#6a4fb3", desc: "Best Git Hosting OAT" },
];
const FRIEND_SHORTCUTS: Shortcut[] = friends.map((f, i) => ({
diff --git a/src/app/components/apps.tsx b/src/app/components/apps.tsx
index 5d2b3c4..7bfb964 100644
--- a/src/app/components/apps.tsx
+++ b/src/app/components/apps.tsx
@@ -875,7 +875,7 @@ export const APPS: AppDef[] = [
{ id: "projects", title: "Projects", icon: "🎮", color: "#b3005f", width: 860, height: 560, content: },
{ id: "hardware", title: "Hardware", icon: "💻", color: "#1257a0", width: 860, height: 560, content: },
{ id: "friends", title: "Friends", icon: "🫂", color: "#c0392b", width: 780, height: 560, content: },
- { id: "gitdxd", title: "GitDXD", icon: "🐙", color: "#333333", width: 780, height: 560, content: },
+ { id: "gitdxd", title: "GitDXD", icon: "🐙", color: "#333333", width: 780, height: 560, url: "https://git.dvorakmortonxd.dev", content: },
{ id: "contributions", title: "Contributions", icon: "📊", color: "#157347", width: 900, height: 470, content: },
{ id: "gym", title: "Gym SBD", icon: "🏋️", color: "#c0392b", width: 620, height: 380, content: },
{ id: "revival", title: "Revival Inc.", icon: "🧟", color: "#6a4fb3", width: 740, height: 560, content: },