docs: align project contracts and retire unused stats

This commit is contained in:
najdek committed 2026-10-13 11:28:47 +02:00
1 parent 23b4a24407
commit a5a1760070
8 files changed
+180 -180

No files matched your search

+33 -5
View File
@@ -12,7 +12,7 @@ To create a repository, you must host a JSON file that conforms to the following
1. Both formats are supported:
- Object wrapper with top-level `"name"` and `"payloads"` array (recommended).
- Bare array of payload objects `[ { ... }, { ... } ]`.
2. When using the object wrapper, the `"name"` field **must appear before** the `"payloads"` field in the JSON structure.
2. In the object wrapper, `"name"` is recommended for a readable catalog label. Field order does not matter; if the label is absent/empty, the source host is used.
3. The `"payloads"` field must be an array of payload objects.
### Example JSON:
@@ -49,7 +49,7 @@ To create a repository, you must host a JSON file that conforms to the following
| Field | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **`name`** (top-level) | String | Yes | The display name of your repository catalog in the user interface (e.g. `"My Custom Payloads"`). |
| **`name`** (top-level) | String | Recommended | The display name of your repository catalog in the user interface (e.g. `"My Custom Payloads"`). |
| **`payloads`** (top-level) | Array | Yes | List of payload items available in this repository. |
| **`id`** (item-level) | String | Recommended | Stable identifier, unique within the repository, unchanged across versions (`[a-z0-9._-]`). Defaults to `slug(name)`. Stable IDs allow slots to maintain favorites and autoload entries across updates. |
| **`name`** (item-level) | String | Yes | Human-readable name of the specific payload. |
@@ -59,11 +59,11 @@ To create a repository, you must host a JSON file that conforms to the following
| **`last_update`** (item-level) | String | No | ISO 8601 date (e.g. `"2026-10-11"`). Serves as a tie-breaker when version strings compare equal. |
| **`description`** (item-level) | String | No | A description of what the payload does, displayed in the dashboard. |
| **`category`** (item-level) | String | No | The category name used to group payloads in the frontend (e.g. `"Networking"`). Defaults to `"Uncategorized"`. |
| **`checksum`** (item-level) | String | No | **SHA-256** hash (64 hex characters) of the file. If provided, the manager automatically validates the downloaded file against it before installation. If versions are identical but checksum differs, it is flagged as an update. |
| **`checksum`** (item-level) | String | No | **SHA-256** hash: exactly 64 hexadecimal characters when nonempty. Missing/empty remains optional; malformed supplied values reject the entry. Every download attempt, including retries, is checked when a hash is supplied. If versions are identical but checksum differs, it is flagged as an update. |
| **`author`** (item-level) | String | No | Author or creator of the payload. |
| **`icon`** (item-level) | String | No | Inline base64 data URI icon (e.g. `data:image/svg+xml;base64,...` or `data:image/png;base64,...`). See [Payload Icons](#payload-icons) below. |
| **`details_url`** (item-level) | String | No | URL to documentation or README markdown for the payload. |
| **`release_notes`** (item-level) | String | No | Markdown release notes for this version (truncated at 2000 characters by backend). |
| **`release_notes`** (item-level) | String | No | Markdown release notes for this version. The backend field holds up to 2047 UTF-8 bytes, with UTF-8-safe truncation and an ellipsis when exceeded. This is a byte budget, not 2000 Unicode characters. |
| **`source`** (item-level) | String | No | Link to the upstream source repository or releases page. |
---
@@ -94,11 +94,39 @@ Payload Manager implements a strict version comparison algorithm:
## Hosting Your Repository
1. **Accessibility**: Upload your JSON file and your payload binaries to any static hosting provider (e.g., GitHub Pages, Vercel, Netlify, or your own VPS). The PS5 must be able to resolve and reach the host IP.
2. **CORS Headers**: To ensure that the web interface can interact with the files from the browser, your server should respond with CORS headers:
2. **CORS Headers**: Catalog and binary downloads happen in the PS5 daemon and do not require browser CORS. The frontend fetches README/details content directly, so that content's host must allow cross-origin browser GETs when it is on another origin. For public documentation, for example:
```http
Access-Control-Allow-Origin: *
```
## Integrity, Storage and Markdown
HTTP and HTTPS custom sources remain supported. HTTPS validates the certificate
and hostname and permits only HTTPS redirects; certificate failures do not retry
with verification disabled. A supplied hash checks file integrity, not the
identity or safety of its author: a manifest publisher controls both the binary
URL and its hash. Use sources you trust; mandatory hashes or LAN authentication
are separate policies that are not enabled here.
Catalog downloads are bounded at 16 MiB; payload downloads at 512 MiB.
Collections also have 4,096-item and 64 MiB allocation limits. Required string
fields must fit their backend buffers. A failed/over-limit serialized response
returns an error instead of a broken partial JSON document.
Managed installations use slots under `/data/pldmgr/payloads/repo/`;
uploads/USB imports use `/data/pldmgr/payloads/local/`. Payload and metadata
publication uses staging/recovery so a failed install does not silently replace
a valid previous version. Favorites/autoload use stable slot keys. The manager
filters its own payload from listings and blocks self-install/self-launch;
replace its ELF via the autoloader or standalone loader to update it.
README and release-note markdown preserves safe links, code, headings and tables
through DOM sanitization. Arbitrary images, remote media, styles and scripts are
excluded even if they use absolute URLs. Emoji use bundled local artwork.
Loaded content and browser-cached README responses can remain readable without
WAN; uncached remote documentation still requires a connection. Repository icons
are a separate data-URI feature, not an exception for raw markdown images.
---
## Adding the Source in Payload Manager
+70 -10
View File
@@ -13,20 +13,36 @@ make frontend-build
### 2. Build the SDK Docker Image
If you haven't already, build the SDK environment:
```bash
docker build -t ps5-payload-sdk -f Dockerfile.sdk .
docker build -t ps5-payload-sdk-pldmgr -f Dockerfile.sdk .
```
### 3. Build the ELF
Use the Docker container to compile the Payload Manager. It is recommended to run `make clean` if you updated the frontend.
Use the SDK container after building the frontend. Preserve your working ELF
and build a candidate at a new path; this regenerates changed embedded assets
without deleting previous candidates.
```bash
docker run --rm -v $(pwd):/src -w /src ps5-payload-sdk make clean all
mkdir -p scratch
docker run --rm --network=none -v "$(pwd):/src" -w /src \
ps5-payload-sdk-pldmgr make ELF=scratch/pldmgr-candidate.elf
```
The resulting `pldmgr.elf` will be created in the root directory.
The resulting candidate is `scratch/pldmgr-candidate.elf`. The SDK image and
embedded CA header must already exist for this offline compilation. The ELF
target does not track ordinary header dependencies; use a fresh candidate name
when changing headers alone. A normal release build with `make all` produces
`pldmgr.elf` at the repository root.
## Automated Deploy
For a quick build & deploy cycle, use the `deploy.sh` script. It will automatically call `/shutdown` on the PS5, run the docker build, and send the new ELF via `socat`.
For a build-and-send cycle, `deploy.sh` builds the frontend, runs
`make clean all` in the existing `ps5-payload-sdk-pldmgr` image, and sends the
root `pldmgr.elf` to port 9021 using `socat`. It does **not** call `/shutdown`,
build the SDK image, or verify successful startup on the console. If replacing a
running instance, stop it explicitly with an authorized POST first; keep a
working ELF outside the root output path because the script cleans that output.
That clean also removes the embedded CA header, whose regeneration downloads the
CA bundle; use the candidate build above with an existing header for offline
compilation.
```bash
./deploy.sh [PS5_IP]
@@ -34,7 +50,8 @@ For a quick build & deploy cycle, use the `deploy.sh` script. It will automatica
(Requires PS5 IP as the first argument).
## Manual Deploy
1. Upload `pldmgr.elf` to your PS5 (e.g., via port 9021).
1. Load the built ELF using your autoloader, or send it to a separate running
ELF loader on port 9021. Payload Manager does not provide that loader itself.
2. The menu will be available at `http://[PS5_IP]:8084`.
## Automated Verification
@@ -84,15 +101,14 @@ UTF-8-safe prefixes. The backend suite uses real repository, source save/load,
and slot metadata read/write code inside temporary storage. It parses their
results independently with Python. Unused network/PS5 code is discarded at
link time, and no download or payload launch occurs. `make test-host` runs all
eight host suites with sanitizers.
host helper suites and actual-HTTP scenarios listed in the Makefile, with sanitizers.
The serializer suite exercises actual history/log/payload/process/repository
output at small and large capacities, sixfold escape expansion, failed format
calls, and metadata/source preservation on serialization failure. Repository
fixtures use fresh temporary caches and a recording download function that
aborts if called. Process records use a host fixture; this checks serialization,
not the PS5 kernel ABI. The existing unused SSE `pos` parameter produces a host
`-Wextra` warning; the native build has no new warnings.
not the PS5 kernel ABI. Current native builds pass without warnings.
The allocation suite executes actual repository parsing, managed-slot discovery,
cached-source serialization, and all USB discovery branches. It faults every
@@ -254,7 +270,7 @@ build a candidate under `scratch/` rather than run `make clean`:
```bash
mkdir -p scratch
docker run --rm --network=none -v "$(pwd):/src" -w /src \
ps5-payload-sdk-pldmgr make ELF=scratch/pldmgr-step8b-emoji-compact.elf
ps5-payload-sdk-pldmgr make ELF=scratch/pldmgr-candidate.elf
```
This command assumes the SDK image, frontend distribution, and embedded CA
@@ -293,3 +309,47 @@ node tests/run_markdown_locale_browser_tests.mjs --baseline
The baseline uses the previous committed renderer without modifying source files,
reproducing the CDN dependency and unavailable-DOM raw HTML fallback. The test
blocks external requests; it runs against a local Vite fixture and Chrome.
## HTTP Access Contract
The native daemon serves the UI/API on HTTP port **8084**. It sends approved
payloads to a separate ELF loader on port **9021**; it does not provide that
loader or a login/pairing service. The mock API uses loopback **8081**, and Vite
normally uses **5173** with its same-origin proxy.
| Request class | Method and behavior |
| --- | --- |
| Read APIs | GET: `/list_payloads`, `/get_config`, `/processes_list`, `/history_list`, `/repository_payloads`, `/sources_list`, `/autoload_status`, `/log`, `/events`, `/version`, `/getip`, `/manage:check`, `/usb_move_check`. Ordinary repository reads may refresh their cache. Anonymous/foreign autoload status reads do not signal frontend readiness. |
| Explicit mutations | POST: `/manage:upload`, `/manage:delete`, `/loadpayload:<slot-or-path>`, `/set_config`, `/abort`, `/autoload_clear`, `/repository_refresh`, `/repository_install`, `/sources_set`, `/sources_add`, `/sources_remove`, `/usb_move_perform`, `/process_kill`, `/shutdown`. GET does not perform these actions. |
| Preflight | OPTIONS is validated against the requesting origin, route/method and supported headers. Foreign origins do not gain mutation access. |
| Removed updater compatibility | GET `/update_check` returns `{"status":"none"}`. It does not check, download or apply updates. |
A mutation requires an Origin matching the HTTP Host, including port; when
Origin is absent, a matching Referer suffices. Only when **both** are absent can
`X-PLDMGR-Request: 1` authorize it. An explicit foreign Origin or Referer is not
overridden by that marker, and `Sec-Fetch-Site: cross-site` is rejected. Thus an
older same-origin browser window can still make a valid POST without the marker.
The frontend helper supplies the marker for normal actions; same-origin exit
beacons can use the Origin/Referer path.
These are browser CSRF checks, not a secret credential, LAN authentication or
DNS-rebinding protection. A terminal script/direct LAN application can provide
its own headers and perform actions. Read responses are not access-controlled
by a login either. Use a trusted local network; pairing/authentication remains
separate feature work. Do not bypass rejected HTTPS certificate checks or add
wildcard CORS to native mutations as a workaround.
Configuration and lists live in `/data/pldmgr/`: `pldmgr_config.txt`,
`autoload.txt`, `favorites.txt`, `sources.json` and repository caches. Managed
payloads use `/data/pldmgr/payloads/repo/` and `/data/pldmgr/payloads/local/` slots.
USB libraries include `/mnt/usb0/pldmgr` through `/mnt/usb7/pldmgr`; optional root
USB scanning adds `.elf`/`.bin` files on those drives. Launches require supported
regular files within canonical approved roots, and block the manager itself.
Imported/repository slot data is published with staging/recovery; configuration
remaps and multi-file settings updates have the limits described above.
## Hardware Verification
Automated host/browser checks use stubs and local transports. They do not prove
PS5 kernel ABI, native TLS behavior, fonts/layout, rest/resume or the external
loader. Verify these on the target console after runtime changes.
+21 -2
View File
@@ -15,7 +15,8 @@
- **Import Payloads**: Easily add new payloads from a USB drive or download them from the cloud repository.
- **Automated Startup**: Set up a list of payloads to load automatically whenever you start the manager.
- **Home Screen Shortcut**: Installs a dedicated "Payload Manager" app icon to your PS5 home screen for quick access.
- **Auto-Close Disc Player**: Optional setting to automatically close the Disc Player on startup (useful for BD-JB users).
- **Favorites and Process Management**: Keep favorite payloads at hand, inspect active processes, and terminate eligible processes.
- **Offline Emoji**: Bundled emoji artwork keeps release notes and loaded README content readable without CDN access. Uncached remote README files still need connectivity.
## Installation
@@ -37,12 +38,30 @@ You can also manually load the manager like any other `.elf` file. Grab the late
## Custom Repositories
You can add third-party payload repositories to the manager. To learn how to create your own repository JSON and host it, see the [Custom Repositories Guide](CUSTOM_REPOSITORIES.md).
## Access and Updates
The dashboard runs at `http://[PS5_IP]:8084`; a separate ELF loader on port
`9021` receives payloads. Settings, lists, repository caches and managed payloads
live under `/data/pldmgr`. USB payloads can also run in place; import them to
internal storage for autoload.
Use the manager on a trusted local network. Browser-origin checks protect
actions from foreign websites, but there is no login or pairing: a direct LAN
client can still upload, launch and manage payloads. The full
[HTTP contract](DEVELOPMENT.md#http-access-contract) explains the boundary.
Update Payload Manager by replacing/loading its ELF through your autoloader or
standalone loader. In-app self-update was deliberately removed; the manager
cannot install or launch another copy of itself. Disc Player auto-close is also
absent from the current settings.
## Translations
PS5 Payload Manager supports multiple languages. For the list of supported languages, how to report translation issues or suggest improvements, and past community translators, see [TRANSLATIONS.md](TRANSLATIONS.md).
## Credits
- [John Törnblom](https://github.com/john-tornblom) - for the [shell UI installer](https://github.com/ps5-payload-dev/ftpsrv/blob/master/install-ps5.c) and various payloads used as reference.
- [BenNoxXD](https://github.com/BenNoxXD) - for the [Disc Player App termination logic](https://github.com/BenNoxXD/PS5-BDJ-HEN-loader/blob/main/HENloader_C_part/src/kill_disc_player.c).
- [BenNoxXD](https://github.com/BenNoxXD) - for the [Disc Player termination logic](https://github.com/BenNoxXD/PS5-BDJ-HEN-loader/blob/main/HENloader_C_part/src/kill_disc_player.c) used in earlier versions.
- Twitter, Inc. and contributors - for the bundled Twemoji artwork; [source, license and modifications](frontend/src/assets/emoji/README.md).
- [owendswang](https://github.com/owendswang) - for [contributions](https://github.com/itsPLK/ps5-payload-manager/commits/main/?author=owendswang)
- Everyone else contributing to the PS5 homebrew scene.
+21 -1
View File
@@ -2,7 +2,27 @@
PS5 Payload Manager supports multiple languages to provide an accessible and intuitive experience for users worldwide.
Starting with **version 0.6.0**, new strings and translations are generated and maintained using **Google Gemini AI**.
Translation files live in `frontend/src/locales/`. Some strings have been
prepared with AI assistance; community corrections remain welcome. Structural
checks do not certify that wording is natural or correct. All 17 bundled locales
are available, including Dutch; there is no percentage-based language-hiding rule.
## Maintaining Translations
Use stable keys from the active UI and preserve `{{interpolation}}` placeholders
and React translation markup such as `<1>...</1>` or `<strong>...</strong>`.
Provide each language's cardinal forms (`_one`, `_few`, `_many`, `_other`, etc.)
for count labels. Product names and technical terms can legitimately match English.
Removed features such as self-update and Disc Player auto-close must not regain
translation keys.
From `frontend/`, `npm run extract` updates the **English** catalog only. Review
its diff before keeping it, especially dynamic categories and plural entries;
it does not translate other locales. Update those locales explicitly, then run
`npm test` and `npm run build`. The locale suite checks active source-key coverage,
placeholders, markup, required plural forms and obsolete entries. Browser checks
include Dutch selection, reload persistence, fallback and representative locales.
Native-speaker wording review and console layout testing remain useful.
---
+6 -4
View File
@@ -11,10 +11,12 @@ A modern, web-based dashboard to easily manage, import, and automatically load p
## Usage
1. Add this repository as a source in Payload Manager.
2. Open the app shop and find the payload.
3. Tap **Install** (or **Update** if a newer version exists).
Load Payload Manager with your autoloader or standalone ELF loader. To update,
replace the manager ELF through that workflow. The current manager filters itself
from the app shop and blocks self-install/self-launch; there is no in-app updater.
## Notes
This is a generated test README served by the local `details/` folder for development. Images referenced here must use absolute URLs.
This is a generated development fixture served by the local `details/` folder.
Its version/date above are sample metadata, not the running daemon version.
Markdown images are excluded by the renderer even when their URLs are absolute.
+29 -9
View File
@@ -1,16 +1,36 @@
# React + Vite
# Payload Manager frontend
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
The React dashboard is bundled into a single HTML file embedded in the native
PS5 daemon. Vite targets `es2015` and Safari 11. Build/deployment and the HTTP
security contract are documented in [DEVELOPMENT.md](../DEVELOPMENT.md).
Currently, two official plugins are available:
Run these commands from `frontend/`:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
```bash
npm ci
npm run dev:mock
```
## React Compiler
The mock API listens on loopback port 8081; Vite prints the UI address (normally
5173). `npm run dev` alone expects an API on 8081 via Vite's proxy. The native
PS5 dashboard uses port 8084. Mock process/storage/loader responses are fixtures;
they do not prove that hardware services work.
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
```bash
npm run build
npm run test:browser
```
## Expanding the ESLint configuration
`build` runs zero-warning lint and Node tests before Vite, then verifies the
production emoji PNG is embedded exactly once, attribution remains, and the old
CDN dependency is absent. `test:browser` requires local Chrome (`CHROME_BIN` may
override its path) and exercises actual editor reloads, StrictMode/save queues,
markdown DOM sanitization, offline rendering and language selection. The separate
mutation-browser/native-fixture checks and host sanitizers run in CI; see the
root development guide for their commands and limits.
If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
Localization instructions are in [TRANSLATIONS.md](../TRANSLATIONS.md).
Markdown uses bundled PNG sprites because native emoji fonts failed on PS5.
Sprite position rules are added only when needed, and both sanitizer passes
exclude arbitrary remote media. Asset regeneration is optional; ordinary builds
use the bundled files and need no graphics download or Pillow.
-11
View File
@@ -1,11 +0,0 @@
#pragma once
#include <stddef.h>
/**
* Get system statistics as a JSON string.
* @param buf The buffer to write to.
* @param size The size of the buffer.
* @return The number of bytes written.
*/
size_t pldmgr_get_system_stats_json(char *buf, size_t size);
-138
View File
@@ -1,138 +0,0 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/sysctl.h>
#include <sys/mount.h>
#include <sys/vmmeter.h>
#include "stats.h"
#include "pldmgr.h"
/* PS5 Kernel Exports */
extern int sceKernelGetCpuTemperature(int *temp);
/* CPU Usage Global State */
static long prev_cp_time[5] = {0};
static float get_cpu_usage() {
long cp_time[5];
size_t size = sizeof(cp_time);
// CTL_KERN = 1, KERN_CP_TIME = 40
int mib[2] = {1, 40};
if (sysctl(mib, 2, &cp_time, &size, NULL, 0) < 0) {
// Fallback to string name if numeric fails
if (sysctlbyname("kern.cp_time", &cp_time, &size, NULL, 0) < 0) {
return 0.0f;
}
}
long total_diff = 0;
for (int i = 0; i < 5; i++) {
total_diff += (cp_time[i] - prev_cp_time[i]);
}
if (total_diff <= 0) {
memcpy(prev_cp_time, cp_time, sizeof(cp_time));
return 0.0f;
}
long idle_diff = cp_time[4] - prev_cp_time[4]; // CP_IDLE = 4
float usage = 100.0f * (1.0f - ((float)idle_diff / (float)total_diff));
memcpy(prev_cp_time, cp_time, sizeof(cp_time));
return usage < 0 ? 0 : usage;
}
size_t pldmgr_get_system_stats_json(char *buf, size_t size) {
/* CPU Info */
float cpu_usage = get_cpu_usage();
int cpu_temp = 0;
sceKernelGetCpuTemperature(&cpu_temp);
/* RAM Info */
struct vmtotal vmt;
size_t vmt_size = sizeof(vmt);
uint64_t ram_total = 0;
size_t ram_total_size = sizeof(ram_total);
// hw.physmem: CTL_HW = 6, HW_PHYSMEM = 6
int mib_phys[2] = {6, 6};
if (sysctl(mib_phys, 2, &ram_total, &ram_total_size, NULL, 0) < 0) {
if (sysctlbyname("hw.physmem", &ram_total, &ram_total_size, NULL, 0) < 0) {
ram_total = 16 * 1024 * 1024 * 1024ULL; // Fallback 16GB
}
}
uint64_t ram_used = 0;
// vm.vmtotal: CTL_VM = 2, VM_METER = 1
int mib_vmt[2] = {2, 1};
if (sysctl(mib_vmt, 2, &vmt, &vmt_size, NULL, 0) == 0 || sysctlbyname("vm.vmtotal", &vmt, &vmt_size, NULL, 0) == 0) {
unsigned int page_size = getpagesize();
if (page_size == 0) page_size = 4096;
// t_rm is total real memory in use (pages)
ram_used = (uint64_t)vmt.t_rm * page_size;
// If vmt.t_rm seems off, fallback to free pages count
if (ram_used == 0 || ram_used > ram_total) {
unsigned int free_pages = 0;
size_t free_pages_size = sizeof(free_pages);
if (sysctlbyname("vm.stats.vm.v_free_count", &free_pages, &free_pages_size, NULL, 0) == 0) {
ram_used = ram_total - ((uint64_t)free_pages * page_size);
}
}
}
/* Storage Info */
struct statfs *mounts;
int num_fs = getfsstat(NULL, 0, MNT_NOWAIT);
char storage_json[4096] = "[";
size_t storage_pos = 1;
if (num_fs > 0 && (mounts = malloc(num_fs * sizeof(struct statfs)))) {
num_fs = getfsstat(mounts, num_fs * sizeof(struct statfs), MNT_NOWAIT);
int entry_count = 0;
for (int i = 0; i < num_fs && entry_count < 10; i++) {
const char *label = NULL;
const char *path = mounts[i].f_mntonname;
if (strcmp(path, "/data") == 0) {
label = "Internal storage";
} else if (strncmp(path, "/mnt/ext", 8) == 0) {
label = "NVME SSD";
} else if (strncmp(path, "/mnt/usb", 8) == 0) {
label = "External storage";
} else if (strncmp(path, "/mnt/", 5) == 0) {
// Any other mount in /mnt that wasn't caught above
label = "System mount";
}
if (label) {
uint64_t total = (uint64_t)mounts[i].f_blocks * mounts[i].f_bsize;
uint64_t avail = (uint64_t)mounts[i].f_bavail * mounts[i].f_bsize;
uint64_t used = total - avail;
if (total == 0) continue; // Skip empty/invalid mounts
if (entry_count > 0) {
storage_json[storage_pos++] = ',';
}
storage_pos += snprintf(storage_json + storage_pos, sizeof(storage_json) - storage_pos,
"{\"label\":\"%s\",\"total\":%lu,\"used\":%lu,\"path\":\"%s\"}",
label, total, used, path);
entry_count++;
}
}
free(mounts);
}
strcat(storage_json, "]");
return snprintf(buf, size,
"{\"cpu\":{\"usage\":%.1f,\"temp\":%d},"
"\"ram\":{\"total\":%lu,\"used\":%lu},"
"\"storage\":%s}",
cpu_usage, cpu_temp, ram_total, ram_used, storage_json);
}