* 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(inbounds): add a narrow endpoint for subscription sort order
Changing an inbound's position in subscription output currently goes through
/update/:id, which takes a whole inbound: the caller has to send settings and
the entire client list back, and whatever it read before the edit is what gets
written. Two people reordering and editing clients in the same inbound race on
one blob, and the reorder wins by overwriting.
Mirror the existing /setEnable/:id shape. The handler takes only the index and
the service reads the stored inbound, so nothing in the request can reach the
settings JSON. Node-owned inbounds are marked dirty in the same transaction and
pushed through the existing runtime update.
* fix(nodes): scope sub sort index updates
---------
Co-authored-by: n0ctal <293235942+n0ctal@users.noreply.github.com>
Keep usage tokens first-link-only while adding an opt-in setting for repeating EMAIL and USERNAME in subscription-body remarks.
Co-authored-by: x06579 <x06579@ai-dashboard>
Fold the standalone 3x-ui-docs project (Next.js 16 + Fumadocs, deployed to
docs.sanaei.dev) into docs/ so the panel and its documentation share a single
source of truth, the way sing-box keeps its docs in-tree. The old repo becomes
redundant and can be retired.
- Import the full site under docs/ (app, components, content, lib, public,
scripts, config). The self-contained pnpm project sits alongside the existing
engineering notes with no filename collisions.
- Re-point "Edit on GitHub" links from MHSanaei/3x-ui-docs to this repo's
docs/content/docs path (docs/lib/shared.ts, docs/app/.../page.tsx).
- Add docs-ci.yml and docs-deploy.yml under .github/workflows/, scoped to
docs/** and run with working-directory: docs, since GitHub only runs
workflows from the repo-root .github/. deploy-static.yml's GitHub Pages
publish (CNAME docs.sanaei.dev) carries over unchanged.
Follow-up (outside this commit): attach the docs.sanaei.dev custom domain to
this repository's Pages (or set the Vercel project's root directory to docs),
confirm the site is live from the monorepo, then delete MHSanaei/3x-ui-docs.