feat(web): consolidate paired devices, self-contained sections, docs + lint
apple / swift (push) Successful in 1m6s
ci / rust (push) Successful in 5m51s
android / android (push) Successful in 6m21s
ci / web (push) Successful in 49s
ci / docs-site (push) Successful in 58s
windows-host / package (push) Successful in 8m6s
release / apple (push) Successful in 8m17s
deb / build-publish (push) Successful in 3m26s
decky / build-publish (push) Successful in 25s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 5s
ci / bench (push) Successful in 4m42s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 30s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 2m36s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 2m17s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Failing after 19s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 51s
apple / screenshots (push) Successful in 5m45s
docker / deploy-docs (push) Successful in 22s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Failing after 22s
apple / swift (push) Successful in 1m6s
ci / rust (push) Successful in 5m51s
android / android (push) Successful in 6m21s
ci / web (push) Successful in 49s
ci / docs-site (push) Successful in 58s
windows-host / package (push) Successful in 8m6s
release / apple (push) Successful in 8m17s
deb / build-publish (push) Successful in 3m26s
decky / build-publish (push) Successful in 25s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 5s
ci / bench (push) Successful in 4m42s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 30s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 2m36s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 2m17s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Failing after 19s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 51s
apple / screenshots (push) Successful in 5m45s
docker / deploy-docs (push) Successful in 22s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Failing after 22s
Web console - Pairing/Library/Stats refactored into self-contained subsections that each own their own queries + mutations; a shared slot-based layout (view.tsx) is filled by the live page (containers) and Storybook (pure cards + fixtures) so the layout can't drift. - All paired devices in one list on Pairing with a protocol column (punktfunk/1 + Moonlight), routing each unpair to the right endpoint; the redundant Clients page is removed. - Library: overview grid split from the add/edit form into separate files. - Login screen links out to the docs. Docs - "Console login password" section on every host page (apt/RPM/Bazzite/SteamOS/Windows) plus a new "Forgot your Password?" troubleshooting page, linked from the login screen. - Console served as HTTP/1.1 over TLS (drop the unusable HTTP/3 advertising) across the Bun entry, launchers, systemd units, and packaging. Tooling - Biome now respects .gitignore (stops linting generated code), config migrated to 2.5.1; all lint issues fixed cleanly. Also includes this branch's in-progress host, Apple client, packaging, and CI changes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -27,14 +27,7 @@ function ShellHarness({ initialPath }: { initialPath: string }) {
|
||||
),
|
||||
});
|
||||
|
||||
const navPaths = [
|
||||
"/",
|
||||
"/host",
|
||||
"/library",
|
||||
"/clients",
|
||||
"/pairing",
|
||||
"/settings",
|
||||
];
|
||||
const navPaths = ["/", "/host", "/library", "/pairing", "/settings"];
|
||||
const navRoutes = navPaths.map((path) =>
|
||||
createRoute({
|
||||
getParentRoute: () => rootRoute,
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { ClientsView } from "@/sections/Clients/view";
|
||||
import { pairedClients } from "./lib/fixtures";
|
||||
|
||||
const meta = {
|
||||
title: "Pages/Clients",
|
||||
component: ClientsView,
|
||||
args: { onUnpair: () => {}, isUnpairing: false },
|
||||
} satisfies Meta<typeof ClientsView>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Paired: Story = {
|
||||
args: { clients: { data: pairedClients, isLoading: false, error: null } },
|
||||
};
|
||||
|
||||
export const Empty: Story = {
|
||||
args: { clients: { data: [], isLoading: false, error: null } },
|
||||
};
|
||||
@@ -1,26 +1,58 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { LibraryView } from "@/sections/Library/view";
|
||||
import { GameForm } from "@/sections/Library/GameForm";
|
||||
import { LibraryGrid } from "@/sections/Library/LibraryGrid";
|
||||
import { library } from "./lib/fixtures";
|
||||
|
||||
const noop = () => {};
|
||||
const idle = { isLoading: false, error: null, refetch: noop };
|
||||
const emptyForm = {
|
||||
title: "",
|
||||
portrait: "",
|
||||
hero: "",
|
||||
header: "",
|
||||
command: "",
|
||||
};
|
||||
|
||||
// The overview grid and the add/edit form are separate components now, so the stories
|
||||
// render each on its own (no combined page view).
|
||||
const meta = {
|
||||
title: "Pages/Library",
|
||||
component: LibraryView,
|
||||
args: {
|
||||
onCreate: () => Promise.resolve(),
|
||||
onUpdate: () => Promise.resolve(),
|
||||
onDelete: () => Promise.resolve(),
|
||||
isSaving: false,
|
||||
isDeleting: false,
|
||||
},
|
||||
} satisfies Meta<typeof LibraryView>;
|
||||
parameters: { layout: "padded" },
|
||||
} satisfies Meta;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
type Story = StoryObj;
|
||||
|
||||
export const Populated: Story = {
|
||||
args: { library: { data: library, isLoading: false, error: null } },
|
||||
render: () => (
|
||||
<LibraryGrid
|
||||
library={{ data: library, ...idle }}
|
||||
onEdit={noop}
|
||||
onDelete={noop}
|
||||
isDeleting={false}
|
||||
/>
|
||||
),
|
||||
};
|
||||
|
||||
export const Empty: Story = {
|
||||
args: { library: { data: [], isLoading: false, error: null } },
|
||||
render: () => (
|
||||
<LibraryGrid
|
||||
library={{ data: [], ...idle }}
|
||||
onEdit={noop}
|
||||
onDelete={noop}
|
||||
isDeleting={false}
|
||||
/>
|
||||
),
|
||||
};
|
||||
|
||||
export const AddForm: Story = {
|
||||
render: () => (
|
||||
<GameForm
|
||||
initial={emptyForm}
|
||||
mode="add"
|
||||
onSubmit={noop}
|
||||
onCancel={noop}
|
||||
isSaving={false}
|
||||
/>
|
||||
),
|
||||
};
|
||||
|
||||
@@ -13,4 +13,4 @@ type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {};
|
||||
|
||||
export const Error: Story = { args: { error: true } };
|
||||
export const ErrorState: Story = { args: { error: true } };
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { MoonlightPairing } from "@/sections/Pairing/MoonlightPairingCard";
|
||||
import { NativePairingCard } from "@/sections/Pairing/NativePairingCard";
|
||||
import { PairedDevices } from "@/sections/Pairing/PairedDevices";
|
||||
import { PendingDevices } from "@/sections/Pairing/PendingDevices";
|
||||
import { PairingView } from "@/sections/Pairing/view";
|
||||
import {
|
||||
nativeClients,
|
||||
nativePairArmed,
|
||||
pairedClients,
|
||||
pairingIdle,
|
||||
pendingDevices,
|
||||
} from "./lib/fixtures";
|
||||
@@ -10,39 +15,70 @@ import {
|
||||
const noop = () => {};
|
||||
const idle = { isLoading: false, error: null, refetch: noop };
|
||||
|
||||
// Renders the REAL page layout (PairingView) — the same component index.tsx uses. The live page
|
||||
// fills its slots with the self-contained containers; here we fill them with the pure cards + mock
|
||||
// state, so there's no duplicated composition to drift.
|
||||
const meta = {
|
||||
title: "Pages/Pairing",
|
||||
component: PairingView,
|
||||
parameters: { layout: "padded" },
|
||||
args: {
|
||||
onApprove: noop,
|
||||
onDeny: noop,
|
||||
pendingBusy: false,
|
||||
onArm: noop,
|
||||
onDisarm: noop,
|
||||
isArming: false,
|
||||
isDisarming: false,
|
||||
onUnpair: noop,
|
||||
isUnpairing: false,
|
||||
pin: "",
|
||||
onPinChange: noop,
|
||||
onSubmitPin: noop,
|
||||
isSubmittingPin: false,
|
||||
pinSuccess: false,
|
||||
pinError: false,
|
||||
},
|
||||
} satisfies Meta<typeof PairingView>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
// The marketing state: a PIN armed for a phone, one device knocking for delegated
|
||||
// approval, two already-paired native clients.
|
||||
// The marketing state: one device knocking for delegated approval, a PIN armed for a phone, the
|
||||
// consolidated paired-devices list (native + Moonlight), idle Moonlight pairing.
|
||||
export const Armed: Story = {
|
||||
args: {
|
||||
pending: { data: pendingDevices, ...idle },
|
||||
native: { data: nativePairArmed, ...idle },
|
||||
clients: { data: nativeClients, ...idle },
|
||||
moonlight: { data: pairingIdle, ...idle },
|
||||
pending: (
|
||||
<PendingDevices
|
||||
pending={{ data: pendingDevices, ...idle }}
|
||||
onApprove={noop}
|
||||
onDeny={noop}
|
||||
busy={false}
|
||||
/>
|
||||
),
|
||||
native: (
|
||||
<NativePairingCard
|
||||
status={{ data: nativePairArmed, ...idle }}
|
||||
onArm={noop}
|
||||
onDisarm={noop}
|
||||
isArming={false}
|
||||
isDisarming={false}
|
||||
/>
|
||||
),
|
||||
moonlight: (
|
||||
<MoonlightPairing
|
||||
pairing={{ data: pairingIdle, ...idle }}
|
||||
pin=""
|
||||
onPinChange={noop}
|
||||
onSubmit={noop}
|
||||
isSubmitting={false}
|
||||
isSuccess={false}
|
||||
isError={false}
|
||||
/>
|
||||
),
|
||||
paired: (
|
||||
<PairedDevices
|
||||
rows={[
|
||||
...nativeClients.map((c) => ({
|
||||
protocol: "native" as const,
|
||||
fingerprint: c.fingerprint,
|
||||
name: c.name,
|
||||
})),
|
||||
...pairedClients.map((c) => ({
|
||||
protocol: "moonlight" as const,
|
||||
fingerprint: c.fingerprint,
|
||||
name: c.subject ?? "",
|
||||
})),
|
||||
]}
|
||||
isLoading={false}
|
||||
error={null}
|
||||
refetch={noop}
|
||||
onUnpair={noop}
|
||||
isUnpairing={false}
|
||||
/>
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,48 +1,77 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { CaptureControlCard } from "@/sections/Stats/CaptureControl";
|
||||
import { DetailCard } from "@/sections/Stats/Detail";
|
||||
import { RecordingsCard } from "@/sections/Stats/Recordings";
|
||||
import { StatsView } from "@/sections/Stats/view";
|
||||
import { captureDetail, captureMetas, statsStatusIdle } from "./lib/fixtures";
|
||||
|
||||
const noop = () => {};
|
||||
const idle = { isLoading: false, error: null, refetch: noop };
|
||||
|
||||
// Renders the REAL page layout (StatsView) — the same component index.tsx uses — with the pure
|
||||
// cards + mock state in its slots, so there's no duplicated composition to drift.
|
||||
const meta = {
|
||||
title: "Pages/Stats",
|
||||
component: StatsView,
|
||||
parameters: { layout: "padded" },
|
||||
args: {
|
||||
onStart: noop,
|
||||
onStop: noop,
|
||||
onSelect: noop,
|
||||
onDownload: noop,
|
||||
onDelete: noop,
|
||||
isStarting: false,
|
||||
isStopping: false,
|
||||
isDeleting: false,
|
||||
},
|
||||
} satisfies Meta<typeof StatsView>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
// A finished run open in the detail view: recordings table populated and the full
|
||||
// graph set (latency stack · throughput · loss/FEC) rendered from a deterministic
|
||||
// fixture series — no live host or capture needed.
|
||||
// A finished run open in the detail view: recordings table populated and the full graph set
|
||||
// (latency stack · throughput · loss/FEC) rendered from a deterministic fixture series — no live
|
||||
// host or capture needed.
|
||||
export const Recording: Story = {
|
||||
args: {
|
||||
status: { data: statsStatusIdle, ...idle },
|
||||
live: { data: undefined, ...idle },
|
||||
recordings: { data: captureMetas, ...idle },
|
||||
detail: { data: captureDetail, ...idle },
|
||||
selectedId: captureMetas[0]?.id ?? null,
|
||||
control: (
|
||||
<CaptureControlCard
|
||||
status={{ data: statsStatusIdle, ...idle }}
|
||||
onStart={noop}
|
||||
onStop={noop}
|
||||
isStarting={false}
|
||||
isStopping={false}
|
||||
/>
|
||||
),
|
||||
live: null,
|
||||
recordings: (
|
||||
<RecordingsCard
|
||||
recordings={{ data: captureMetas, ...idle }}
|
||||
selectedId={captureMetas[0]?.id ?? null}
|
||||
onSelect={noop}
|
||||
onDownload={noop}
|
||||
onDelete={noop}
|
||||
isDeleting={false}
|
||||
/>
|
||||
),
|
||||
detail: (
|
||||
<DetailCard detail={{ data: captureDetail, ...idle }} onClose={noop} />
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
export const Empty: Story = {
|
||||
args: {
|
||||
status: { data: statsStatusIdle, ...idle },
|
||||
live: { data: undefined, ...idle },
|
||||
recordings: { data: [], ...idle },
|
||||
detail: { data: undefined, ...idle },
|
||||
selectedId: null,
|
||||
control: (
|
||||
<CaptureControlCard
|
||||
status={{ data: statsStatusIdle, ...idle }}
|
||||
onStart={noop}
|
||||
onStop={noop}
|
||||
isStarting={false}
|
||||
isStopping={false}
|
||||
/>
|
||||
),
|
||||
live: null,
|
||||
recordings: (
|
||||
<RecordingsCard
|
||||
recordings={{ data: [], ...idle }}
|
||||
selectedId={null}
|
||||
onSelect={noop}
|
||||
onDownload={noop}
|
||||
onDelete={noop}
|
||||
isDeleting={false}
|
||||
/>
|
||||
),
|
||||
detail: null,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -216,6 +216,13 @@ export const pendingDevices: PendingDevice[] = [
|
||||
"9f8e7d6c5b4a39281706f5e4d3c2b1a0998877665544332211ffeeddccbbaa00",
|
||||
age_secs: 8,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Mac Mini",
|
||||
fingerprint:
|
||||
"ff00eeddccbbaa998877665544332211009f8e7d6c5b4a39281706f5e4d3c2b1",
|
||||
age_secs: 30,
|
||||
},
|
||||
];
|
||||
|
||||
export const nativeClients: NativeClient[] = [
|
||||
|
||||
Reference in New Issue
Block a user