diff --git a/README.md b/README.md
index 1b1fd2d6..5a84177b 100644
--- a/README.md
+++ b/README.md
@@ -23,16 +23,16 @@
- **One way to install any package.** PS4 `.pkg` and PS5 fake packages —
base games, updates and DLC — install through ps5upload's own on-console
- installer, whether you **Stream install** straight from your PC (nothing
+ installer, whether you **Stream & install** straight from your PC (nothing
copied to the PS5), **Upload & install** (copied first, kept for
reinstalls), or install from a NAS, a USB drive or a download link. Every
route shows the same live progress, only says "installed" once the console
has the whole package, checks that an update really raised the game's
version, and never wipes your base game.
-- **Turn a game into an installable PS5 package.** **Convert to FPKG** (beta)
+- **Turn a game into an installable PS5 package.** **Convert to FPKG**
builds a fake package from a decrypted game folder or an `.exfat` / `.ffpkg`
image, on your computer, compressed the way Sony's own packages are — then
- installs it with Stream install or Upload & install in one click.
+ installs it with Stream & install or Upload & install in one click.
- **Sleep, wake and power from your desk.** Put the PS5 in rest mode, reboot
or shut it down, and **wake it back up over the network** — even straight
into your signed-in user with Wake & sign in. The app keeps the console and
@@ -131,7 +131,7 @@
playlist step can pull straight from a repo at run time so you don't
keep a pile of `.elf` files on your PC.
- **Install packages** — install a PS4 `.pkg` or PS5 fake package two ways:
- **Stream install** sends it straight from your PC (nothing is copied to
+ **Stream & install** sends it straight from your PC (nothing is copied to
the PS5 first — handy when console storage is tight), and **Upload &
install** copies it to the PS5's package library first, where it stays so
you can **Install**, **Reinstall** or **Delete** it later without
@@ -142,10 +142,10 @@
and the result checks out — an update that didn't raise the game's version
is reported, not called a success. Base, update and DLC installs verified
on FW 5.10.
-- **Convert to FPKG (beta)** — turn a decrypted game folder or an `.exfat` /
+- **Convert to FPKG** — turn a decrypted game folder or an `.exfat` /
`.ffpkg` image into an installable fake package on your computer, with the
- same compression Sony's packages use, then install it with Stream install
- or Upload & install. Enable it under Settings → Beta features.
+ same compression Sony's packages use, then install it with Stream & install
+ or Upload & install.
- **Web browser access** — run the engine (or the official Docker image)
and manage your PS5 from any browser on the LAN, the full app served
over HTTP. No desktop install needed on that machine. Unauthenticated —
@@ -430,7 +430,7 @@ port 9021 — a third-party component, not part of ps5upload.
ps5upload connects and then drops after a few seconds. Loading elfldr
once from PLDMGR and then sending ps5upload again also fixes it.
-**Q: Stream install fails before the PS5 downloads anything?**
+**Q: Stream & install fails before the PS5 downloads anything?**
* The PS5 pulls the package from your computer, so it has to be able to
reach it. Allow ps5upload through your computer's firewall (on Windows,
for both Private and Public networks), keep the computer and the PS5 on
diff --git a/client/src/layout/navItems.test.ts b/client/src/layout/navItems.test.ts
index 130f1607..114dd9e6 100644
--- a/client/src/layout/navItems.test.ts
+++ b/client/src/layout/navItems.test.ts
@@ -322,3 +322,14 @@ describe("Convert to FPKG", () => {
expect(APP).not.toMatch(/BetaRoute/);
});
});
+
+// A switch that reveals nothing is a dead control: Settings shows it only while
+// something is actually in beta.
+describe("hasBetaItems", () => {
+ it("is false when no screen is in beta", async () => {
+ const { hasBetaItems } = await import("./navItems");
+ expect(hasBetaItems([{ to: "/a", beta: false } as never])).toBe(false);
+ expect(hasBetaItems([{ to: "/a", beta: true } as never])).toBe(true);
+ expect(hasBetaItems()).toBe(NAV_ITEMS.some((i) => i.beta));
+ });
+});
diff --git a/client/src/layout/navItems.ts b/client/src/layout/navItems.ts
index d15873e3..4e492b83 100644
--- a/client/src/layout/navItems.ts
+++ b/client/src/layout/navItems.ts
@@ -315,6 +315,11 @@ export const PERMANENT_NAV_ITEMS: readonly NavItem[] = [
/** Whether an item is currently visible. Beta items stay hidden until the
* user turns them on, which is what keeps a half-finished screen out of a
* sidebar that the user never asked to be a test bench. */
+/** Whether any screen is in beta — Settings only offers the switch then. */
+export function hasBetaItems(items: NavItem[] = NAV_ITEMS): boolean {
+ return items.some((i) => i.beta);
+}
+
export function navItemVisible(item: NavItem, betaEnabled: boolean): boolean {
return !item.beta || betaEnabled;
}
diff --git a/client/src/screens/InstallPackage/index.tsx b/client/src/screens/InstallPackage/index.tsx
index c48fcdd3..5e40dbda 100644
--- a/client/src/screens/InstallPackage/index.tsx
+++ b/client/src/screens/InstallPackage/index.tsx
@@ -787,7 +787,7 @@ export default function InstallPackageScreen() {
}
}
- // Stream-install (beta, #81): pick a single PC-side .pkg and install it
+ // Stream & install (#81): pick a single PC-side .pkg and install it
// WITHOUT staging it on the PS5 first — the engine serves the file over
// HTTP and the DPI daemon pulls it directly. Useful for a quick one-shot
// install when you don't want to wait out the staging upload (or don't
diff --git a/client/src/screens/Settings/index.tsx b/client/src/screens/Settings/index.tsx
index e4790fca..8f6e2155 100644
--- a/client/src/screens/Settings/index.tsx
+++ b/client/src/screens/Settings/index.tsx
@@ -68,6 +68,7 @@ import { useConnectionStore } from "../../state/connection";
import { useEngineStore, DEFAULT_ENGINE_URL } from "../../state/engine";
import { useSaveSettingsStore, DEFAULT_SAVE_PATH } from "../../state/saveSettings";
import { useBetaFeaturesStore } from "../../state/betaFeatures";
+import { hasBetaItems } from "../../layout/navItems";
import { useRestAfterUploadStore } from "../../state/restAfterUpload";
import { userConfigPath, resetAllAppData } from "../../state/userConfig";
import { useUpdateStore, type UpdatePhase } from "../../state/update";
@@ -1001,26 +1002,32 @@ export default function SettingsScreen() {
-
- {tr("settings_group_beta", undefined, "Beta features")}
-
+ {/* Only while something is in beta: a switch that reveals nothing
+ reads as broken. */}
+ {hasBetaItems() && (
+ <>
+
+ {tr("settings_group_beta", undefined, "Beta features")}
+
-
- setBetaFeatures(on)}
- label={tr(
- "beta_features_label",
- undefined,
- "Show features that are still being finished",
- )}
- hint={tr(
- "beta_features_hint",
- undefined,
- "Adds work-in-progress screens to the sidebar when there are any. These are usable but not yet reliable, and may change or be removed.",
- )}
- />
-
+
+ setBetaFeatures(on)}
+ label={tr(
+ "beta_features_label",
+ undefined,
+ "Show features that are still being finished",
+ )}
+ hint={tr(
+ "beta_features_hint",
+ undefined,
+ "Adds work-in-progress screens to the sidebar when there are any. These are usable but not yet reliable, and may change or be removed.",
+ )}
+ />
+
+ >
+ )}
{tr("settings_group_automation", undefined, "Automation")}
diff --git a/client/src/state/pkgLibrary.ts b/client/src/state/pkgLibrary.ts
index 8ab51519..eae6d4a1 100644
--- a/client/src/state/pkgLibrary.ts
+++ b/client/src/state/pkgLibrary.ts
@@ -992,7 +992,7 @@ interface PkgLibraryState {
host: string,
opts?: { onDest?: (destPath: string) => void },
) => Promise<{ ok: boolean; message?: string }>;
- /** Stream-install (beta, #81) a local PC-side `.pkg` WITHOUT uploading it
+ /** Stream & install (#81) a local PC-side `.pkg` WITHOUT uploading it
* to PS5 staging first. The engine serves the file over HTTP at
* `/pkg-host/{session}/` and the DPI daemon pulls it directly. Saves the
* staging upload (and the disk space) for the quick-install case. The