desktop: embed GitDXD too (Forgejo needs [cors] X_FRAME_OPTIONS = unset)

This commit is contained in:
Martin Dvořák 2026-09-25 11:07:25 +00:00
commit 02c516af74
3 changed files with 25 additions and 20 deletions

View file

@ -36,10 +36,10 @@ heatmap**, a gym SBD panel and links to friends' sites.
- **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 frameable self-hosted service (Llama.cpp/Open WebUI, - **Embedded live services** — opening a frameable self-hosted service (Llama.cpp/Open WebUI,
Jellyfin, Navidrome) from the desktop shows the real site inside the window (an iframe). A Jellyfin, Navidrome, GitDXD) from the desktop shows the real site inside the window (an
service must not send `X-Frame-Options`: Navidrome sends `DENY` unless it runs with iframe). A service must not send a restrictive `X-Frame-Options`: Navidrome needs
`ND_HTTPHEADERS_FRAMEOPTIONS=ALLOWALL`. Forgejo/GitDXD (`SAMEORIGIN`) and Discord (`DENY`) `ND_HTTPHEADERS_FRAMEOPTIONS=ALLOWALL` and Forgejo needs `[cors] X_FRAME_OPTIONS = unset`.
keep a link card / their own app card instead. Discord sends `DENY` and keeps a link card instead.
- **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

@ -271,15 +271,18 @@ list reordering, not free placement. Don't confuse the two.
`createWebsiteApp({ embed: true })` (the shortcut's `embed` flag), but the `createWebsiteApp({ embed: true })` (the shortcut's `embed` flag), but the
browser refuses to frame a site that sends `X-Frame-Options` / browser refuses to frame a site that sends `X-Frame-Options` /
`frame-ancestors`, leaving a blank window. `embed` is set on Llama.cpp (Open `frame-ancestors`, leaving a blank window. `embed` is set on Llama.cpp (Open
WebUI), Jellyfin and Navidrome. Llama.cpp/Jellyfin allow framing; Navidrome WebUI), Jellyfin, Navidrome and GitDXD. Llama.cpp/Jellyfin allow framing out
only does when its host runs with `ND_HTTPHEADERS_FRAMEOPTIONS=ALLOWALL` of the box; the other two need their host configured so the header is an
(Navidrome maps `HTTPHeaders.FrameOptions` to that env var and it overrides unrecognised value (browsers ignore unknown X-Frame-Options values):
its built-in `FrameDeny`; any unrecognised value is ignored by the browser). Navidrome via `ND_HTTPHEADERS_FRAMEOPTIONS=ALLOWALL` (it maps
For a scoped, clickjacking-safe alternative, remove the header at the edge `HTTPHeaders.FrameOptions` to that env var and it overrides the built-in
(Cloudflare "Modify Response Header" / nginx) and add `FrameDeny`), and Forgejo via `[cors] X_FRAME_OPTIONS = unset` (Forgejo reads
`Content-Security-Policy: frame-ancestors`. Forgejo/GitDXD (`SAMEORIGIN`) this from the `[cors]` section, **not** `[security]`) followed by a Forgejo
keeps its dedicated app card. Always verify headers before enabling `embed` restart. For a scoped, clickjacking-safe alternative, remove the header at the
on a new service. edge (Cloudflare "Modify Response Header" / nginx) and add
`Content-Security-Policy: frame-ancestors`. Discord keeps a link card because
it sends `DENY` and can't be configured. 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

@ -41,19 +41,21 @@ interface IconEntry {
// `embed: true` renders the real site in an iframe, which the browser refuses // `embed: true` renders the real site in an iframe, which the browser refuses
// (blank window) when the server sends X-Frame-Options / frame-ancestors. // (blank window) when the server sends X-Frame-Options / frame-ancestors.
// Llama.cpp and Jellyfin allow framing out of the box. Navidrome sends // Llama.cpp and Jellyfin allow framing out of the box; Navidrome and Forgejo
// `X-Frame-Options: DENY` unless it is started with // need to be told to stop sending a restrictive value:
// `ND_HTTPHEADERS_FRAMEOPTIONS=ALLOWALL`, which replaces that header with an // - Navidrome: `ND_HTTPHEADERS_FRAMEOPTIONS=ALLOWALL`
// unrecognised value the browser ignores (that env var is set on the Navidrome // - Forgejo: `[cors] X_FRAME_OPTIONS = unset` (Forgejo reads this from
// host — without it this window is blank again). Forgejo (GitDXD) sends // [cors], NOT [security]) then restart Forgejo.
// `SAMEORIGIN`, so it keeps its dedicated app card. // Any X-Frame-Options value a browser doesn't recognise is ignored, so the
// frame is allowed. These live on the service hosts — without them the windows
// go blank again.
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", embed: true },
{ id: "svc-gitdxd", icon: "🐙", label: "GitDXD", appId: "gitdxd", color: "#6a4fb3", desc: "Best Git Hosting OAT" }, { id: "svc-gitdxd", icon: "🐙", label: "GitDXD", url: "https://git.dvorakmortonxd.dev", color: "#6a4fb3", desc: "Best Git Hosting OAT", embed: true },
]; ];
const FRIEND_SHORTCUTS: Shortcut[] = friends.map((f, i) => ({ const FRIEND_SHORTCUTS: Shortcut[] = friends.map((f, i) => ({