Skip to content

refactor(amuleapi): self-explanatory /preferences names, one schema table, Web UI label CI gate (#655) - #696

Merged
got3nks merged 4 commits into
amule-org:masterfrom
got3nks:feat/api-prefs-field-naming
Jul 29, 2026
Merged

refactor(amuleapi): self-explanatory /preferences names, one schema table, Web UI label CI gate (#655)#696
got3nks merged 4 commits into
amule-org:masterfrom
got3nks:feat/api-prefs-field-naming

Conversation

@got3nks

@got3nks got3nks commented Jul 29, 2026

Copy link
Copy Markdown

Closes #655.

Acts on ngosang's field-by-field review of GET /api/v0/preferences: 46 of the 119 fields change name or position so that each one states what it does without a lookup table, and the three code paths that describe those fields collapse into one declarative table. 73 fields were already fine and are untouched.

Since amuleapi has not shipped and the protocol stays at v0, the old names are removed outright rather than dual-emitted. There is no compatibility shim to carry or remove later.

The EC protocol is unchanged. Every tag number, presence-vs-value encoding and thePrefs:: accessor stays exactly as it was, so amulegui, amulecmd and amuleweb are unaffected. All of the translation happens at the webapi boundary, the same way extended_udp_port_enabled already inverted EC_TAG_CONN_UDP_DISABLE before this PR.

Three commits

1. Field names. Renames grouped by the problem each one fixes, following the issue's rules: positive names only, units in the name, no abbreviations.

  • Actively misleading: security.obfuscation_supportedobfuscation_enabled (it is a writable preference, whereas _supported / _available elsewhere in the payload means a read-only build capability, so a client filtering on that suffix would silently drop it); message_filter.friends / .secure / .allaccept_from_friends_only / accept_from_known_clients_only / filter_all_messages; directories.exclude_regexexclude_patterns_use_regex, which is a modifier on exclude_patterns rather than a second exclusion list.
  • Missing units: core_tweaks.srv_keepalive_timeoutserver_keepalive_timeout_ms (its two siblings in the same object already said _ms), plus upload_slot_kbps, file_buffer_bytes, update_frequency_seconds, refresh_seconds.
  • Magic numbers: connection.proxy_type and security.can_see_shares (now shared_files_visibility) become enum strings, so no client needs to hardcode the integer tables the Web UI used to carry.
  • Structure: remote_controls.{webserver,amuleapi}.{...} replaces nine webserver_* / amuleapi_* prefixes. Both sub-objects still pack into the single EC_TAG_PREFS_REMOTECTRL group — the nesting is an API shape, not an EC one.

The Web UI moves in the same commit: field keys, the derived prefs_field_* dictionary entries in both locales, and dotted category paths for the nested objects. Two labels copied verbatim from the desktop dialog are reworded ("Slot Allocation" → "Upload speed per slot (KB/s)"), and the keepalive field now scales to minutes like the desktop slider instead of rendering a bare millisecond count.

2. One schema table. The 119 fields were described three times in three different idioms — a hand-written EC decode per category, a w.Key/w.Value pair per field, and a PATCH applier that had grown two parallel helper families (87 call sites on the generic PrefTake* helpers plus 22 more on connection-local lambdas doing the same job with their own error strings and a different return convention). A field touched in two of the three and missed in the third compiled cleanly. That is the failure mode this rename pass would have been most exposed to.

All three now walk one table in src/webapi/PrefsSchema.cpp. Adding a preference is one row; renaming one is one token. What used to be special-case code became a column: invert, the presence-vs-value encoding, read-only/write-only/rejected access, the capability gate behind files.mmap_enabled's 409, and the one field whose EC tag lives in a different group than its JSON category implies (connection.upnp_available, serialized into [General]). Rows carry the address of their backing member and the macros static_assert that the declared type matches the member's real C++ type, so a mis-typed row fails the build rather than misbehaving at runtime.

Net −1655 / +1469 lines across the PR, with the preferences handling in Api.cpp down 912 lines and Refresher.cpp down 212.

One field genuinely cannot be described by the table and stays hand-written: remote_controls.webserver.guest_enabled and .guest_password share a single EC tag, which carries the enable bool as its value and the password hash as a child, so there is no 1:1 field-to-tag mapping to record. The schema marks it Bespoke and its GET emission is still table-driven.

3. The sparse-file flag. files.create_normalfiles.create_sparse_files, with its meaning inverted — the one field in the payload whose positive name could not be reached by renaming alone.

Every layer except EC already spoke in terms of "sparse": the config key is /eMule/CreateSparseFiles, the core stores s_createFilesSparse (default on), PartFile.cpp reads CreateFilesSparse(), and the desktop checkbox added in #662 is labelled "Create new files as sparse files". Only the EC tag is named for the negative case, and amuleapi was the one consumer republishing that name outward — which is why the Web UI said "non-sparse" where the desktop said "sparse" for the same setting. Doing this after the table meant it was one column rather than two structurally different edits on the read and write paths.

Verification

Equivalence was checked against a GET /preferences captured from the pre-refactor build: the EC decode and the GET emitter each produce byte-identical output, and the final key set matches at 119/119.

The polarity flip was verified against the daemon's own amule.conf rather than the API's echo, since that is precisely where a self-consistent API can still be wrong:

Starting config Action Result
CreateSparseFiles=1 GET create_sparse_files: true
PATCH false CreateSparseFiles=0
CreateSparseFiles=0 GET create_sparse_files: false
PATCH true CreateSparseFiles=1

Also: full macOS build clean with no new warnings; ctest 28/28; curl phase 15 (preferences-patch) 117/117 including new coverage for the nested remote_controls shape, both enums and every error path; phases 05 and 20 pass all /preferences assertions. clang-format 18, matching CI.

Two new unit tests pin the table's own invariants — no duplicate fields, every category resolving to an EC group, backing members present exactly where a value round-trips, enum rows carrying a name table, capability gates naming a real sibling bool, and the emitted count holding at the documented 119. A second test enumerates the deliberate irregularities, so a new one has to be added consciously; it caught commit 3 on its own and failed until the inverting fields were listed explicitly.

check-i18n.mjs gains a check that every preferences field resolves to a real label, and is wired into the i18n workflow. Those keys are derived from the field names, so before this a renamed field with no dictionary entry would have rendered the raw key with nothing failing anywhere. It is verified to catch a deliberately broken key.

Notes for review

  • Breaking for any client built against the pre-release shape. files.create_sparse_files is the one field where renaming the key without flipping the value produces the opposite behaviour.
  • Error bodies are now uniform ("endgame_enabled must be a bool") where they used to be per-category ("connection field must be a bool"). Nothing asserted the old wording.
  • JSON key order within each category changed, since emission follows the table. Object key order is not significant, but it does change the ETag once.
  • The Web UI's sparse checkbox now renders checked by default where it rendered unchecked, for an unchanged underlying setting.

Two judgement calls where the issue offered options: I kept the ipfilter_ prefix on all six security fields rather than dropping it from two of them, since the issue's own suggestions in A8 and C5 retain it and dropping it would leave security.update_url not saying what it updates; and security.paranoid_filtering became reject_spoofed_source_ips to say what the setting does rather than name the mood.

Found but deliberately not fixed here

Three pre-existing issues surfaced while working through this; none is caused by this PR and each would muddy it. Ready to open separate issues if wanted.

  • The Web UI exposes an amuleapi password field under Remote Controls that the backend rejects with 400, since those credentials belong to PATCH /auth/passwords. Predates this work; behaviour is preserved exactly.
  • kad.buddy.status is asserted against an enum including "unknown", but the decoder leaves it empty when Kad is not running, so the assertion only holds on a connected node.
  • core_tweaks.file_buffer_bytes silently quantizes to 15000-byte steps, because the core stores the value divided by 15000. PATCH 250000 and it reads back 240000. Worth documenting in the API reference.

got3nks added 3 commits July 29, 2026 12:27
…mule-project#655)

Rename 45 of the 119 fields GET/PATCH /api/v0/preferences exposes so each
one states what it does without a lookup table, and nest the two unrelated
remote-control subsystems instead of prefixing every field. amuleapi has
not shipped and the protocol stays at v0, so the old names are simply
removed rather than dual-emitted.

The EC protocol is untouched: every tag number, presence-vs-value encoding
and thePrefs:: accessor stays as-is, so amulegui / amulecmd / amuleweb
remain compatible. All translation happens at the webapi boundary, the
same way extended_udp_port_enabled already inverts EC_TAG_CONN_UDP_DISABLE.

Highlights, by the class of problem each fixes:

- Misleading names: security.obfuscation_supported -> obfuscation_enabled
  (it is a writable preference, while _supported / _available elsewhere
  means a read-only build capability); message_filter.friends / .secure /
  .all -> accept_from_friends_only / accept_from_known_clients_only /
  filter_all_messages; directories.exclude_regex ->
  exclude_patterns_use_regex (a modifier on exclude_patterns, not a second
  list); files.preview_prio -> prioritize_first_last_chunks.
- Missing units: core_tweaks.srv_keepalive_timeout ->
  server_keepalive_timeout_ms (its two siblings already said _ms),
  connection.slot_allocation -> upload_slot_kbps, filebuffer ->
  file_buffer_bytes, online_signature.update_frequency ->
  update_frequency_seconds, webserver refresh -> refresh_seconds.
- Magic numbers: connection.proxy_type and security.can_see_shares (now
  shared_files_visibility) become enum strings. A new PrefTakeEnum helper
  maps them to the wire ints and also replaces the hand-rolled inline
  mapping ip2country.source was using.
- Structure: remote_controls.{webserver,amuleapi}.{...} replaces the nine
  webserver_* / amuleapi_* prefixes. Both sub-objects still pack into the
  single EC_TAG_PREFS_REMOTECTRL group, and passwords stay write-only.
- Abbreviations spelled out: dl / ul / prio / cat / srv / autoconn.

files.create_normal is deliberately left alone. It is the one field whose
positive name requires inverting its meaning, so it gets its own change.

The Web UI moves with the API in the same commit: field keys, the derived
prefs_field_* dictionary keys in both locales, and dotted category paths
for the nested objects. Two labels copied verbatim from the desktop are
reworded ("Slot Allocation" -> "Upload speed per slot (KB/s)"), and the
keepalive field now scales to minutes like the desktop slider instead of
showing a bare millisecond count.

check-i18n.mjs gains a check that every preferences field resolves to a
real label, and is wired into the i18n workflow. These label keys are
derived from the field names, so before this a renamed field with no
dictionary entry would have rendered the raw key with nothing failing.

Verified: full macOS build clean, 28/28 ctest green, curl phases 05 (73)
and 15 (117 incl. new nested remote_controls and enum coverage) pass
against a live daemon. Documented and emitted key sets diffed to zero.
…le (amule-project#655)

The 119 fields on /api/v0/preferences were described three times, in three
different idioms: a hand-written EC decode per category in Refresher, a
w.Key/w.Value pair per field in the GET emitter, and a PATCH applier that
had grown two parallel helper families -- 87 call sites on the generic
PrefTake* helpers plus 22 more on connection-local lambdas doing the same
job with their own error strings and a different return convention. A field
touched in two of the three and missed in the third compiled cleanly.

Replace all three with a walk over one table (PrefsSchema.{h,cpp}). Each row
names a field's JSON category and key, its EC tag, its type, how EC encodes
it, and whether it is readable, writable or neither. Adding a preference is
one row; renaming one is one token.

What used to be special-case code is now a column:

- `invert` -- extended_udp_port_enabled, whose EC tag (EC_TAG_CONN_UDP_DISABLE)
  is negatively named. This is also what makes the deferred create_normal ->
  create_sparse_files flip a one-value change instead of two structurally
  different edits on the read and write paths.
- `enc` -- presence-tag vs value-tag booleans, which the core serializer mixes
  (57 presence, 9 value) and which every reader previously had to know.
- `access` -- read-only capabilities and live status, write-only passwords and
  the ip2country trigger, and the amuleapi passwords that belong to
  /auth/passwords and are rejected here.
- `gated_by` -- the 409 when files.mmap_enabled is set against a core built
  without mmap.
- `read_group` -- connection.upnp_available, the one field whose EC tag lives
  in a different group ([General]) than its JSON category implies.

Rows carry the address of their backing member, and the PREF_* macros
static_assert that the declared PrefType matches the member's real C++ type,
so a mis-typed row fails the build rather than misbehaving at runtime.

One field genuinely cannot be described: remote_controls.webserver
.guest_enabled and .guest_password share a single EC tag, which carries the
enable bool as its value and the password hash as a child. There is no 1:1
field-to-tag mapping to write down, so its PATCH packing stays hand-written
and the schema marks it Bespoke. Its GET emission is still table-driven.

Equivalence was checked against a captured GET /preferences from the previous
build: the EC decode and the GET emitter each produce byte-identical output,
and the final key set matches at 119/119. PATCH is covered by curl phase 15
(117/117, unchanged) plus direct checks that every type, the inverted field,
the nested categories and each error path still behave. Error bodies are now
uniform ("<key> must be a bool") where they used to be per-category
("connection field must be a bool"); no test or doc asserted the old wording.

Two new tests pin the table's own invariants: no duplicate fields, every
category resolving to an EC group, members present exactly for the rows that
round-trip a value, enum rows carrying a name table, capability gates naming a
real sibling bool, and the emitted count staying at the documented 119. A
second test asserts the three irregularities above stay at one field each, so
a second one appearing is caught rather than copied.

Net: -1437 / +842 lines, with the prefs handling in Api.cpp down 912 lines and
Refresher.cpp down 212.
…roject#655)

files.create_normal becomes files.create_sparse_files, and its meaning
inverts: create_sparse_files == !create_normal. This is the one field in the
payload whose positive name could not be reached by renaming alone, so the
issue asked for it on its own commit.

Every layer except EC already spoke in terms of "sparse". The config key is
/eMule/CreateSparseFiles, the core stores s_createFilesSparse (default on),
PartFile.cpp reads CreateFilesSparse(), and the desktop checkbox added in
amule-project#662 is labelled "Create new files as sparse files". Only the EC tag is named
for the negative case -- EC_TAG_FILES_CREATE_NORMAL is an empty tag present
only when sparse is off -- and amuleapi was the one consumer that took that
name and published it outward, leaving the Web UI saying "non-sparse" where
the desktop says "sparse" for the same setting.

EC is untouched: same tag number, same presence semantics, same
thePrefs::CreateFilesNormal(). The negation is undone at the webapi boundary
instead, exactly as extended_udp_port_enabled already does for
EC_TAG_CONN_UDP_DISABLE. Thanks to the schema table this is one column:

    PREF_BOOL("files", "create_sparse_files", EC_TAG_FILES_CREATE_NORMAL,
              PrefEnc::Presence, /*invert=*/true, ...)

The snapshot default flips to true to match the core's own default, and the
Web UI label moves to the desktop's positive wording, which also flips the
checkbox's rendered state for an unchanged setting.

Verified end to end against a daemon's own config file rather than just the
API's echo, since a polarity change is precisely where a self-consistent API
can still be wrong:

  CreateSparseFiles=1  -> GET create_sparse_files: true
  PATCH false          -> CreateSparseFiles=0 in amule.conf
  CreateSparseFiles=0  -> GET create_sparse_files: false
  PATCH true           -> CreateSparseFiles=1 in amule.conf

The schema-invariant test caught this change on its own: it enumerates which
fields may invert, so adding a second one failed until the list was updated
deliberately. That is the check working, and the list now names both tags and
why each is negative.

Note for clients: this is the one field where renaming the key without
flipping the value yields the opposite behaviour. Nothing ships with the old
name -- amuleapi is unreleased and the protocol stays at v0 -- so no
compatibility shim is carried.
@got3nks got3nks changed the title refactor(amuleapi): self-explanatory /preferences field names, driven by one schema table (#655) refactor(amuleapi): self-explanatory /preferences names, one schema table, Web UI label CI gate (#655) Jul 29, 2026
CECTag::GetInt() returns uint64_t, so copying it into an std::int64_t
narrows, which bugprone-narrowing-conversions flags on a new line. Keep the
index unsigned: the >= 0 half of the range check was dead weight anyway, and
the remaining bound is the only one that matters.
@got3nks
got3nks merged commit c365675 into amule-org:master Jul 29, 2026
14 checks passed
@got3nks

got3nks commented Jul 29, 2026

Copy link
Copy Markdown
Author

The Web UI exposes an amuleapi password field under Remote Controls that the backend rejects with 400, since those credentials belong to PATCH /auth/passwords.

This is worth fixing @ngosang

@got3nks
got3nks deleted the feat/api-prefs-field-naming branch July 29, 2026 12:15
@ngosang

ngosang commented Jul 30, 2026

Copy link
Copy Markdown
Member

Admin / Guest passwords fixed in WebUI #715

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

GET /api/v0/preferences — Field Naming Review

2 participants