* feat(clients): show short HWID fingerprint in admin device list
Expose a 12-char prefix of the stored hwid_hash in the admin HWID list API and UI so admins can distinguish devices without querying the database. Full hashes and raw HWIDs remain unexposed.
Fixes#6359
* ci: retrigger release matrix after 386 dependency download flake
---------
Co-authored-by: mrchatam <mrchatam@users.noreply.github.com>
Co-authored-by: mrchatam <287639636+mrchatam@users.noreply.github.com>
* feat(sub): add read-only HWID device-slot status endpoint
Closes#6357
A client with an HWID limit had no way to tell a subscriber how many device
slots were left: /{subPath}/{subId} only exposes the gate as a boolean through
X-Hwid-* headers on a 404, and ?format=info carries no limitHwid or registered
count. Every "why can't I connect on my new phone" case therefore had to be
answered by the operator by hand.
GET /{subPath}/{subId}/hwid-status now returns the aggregate counters:
{"active":true,"limit":2,"registered":1,"remaining":1,"full":false}
- SELECT-only. It never registers an hwid, never touches last_seen and never
calls the enforcement path, so asking about a slot cannot spend one.
- Counters only: no hwid value or hash, no email, no device metadata, no IP,
no User-Agent, and none of the X-Hwid-* gate headers.
- The subscription id is already the bearer secret for /{subPath}/{subId}, so
no admin token and no new auth mechanism.
- Unknown and disabled subscriptions both answer a bare 404, with identical
status, headers and body, so the route cannot be used to probe which
subscription ids exist.
- No HWID limit configured returns {"active":false,"limit":0,...}.
- No schema change and no migration.
Scoped to enabled clients exactly like effectiveHwidLimitForSubID, so the
reported limit is always the limit the gate enforces on a shared sub_id, and
remaining clamps at zero when the effective limit drops below the number of
registered devices. A separate route leaves /{subPath}/{subId}, ?format=info
and the JSON/Clash routes byte-for-byte unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(sub): document hwid-status as the bare object it returns
The OpenAPI operation for GET /{subPath}/{subId}/hwid-status inherited the
{success,msg,obj} panel envelope from build-openapi.mjs's default 200
response, while the handler writes the HwidSlotStatus struct bare. A client
generated from the spec would read `obj` and never find the counters, and
the description prose contradicted the schema with a hand-written example.
HwidSlotStatus now sits in openapigen's StructAllow with example: tags, the
entry references the generated schema through a `responses` block, and
build-openapi.mjs attaches the generated example to any `responses` entry
that $refs a generated schema, so no example is hand-written. The HEAD
variant the controller registers is documented like its siblings, and the
summary follows the "path prefix is configured by subPath" wording now that
fresh panels randomise the prefix.
Regenerated frontend/public/openapi.json, docs/public/openapi.json and the
subscription-server MDX. openapi-runtime-contracts.test.ts pins the bare
schema, the generated example and the HEAD operation.
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
* feat(clients): allow removing a single HWID device
Only "list" and "clear all" existed for registered HWID devices, so
freeing one slot under a client's HWID limit meant clearing every
device and waiting for the ones you kept to re-register. Adds a
per-device delete: DELETE /panel/api/clients/hwids/:email/:id, scoped
to the client's own sub_id (device ids are a global auto-increment,
not per-subID, so this also prevents deleting another client's
device), plus a delete button next to each device in the existing
HWID modal.
Addresses MHSanaei/3x-ui#6245.
* feat(clients): surface HWID limit + device log in the client info card
Mirrors the existing IP-limit row/eye-icon-modal pattern that's
already in this card. The HWID devices modal reuses the same
list/clear-all/per-device-delete UI already shipped for the edit
form's own HWID modal, so a device can be removed without opening the
edit form at all.
* i18n: add HWID single-delete strings to all 13 locales
deleteHwid/deleteHwidConfirm/hwidDeleted were only added to en-US and
ru-RU in the previous commit; backfilling the other 11 locales the
project's own translation set covers.
* fix(clients): address automated review of HWID single-delete PR
- ClientInfoModal: use the existing dateLabel() helper (Jalali-aware)
for HWID first/last-seen instead of a raw dayjs format, matching
every other timestamp in the same modal.
- Add okText/cancelText to the delete-device Popconfirm in both
ClientInfoModal and ClientFormModal so all 13 locales get a
translated confirm dialog instead of Antd's English default.
- deleteHwid controller: stop reusing the success toast key on both
error paths, which rendered a red "Update successful" toast on a
real (not just theoretical) failure such as a stale HWID modal.
- Trim DeleteClientHwid's doc comment to the repo's 2-line cap and
correct it: deletion is scoped by sub_id, which can span more than
one ClientRecord, not strictly "this client only".
- Add TestDeleteClientHwid covering cross-sub_id id rejection, unknown
id rejection, and a real successful delete.
* chore: retrigger CI (previous run stuck installing Playwright Chromium)
* fix(clients): address the arbiter review on the HWID single-delete PR
- Extract the HWID device list into a shared frontend/src/lib/clients/
hwid-log.ts type/normalizer, a shared useClientHwids hook, and a
shared ClientHwidListModal component, mirroring the existing IP-log
pattern. ClientInfoModal and ClientFormModal both render the same
component now, so the two copies can no longer drift the way they
already had (different date formatting, different tag styles).
- Add a Popconfirm to the HWID "Clear all" button (previously
unconfirmed, unlike the per-device delete right next to it) — closes
the confirm/no-confirm asymmetry the review flagged as the main risk.
- Sync docs/public/openapi.json with the two hwids paths and regenerate
clients.mdx. Scoped to just those two paths rather than a full copy
from frontend/public/openapi.json: the docs copy is far enough behind
on unrelated paths (a host-group API rename) that a full sync breaks
the Next.js build on locale pages referencing the old shape — out of
scope for this PR.
* fix(clients): trim HWID list comment blocks to 2 lines
Repo convention caps comment blocks at 2 lines; both were 1 line over.
* chore: retrigger CI
build (arm64) and build (armv6) failed on a transient Go module proxy
network error (INTERNAL_ERROR stream reset), unrelated to this PR's
changes.
* feat(sub): add per-client subscription HWID limits
* fix(sub): address HWID review on shared subId and bulk create
* fix(sub): store HWID devices by sub_id and drop anchor client workaround
* fix(sub): restore UA auto-detect and HTML page routing in subs()
The cherry-pick of the HWID gate onto main's refactored SUBController
had dropped main's UA-based format auto-detection and sub-page handling
from subs(). Restore those branches, slotting enforceHwid after the
HTML page and before format detection so the gate only applies to
machine-readable subscription bodies.
Also adapt tests to main's options-struct constructor and to the
ClientService.Update signature extended with limitHwid.
* fix(frontend): drop axios from HttpUtil.delete
The bulk-delete rework's committed version still referenced axios,
which this file no longer imports, breaking typecheck in CI. Use the
httpRequest wrapper like the other verbs.
---------
Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>