Cards show installed, update and newest version side by side, and
problems as notes with their action. Other versions became a button.
The dialog groups versions into available, needs newer firmware,
installed and older (folded), with larger type throughout. Text is at
least 13px, buttons 44px tall, and every control has a focus ring.
Phones get two status tiles per row and full-width card content.
The UI follows the browser language, which on the PS5 is the system
language, and Settings has a selector to override it per device (kept in
localStorage). All UI text moved into web/i18n.js (306 keys) with English,
German, French, Spanish, Italian, Portuguese (Brazil), Japanese, Simplified
Chinese, Korean and Russian; missing keys fall back to English.
Log entries keep their key and values, so the Logs view shows them in the
chosen language while the diagnostic report stays English for issues.
An optional "Other versions" link on a game card lists every version the
patch site knows: Prospero Patches for PS5, its sister site Orbis Patches
for PS4 (same API). Versions newer than the installed one that run on this
firmware can be picked; older ones are shown but not offered, since
installing an older patch over a newer one is untested.
Picking a version opens the existing guide for exactly that version: QR
code to the phone, copy the link on the site, paste it. Orbis offers the
piece links of a PS4 patch ("Copy all links!"); they sit in the same gs2
folder as the manifest, so PatchDL derives "<name>.json" from any
"<name>_<n>.pkg" and fetches it over http like other PS4 manifests. The
size check uses the size the list showed for the version picked.
The Prospero lookup behind "Old FW patch" now uses the same list.
Sideloaded apps
- A Sony-signed package installed with another tool ("fake": true in
app.json, but no PatchDL loopback URL) stays Official but is marked
Sideloaded, and its Updates switch starts off, so Update all skips it.
Switching it on is saved separately, so a switch saved earlier can't
turn it on. The card says an update replaces this exact version.
Failed downloads
- When the package list can't be fetched, the download job records why
(DNS, connection, timeout, TLS, redirect, HTTP status, not a manifest)
with the curl and HTTP codes; the card says so with a Details link.
- Details runs /api/netcheck?title=<id>, which walks the game's update
list and manifest the way a download does and reports PatchDL's own
DNS answer for each host. The dialog shows a checklist and a report to
copy into an issue; on the PS5 a QR code opens it on a phone.
Cards are rebuilt on list updates, and each rebuild restarted the bar's
animation. A negative animation-delay taken from the clock lets a new bar
continue where the old one was. The sweep is linear now instead of easing
in from a standstill, and it stops for prefers-reduced-motion.
Library
- Rescan when /user/app or Sony's app.db changes, after an install, and on
the Refresh button (which also asks Sony for updates again). Later scans
keep a handle to the system root and use relative paths instead of
swapping the process root while other threads run; app.db is copied out
first because SQLite resolves relative paths with getcwd(). The scan
thread gets a 1 MB stack (the default made reads fail with EFAULT).
- Take the newer of app.db and appmeta param.json as installed version:
app.db lags behind until the game is started.
- A package header decides "fake"; the installer's "fake": true marker
only says the package didn't come from Sony's CDN, and PatchDL's own
installs set it too.
- PS4 titles are listed as not supported.
Downloads
- Fetch the manifest's .crc next to the package; the console asks for it.
- Keep the sidecar (with the version) after a download completes, so a
restart still knows the package is whole.
- A package already on disk without a matching sidecar is hashed against
the manifest piece by piece; only pieces that don't match are fetched.
Install refuses an unverified package.
Installs
- One install per title: a second request (another tab, a double click)
gets install_running; an installed version gets already_installed.
Download, Cancel and Delete wait until the console is done reading.
- Progress comes from the bytes PatchDL serves to the console (distinct
ranges), since Sony's status query fails for these installs. Phases:
waiting, reading with percent, finishing, done or stalled. Each title
is tracked on its own.
- After an install: check that the installed version matches and the new
app.json names this package and its size. With "Delete PKG after
install" on, delete the package only then. The setting existed but was
never read; the old cleanup trusted app.db alone and deleted packages
that were never applied.
UI
- Settings list every package on the console with size, version and
whether it is installed, newer or for a game that's gone, with Delete.
The storage tile links there.
- The list reloads quietly when the server reports a change.
- Bigger covers on wide screens, "Updates on/off" on the switch, no
"Shared master" badge, no endless auto-install retries.
Disc and store installs whose update Sony serves from another region's
master package can't be installed from homebrew; Sony's installer refused
them in testing. They now sit under Not supported with a short reason
instead of offering a download that leads nowhere.
Before preallocating a package, PatchDL now checks that it fits on /data
with 2 GB to spare and reports how much it needs. The Prospero link check
runs the same test, so the phone gets the answer right away.
The PS5 browser can't copy links, so on the console the old-FW guide only
shows a large QR code and the link for the phone.
Sony's version.xml only lists the newest patch. When that one needs a
newer firmware, PatchDL now asks Prospero Patches for the version history
and offers the newest patch that still runs on the console. Prospero only
hands out download links after a human check, so a guide with a QR code
moves the user to a phone, where they copy the SC link. PatchDL derives
Sony's manifest from that link and downloads like an official update.
The game list now groups by what the user can do: Updates, Up to date,
Not supported. Update all read a nonexistent state.games and did nothing;
it now uses state.titles.
- The scan opens each game's icon0.png (app.db icon0Info first, then
/user/appmeta and the title's sce_sys) while it can still see the
whole filesystem, and keeps the fd. GET /api/icon/<title_id> serves
it with a one-day cache. Cards show the icon and fall back to the
initials; loaded icons are reused across re-renders.
- "Latest x · FW y" read like the console firmware. It now says
"needs FW y".
A sticky-feeling banner appears whenever any job is queued or active.
It shows the eyebrow (Downloading / Queued), the current title's name,
a "3 of 9" chip when more than one job is in this batch, plus
percent, speed, and ETA. A thin gradient bar runs edge-to-edge along
the bottom so the at-a-glance state matches the per-card progress.
Batch counting: jobs with state in {queued, active, done} make up the
batch; done count + 1 is the current position. When all queued jobs
finish and roll out of the pool list the banner hides on the next poll.
While the active job's manifest is still being fetched (no total_bytes
yet) the bar runs an indeterminate sweep so the user isn't staring at
a frozen 0%. Speed comes from the existing per-job smoothed estimate
in reconcileFromJobs so the banner shares one source of truth with
the per-game tiles.
Render hooks: renderGames() and applyDownloadProgress() both call
renderGlobalStatus(), so every refresh path keeps it in sync without
adding a separate timer.
Resume: write the sidecar after every completed piece instead of batching
every 8. Each piece's bytes are already fdatasync'd and the sidecar write is
a tiny atomic tmp+rename, so an unclean kill now re-downloads only the pieces
still in flight, not a batch of up-to-8 already-finished ones. Drops the now
-unused 'unpersisted' counter.
UI: the "parallel download connections" control is now a stepper — two large
54px -/+ buttons around a tabular value, in a row beside its label, instead of
a full-width number field for a 1-16 value. Big targets and a clear green focus
ring suit controller navigation (the UI is driven by the PS5 pad via the home
tile). Tapping -/+ updates and auto-saves that field alone (debounced), applying
live on the server.
Split the single morphing button into a green/amber play-pause (Update →
Pause → Resume) and a red stop (Cancel). Pause aborts the download but
keeps the partial (resumable); Cancel aborts and deletes. Backend gets a
separate pause flag distinct from cancel.
Resume now continues WITHIN a part: the partially-written piece is fetched
from its last byte via an HTTP byte range (with a safe fall back to
re-fetching the whole piece if the CDN ignores the range), instead of
re-downloading the whole part. A title is resumable as soon as any bytes
are on disk.
Version bumped to 0.0.3 (no release tagged).
Replace the single-page layout with a view-based one (Games / Settings /
Logs via the left nav) and fold all download UI into the game tile — the
separate download queue is gone.
Per tile while downloading: an in-tile progress bar with auto-scaled size
(B/KB/MB/GB/TB), live transfer speed and ETA from the poll deltas, and a
green "Downloading" marker. One fixed-width action button that no longer
reflows with its label: a blue Update/Download/Install that morphs into an
amber Cancel while the download runs, plus a ghost Delete for a finished
package.
Settings and Logs moved to their own pages. Mobile-first responsive layout
(rail collapses to a top bar, icon-only nav, single-column tiles, >=44px
touch targets, 16px inputs). Real free space (statvfs) is shown auto-scaled
in the rail and status strip.
New "Home-screen shortcut" toggle (default on, persisted as home_shortcut
in config.json). The actual PS5 tile install is not wired yet: it needs a
prebuilt deeplinkUri stub PKG installed via sceAppInstUtil.
Fixes from an adversarial review pass:
- Reconcile in-flight downloads from /api/downloads onto the cards, so
progress + Cancel appear after a reload or a download started elsewhere,
and stop the per-poll full-grid rebuild.
- Clear the downloaded flag on install and once the server reports the
title up to date, so a patched title no longer shows Install/Delete
forever.
- Zero a stale speed/ETA if the byte counter goes backwards.
- a11y: nav buttons keep an accessible name when the label is hidden on
small screens; visible focus ring on the search box; filter group is
role=group with aria-pressed; drop the noisy grid-level aria-live; fold
the transient "checking" state into a visible filter bucket.
Cancel a running download (the worker aborts mid-piece and the partial
file is removed) or delete a finished package, from the queue or the
title card. The progress callback now returns an abort signal that
reaches libcurl and the manifest merge loop.
Manifest merge: bound the piece scan to the "pieces" array so a later
"url" key (e.g. playgoChunkCrcUrl) can't be appended as a bogus piece,
and require each piece's fileOffset to match the bytes written so far so
an out-of-order manifest fails instead of silently producing a corrupt
package.
Report real free space on the download partition via statvfs; it was a
hardcoded 0.
Fixes found in review:
- scan: bound the SFO entry table to the bytes actually read and require
the key to be NUL-terminated before strcmp (OOB read on a crafted
param.sfo from a shadow-mounted dir).
- proc: bound the kinfo_proc walk and the name compare to the record and
the buffer.
- install: publish the API probe under the lock (data race with the MHD
worker thread) and initialize rc2.
- verxml: reject a truncated attribute value instead of returning it as
valid.
- web: keep download/install/downloaded flags across a refresh, stop the
queue poll only after repeated empty results, coerce the progress
number, and treat a cancelled download (HTTP 200, ok:false) as
not-downloaded.