ci / bun-nix (pull_request) Successful in 51s
ci / docs-site (pull_request) Successful in 1m35s
ci / web (pull_request) Successful in 2m30s
ci / rust-arm64 (pull_request) Successful in 3m16s
ci / rust (pull_request) Failing after 9m12s
nix / flake (pull_request) Failing after 19m50s
The broken inset on the Displays configuration card was the symptom. The cause is structural, and it had already been diagnosed at least twice in-tree without being fixed. Two faults, both in components/ui/card.tsx: 1. The padding was a RESPONSIVE COMPOUND: `p-4 pt-0 sm:p-6 sm:pt-0`. tailwind-merge resolves conflicts only within a variant, so any call-site override won at the base and lost at `sm:` — correct on a phone, wrong on every desktop. Measured on the Displays card before this change: padding-top 24px at 500px, 0px at 1440px. 2. `pt-0` encoded an assumption about a SIBLING that nothing enforced — "a CardHeader is above me and supplies the top inset". Delete the header, which is exactly what tabbing a page does since the tab label replaces the card title, and the top inset silently vanishes at ≥640px. Fix: - One single-variant utility, `p-padding-card` — the same `--spacing-padding-card` token @unom/ui's own Card uses, so nested cards finally agree on their inset. A single variant cannot half-lose an override. - Top inset is now self-correcting: `[&:not(:first-child)]:pt-0`. Ask the DOM instead of the author. A headerless CardContent keeps its inset with nothing to remember. Seven call sites had grown their own compensation in five dialects — `p-6`, `p-card pt-card sm:pt-card` (×3), `p-4 sm:pt-6` (×3), `pt-4 sm:pt-6`, and my own `pt-6` from the tabs commit. All removed; they are the symptom-fixes this replaces. LogsCard even carried a six-line comment correctly describing the trap and working around it locally — that comment is now three lines saying it no longer needs saying. `flush` stays: full-bleed content is a real intent, expressed as a prop the component honours rather than a utility that has to out-argue the one already there. Guarded by UI/Card → "Inset with and without header", a headered/headerless pair that has to look identical on every side. It must be checked at BOTH widths — a single width cannot show this class of bug, which is why it kept surviving. Verified by measuring computed padding at 500px and 1440px: first child 20px on all four sides, after-a-header 0px top and 20px elsewhere, identical at both widths. tsc clean, biome clean on every touched file, 9/9 server tests, build + i18n clean, 32/32 screenshots.
55 lines
2.3 KiB
TypeScript
55 lines
2.3 KiB
TypeScript
import { AlertTriangle } from "lucide-react";
|
|
import type { FC } from "react";
|
|
import { useGetLocalSummary } from "@/api/gen/host/host";
|
|
import { Card, CardContent } from "@/components/ui/card";
|
|
import { m } from "@/paraglide/messages";
|
|
|
|
/**
|
|
* "Something else is already listening on these ports."
|
|
*
|
|
* The host detects other Moonlight-compatible servers (Sunshine, Apollo, …) on the same machine at
|
|
* startup and reports them in `GET /local/summary` as `conflicts`. Nothing surfaced it, even though
|
|
* it is the single most common reason a Punktfunk host looks installed and working but no client can
|
|
* reach it — two servers fighting over the same ports, with whichever won the bind answering the
|
|
* client.
|
|
*
|
|
* `conflicts` carries only servers that are running or set to start on their own; the host filters
|
|
* dormant leftovers out (see `detect.rs`), because an uninstalled Sunshine's `Program Files` folder
|
|
* clashes with nothing and this card used to shout about it on every load. Each entry names what was
|
|
* observed — `Sunshine (running)`, `Apollo (starts automatically)` — so the heading never has to
|
|
* guess, which it previously did by hardcoding "is running".
|
|
*
|
|
* Renders nothing at all when there is no conflict, so a healthy host sees no extra chrome.
|
|
*/
|
|
export const ConflictsCard: FC = () => {
|
|
// Static per host boot (the host probes once at startup), so there is nothing to poll for.
|
|
const summary = useGetLocalSummary({ query: { staleTime: 5 * 60_000 } });
|
|
const conflicts = summary.data?.conflicts ?? [];
|
|
if (conflicts.length === 0) return null;
|
|
return (
|
|
<Card className="border-amber-600/40 dark:border-amber-500/40">
|
|
<CardContent className="flex items-start gap-3">
|
|
<AlertTriangle className="mt-0.5 size-5 shrink-0 text-amber-600 dark:text-amber-500" />
|
|
<div className="min-w-0 flex-1 space-y-2">
|
|
<p className="text-sm font-medium text-amber-600 dark:text-amber-500">
|
|
{m.host_conflicts_title()}
|
|
</p>
|
|
<p className="max-w-prose text-sm text-muted-foreground">
|
|
{m.host_conflicts_help()}
|
|
</p>
|
|
<ul className="flex flex-col gap-1">
|
|
{conflicts.map((c) => (
|
|
<li
|
|
key={c}
|
|
className="rounded-md bg-muted px-3 py-1.5 font-mono text-xs text-muted-foreground"
|
|
>
|
|
{c}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
};
|