GitHub Pages was never enabled in this fork's repo settings, and the
workflow's own comment ties it to docs.sanaei.dev, upstream's custom
domain, not something this fork has. Failed both times it ever ran
(including once before today, unrelated to any of this session's
other changes) with "Ensure GitHub Pages has been enabled". Docs CI
(docs-ci.yml, lint/build-check only, no deploy) is untouched.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Never used by this fork's own distribution (install.sh/x-ui.sh is the
only supported install path), AmneziaWG structurally can't run in the
Alpine-based image anyway, and Docker Hub publishing was failing on
every release for lack of configured credentials. Removed Dockerfile,
docker-compose.yml, DockerEntrypoint.sh, DockerInit.sh, .dockerignore,
and the docker.yml CI workflow; dropped the now-dead "Docker" README
subsection (all 7 languages), the "Docker image"/"Docker Compose"
options from the issue/PR templates, and corrected claude-bot.yml's
now-stale references to the deleted files, the never-actually-ours
ghcr.io/mhsanaei/3x-ui image, and (caught in passing) an already-stale
claim that Windows is a supported platform.
Left untouched: generic container-runtime adaptations that apply
regardless of image source (x-ui.sh's running-in-docker detection,
the virtual-interface-name filters, the DNS-over-container-network
note) and deploy/test/smoke-noninteractive.sh, which uses Docker only
as its own test sandbox, not as something this repo ships.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The push/pull_request paths filter didn't include .github/workflows/**,
so the previous commit (fixing the Audit step) never actually ran
through CI -- editing the workflow itself silently skipped the trigger.
A broken workflow-syntax change could merge untested the same way.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
npm audit --audit-level=high doesn't read code comments -- it was still
exiting 1 on the one remaining, already-investigated-as-unfixable advisory
(eslint-plugin-jsx-a11y's own pinned minimatch, GHSA-mh99-v99m-4gvg), so
every run on main has actually been red despite the prior commit's comment
calling it "known-accepted". That advisory lives entirely in
devDependencies (lint tooling, never shipped), so --omit=dev is the
correct signal for "doesn't apply to what we ship" -- verified locally,
0 vulnerabilities. Runtime dependencies are still audited at high severity.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Two real, forward-compatible fixes for npm audit's high-severity
advisories (not the downgrades npm audit fix --force offers):
- react-router (GHSA-qwww-vcr4-c8h2, RSC CSRF bypass): react-router-dom
is frozen at 7.18.1, pinning the vulnerable react-router@7.18.1 -- no
newer react-router-dom release exists pointing at the fixed line.
react-router itself has shipped the real fix at 8.3.0. Migrated the 9
files importing from react-router-dom (all using plain
createBrowserRouter/RouterProvider/useLocation/useNavigate/Outlet, no
RSC anywhere) to import from react-router directly instead.
- brace-expansion/minimatch (GHSA-mh99-v99m-4gvg): fixed for eslint's
own dependency chain (minimatch@10.2.5, used by eslint itself,
storybook, typescript-eslint, swagger-client) via a scoped
"minimatch@^10" override forcing brace-expansion to the now-published
5.0.8 patch -- within the range minimatch@10.2.5 already declares
wanting (^5.0.5), so this isn't a version-pin workaround, just
unblocking a patch release npm's resolver hadn't picked up.
One advisory remains genuinely unfixable from our side:
eslint-plugin-jsx-a11y pins minimatch@^3.1.2 (old major, never
patched); forcing it to the 10.x line via override breaks npm's own
dependency-tree validation (a real incompatibility, not just an npm
quirk), so this needs an eslint-plugin-jsx-a11y release bumping its own
minimatch. Lint-time only, no untrusted input reaches it -- ci.yml's
Audit step comment updated to reflect the new, smaller remaining scope.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Neither is exploitable in this app (react-router CVE is RSC-only, we use
createBrowserRouter; jsx-a11y's brace-expansion chain only runs against our
own lint globs), and npm's suggested fixes are both downgrades with no real
forward patch published yet -- left as-is rather than trading a working
version for one that doesn't fix anything reachable here.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Removing build-windows (previous commit) means dev-artifacts/*.zip
never matches anything — gh release upload treats an unmatched glob as
fatal, so every dev-latest publish since has failed outright (verified:
this run's publish-dev job errored "no matches found for
`dev-artifacts/*.zip`" on all 5 retries). Only the *.tar.gz glob remains.
Also manually deleted the stale x-ui-windows-amd64.zip asset from the
current dev-latest release — it would never be refreshed again
otherwise, and silently going stale forever is worse than not being
there.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
AmneziaWG (awg-quick, the DKMS kernel module) and this whole panel's
real deployment target here are Linux servers/routers — the Windows
job (MSYS2 + CGO cross-build, its own Xray/mtg-multi asset download
step) never served a purpose for this fork and just spent CI minutes
producing a binary nobody uses. Removed build-windows entirely and
dropped it from publish-dev's needs.
Also updated both READMEs' Supported Platforms line to match — this
fork's CI no longer produces a Windows binary, upstream's still does.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The dev-latest publish step probed for the release with gh release
view before choosing edit or create. During the api.github.com 503
storm the probe itself failed, mis-routing an existing release into
the create path, which then died on a permanent 422 already-exists
error that no amount of retrying can fix.
The release exists on every run but the very first, so edit first and
fall back to create only when the edit fails. The retry log line now
names the gh subcommand instead of just the binary.
The api.github.com outage that failed the previous release run outlasted
the retry window, while asset downloads from github.com kept working the
whole time. The latest-release lookup was the only api.github.com
dependency left in the build jobs, so resolve the tag from the release
page redirect on github.com instead: tag resolution now shares exactly
the failure domain of the downloads it feeds, and an API-only outage can
no longer fail a build that could otherwise finish. No token needed for
the redirect, on either platform.
A GitHub API 503 storm failed the release run four attempts in a row:
the mtg-multi latest-release lookup died on every platform (even s390x,
which never packages the sidecar), and the matrix default fail-fast then
cancelled the six healthy builds alongside the one that hit the outage.
Retry every external fetch with backoff (curl --retry-all-errors, wget
--retry-on-http-error, Invoke-* -MaximumRetryCount), scope the mtg-multi
tag lookup to the platforms that actually package the sidecar, disable
fail-fast on the build matrix, and retry the idempotent dev-latest
publish commands. The workflow file itself now triggers the run's path
filters, so editing it exercises the release build immediately instead
of failing silently at the next code push.
pnpm/action-setup resolved 'version: 11' to the newest 11.x, and its self-installer crashes upgrading to 11.12.0 (Cannot use 'in' operator to search for 'integrity'), failing both docs workflows at setup. Reading docs/package.json instead installs the exact packageManager pin (pnpm@11.9.0), which also keeps the workflows and the lockfile toolchain on a single source of truth.
The repo now pins Node 24 everywhere instead of mixing 22 and hardcoded workflow versions. The docs workflows read .nvmrc like the main CI already did, so the Storybook bundle in the Pages deploy builds on the same runtime as the PR gate. The docs gen:api script runs its TypeScript entry natively, dropping the experimental type-stripping flag that Node 24 makes default; the matching frontend cleanup (engines and gen:api) landed with the Storybook commit.
The docs site and the component workbench were entirely disconnected. The Pages deploy now builds the frontend Storybook and bundles it into the artifact under /storybook, so the live component reference ships with the documentation, and the navbar links to it. Story changes trigger a redeploy so the published workbench cannot go stale.
Storybook existed only as an undocumented local tool: 9 of 24 reusable components had stories, autodocs pages were bare prop tables, nothing built or tested the stories, and no contributor doc mentioned the workbench existed.
Every reusable component under src/components/ now has a co-located story with enriched autodocs (component descriptions plus per-prop argTypes, kept as string metadata since the repo bans line comments). Stories double as headless Chromium tests through the Storybook vitest addon, with axe accessibility checks enforced as errors and play-function interaction tests covering the modals, the RHF field bridge, the config block, and the select-all buttons. The preview now mirrors the panel's real theme DOM (body class, shared AntD theme config, seeded theme storage) so what stories render matches production.
CI and make verify gain a static Storybook build as a compile gate, and the frontend test job installs Chromium so story tests run on every PR. Contributor docs (frontend README, CONTRIBUTING, agent guides) document the workbench, the story conventions, and the Controls setup. Node engines move to 24 LTS and gen:api drops the type-stripping flags that Node 24 makes default.
Bump xtls/xray-core to 50231eaf (v26.7.11) and the three binary pins
(DockerInit.sh, release.yml x2) in lockstep.
Adapt the panel to the upstream changes:
- Shadowsocks "none"/"plain" and VMess "none"/"zero" were removed from
the core. A migration rewrites stored none/plain SS methods to a
supported cipher and none/zero VMess security to "auto" (on both the
clients column and inbound settings JSON); the SS build-time heal does
the same so a row injected after boot cannot brick startup. The removed
values are dropped from every frontend option list, schema and adapter,
and coerced to "auto" at the Go link/sub/Clash emit sites and both link
importers. Fix the CipherType_NONE sentinel that no longer compiles.
- Unencrypted vless/trojan outbounds to a public address are now refused
by the core. Validate outbounds through the vendored config loader when
saving the xray template and when storing/merging outbound
subscriptions, so one such outbound cannot keep the core from starting.
- New TCP finalmask type "xmc" (Minecraft mimicry): add it to the sub
link allowlist, the frontend enum and the FinalMask form (hostname,
usernames, required password), and document it.
- streamSettings gained a "method" alias for "network"; canonicalize it
to "network" at inbound save time and in the form adapters/schema so a
method-keyed config keeps its transport.
- New root "env" config key is passed through xray.Config, compared in
Equals, and forces a restart in the hot diff.
- REALITY now defaults minClientVer to 26.3.27; update the form
placeholder.
Rework the handle-pr-review, handle-pr-fix, and handle-issue prompts to produce professional, structured output. The review job now rates findings by severity and confidence across explicit review areas and reports a Summary, Findings, and a text-only verdict in one plain comment; the fix job reuses the same lens to prioritize what it applies versus leaves for the author; issue triage gains a structured bug-confirmation format and explicit outcomes for mislabeled and not-a-bug reports, closing conservatively. Severity uses text labels to respect the no-emoji house style, and the adapted ignore-list keeps i18n and generated files flaggable.
claude-code-action only pushes a branch and posts a Create PR link by design; it never opens the PR itself. Add a post-step to the mention job that opens a PR from the action's branch_name output when the trigger was an issue (guarded against no-op branches and against an existing PR).
Simplify the mention prompt so the agent just makes edits with Edit/Write and lets the workflow commit and open the PR, instead of running git/gh pr create itself (which fought the action's built-in flow and left only a link).
Make every automatic, untrusted trigger read-only and require an explicit trusted actor for any code change.
- handle-issue (issue opened): read-only triage; confirm bugs and tag the maintainer, never edit code or open a PR. Authenticates as GITHUB_TOKEN so replies post as github-actions[bot], not a personal account.
- handle-pr-fix (PR opened): applies fixes only for owner/member/collaborator authors; dropped allowed_non_write_users so the default write gate also applies.
- handle-pr-review (PR opened, external authors): read-only review comment only.
- mention (@claude comment): runs only for the repository owner; may open a PR from an issue or commit to a PR on explicit request.
No job authenticates as the static PAT anymore; the PAT is used only to route git pushes for the trusted PR-fix and owner-mention paths.
Split the review-only handle-pr job into handle-pr-fix (owner/member/collaborator PRs: apply refactors and bug fixes directly, commit to the PR branch, no suggestion blocks) and handle-pr-review (external/fork PRs: one review-only comment, no suggestions, no code checkout).
Upgrade handle-issue to open a fix PR for easy bugs (pushed via CLAUDE_BOT_PAT so pull_request CI runs on it), confirm the root cause and tag the maintainer for big bugs, and never open a PR for feature or enhancement requests.
Map each mtproto client's totalGB and expiryTime onto mtg-multi's new
[secret-limits] (quota/expires): emit them into the generated config and
hot-apply through PUT /secrets so live connections survive. Quota is
written as an exact "<n>B" byte count that round-trips through both the
config and API parsers without the precision loss of a base-2 unit.
The sidecar's quota counter is not pruned when a secret is dropped, so a
panel-side traffic reset re-pushes the client's secret and then calls
POST /secrets/{name}/reset-quota (wired into every reset path) so a
renewed client is not immediately re-blocked.
Resolve the mtg-multi binary from the fork's latest release tag in
DockerInit.sh and release.yml instead of a hardcoded version pin, so the
panel no longer needs a manual bump per fork release.
* chore(frontend): add husky + lint-staged pre-commit gate
Wire a local pre-commit gate that runs eslint --fix on staged
frontend TypeScript via lint-staged. Because the only package.json
lives in frontend/ while the git root is one level up, the prepare
script installs husky hooks at frontend/.husky from the repo root
(cd .. && husky frontend/.husky), and the pre-commit hook cd's into
frontend/ before invoking lint-staged so node_modules resolves.
* test(frontend): add MSW request mocking
Add Mock Service Worker so tests can exercise the real http-init.ts
request pipeline (CSRF acquisition, 403 refetch-and-retry, body
parsing) instead of only stubbing HttpUtil. A node setupServer is
started for the vitest unit project with onUnhandledRequest bypass so
the existing HttpUtil spies and 55 component tests are untouched; the
browser worker is copied to public/ for Storybook and dev use.
* chore(frontend): add Storybook + component stories
Set up Storybook 10 on the React-Vite builder (compatible with the
pinned Vite 8.1.3 and React 19). The preview decorator mirrors the
vitest component harness: an Ant Design ConfigProvider with a
light/dark toolbar toggle and an en-US i18next instance. main.ts
neutralizes the app vite config bits that do not belong in a component
workshop (the three-entry rollup input, renderBuiltUrl, and the shared
dist outDir) so build-storybook can never clobber internal/web/dist.
Seeds stories across the presentational library (viz, ui, clients,
feedback). build-storybook is a local tool and is not wired into the
CI gate.
* feat(frontend): add React Hook Form primitives
Introduce the shared RHF layer that AntD inputs bind through, ahead of
migrating the forms off Ant Design's Form store:
- FormField wraps a Controller in an Ant Design Form.Item shell,
reconciling the value/onChange shapes of Input, Switch, InputNumber,
Select and friends via normalizeAntdOnChange, with input/output
transforms and Zod-issue-key error messages resolved through t().
- useZodForm wires zodResolver (Zod 4) with the AntD-matching modes
(validate on submit, then live) and shouldUnregister false so hidden
and unmounted-tab fields keep their values.
- rhfZodValidate covers the rare per-field rule sites.
Covered by a FormField test exercising normalization, transforms, and
resolver error surfacing.
* refactor(frontend): migrate Pattern-B leaf forms to React Hook Form
Move the controlled-useState leaf forms onto RHF via the FormField
primitive, keeping Ant Design components and each form's exact submit
behaviour (same safeParse, same toast on the first Zod issue, same
payload building):
- clients: ClientBulkAdjustModal, BulkAddToGroupModal, ClientBulkAddModal
- xray: RuleFormModal, BalancerFormModal, WarpModal, NordModal
Multi-control widgets that don't fit a single input (inbound dual
select, subId regen, expiry branches, the balancer tag warning) stay as
explicit Controller/setValue. Derived visibility now reads live values
through useWatch. FormField gains a required prop so migrated fields keep
their required-asterisk affordance.
Settings tabs are intentionally excluded: they are control-panel
components that live-patch a parent AllSetting via SettingListItem, not
Ant Design Form submit-forms.
* refactor(frontend): migrate LoginPage to React Hook Form
Replace the Ant Design Form store + antdRule per-field validation with
useForm + FormField. The AntD Form stays as the layout/submit wrapper,
now driving methods.handleSubmit(onSubmit) via onFinish. Username and
password validate through rhfZodValidate(LoginFormSchema.shape.*); the
two-factor field keeps its conditional required rule (only registered
when 2FA is enabled). Submit posts the same values to /login.
* refactor(frontend): migrate ClientFormModal to React Hook Form
Move the client add/edit form off controlled useState onto RHF while
preserving exact submit behaviour (same ClientFormSchema /
ClientCreateFormSchema safeParse, same toast, same payload + attach/
detach diff + external-links build). expiryDate is stored as an epoch
number (never a Dayjs) to survive RHF's value cloning, converted at the
DateTimePicker boundary. externalLinks uses useFieldArray with stable
ids. inboundIds and the derived show*/ss2022 visibility read live via
useWatch. Space.Compact button-group widgets stay manual Controllers so
the joined borders keep working.
* refactor(frontend): migrate Node and DNS modals to React Hook Form
Both are self-contained Pattern-A forms (no shared fragments). Replace
Form.useForm with useForm + FormProvider, Form.useWatch with useWatch,
setFieldValue with setValue, and partial validateFields([...]) with
methods.trigger([...]). Per-field antdRule becomes rhfZodValidate rules;
the Node scheme->tlsVerify cascade moves to FormField onAfterChange; the
DNS domains/expectIPs/unexpectIPs string arrays are driven by
useWatch + setValue. Submit runs through handleSubmit on the modal OK
button, preserving each form's exact validation, payload build, and
save/onConfirm behaviour.
* refactor(frontend): migrate HostFormModal to React Hook Form
The host external-proxy editor's outer form moves to useForm +
FormProvider. Security/tab visibility reads via useWatch; the three
json-form editors (HostMuxForm/HostSockoptForm/HostFinalMaskForm) are
bound as value/onChange black boxes through a Controller (their own
internal forms are unchanged). remark/inboundId keep their validation
via rhfZodValidate; submit runs through handleSubmit and builds the
same payload (isDisabled = !enable) and save call.
* refactor(frontend): migrate OutboundFormModal + fragments to React Hook Form
Move the outbound form cluster off Ant Design's Form store onto RHF.
The parent uses useForm + FormProvider with a watch() subscription for
the protocol reseed cascade and setValue-based network/security/xmux
cascades; the JSON<->Basic bridge and the formValuesToWirePayload
submit are preserved exactly. Every outbound transport/protocol/security
fragment now binds through FormField/useWatch via context.
The shared config editors stay untouched and are bound through small
value/onChange adapters (src/lib/xray/forms/fields: FinalMaskField,
SniffingField, SockoptCustomField) via Controller; HeaderMapEditor binds
directly. The host json-form wrappers that reuse the outbound MuxForm/
SockoptForm (HostMuxForm, HostSockoptForm, OutboundSubtreeJsonForm) move
to a local RHF provider to match. Outbound render/link tests pass
unchanged.
* refactor(frontend): migrate InboundFormModal + fragments to React Hook Form
Move the inbound add/edit form (the largest form in the panel) and its
transport/protocol/security fragments off Ant Design's Form store onto
RHF, mirroring the outbound migration. The parent uses useForm +
FormProvider with a watch() subscription for the protocol reseed
cascade (type==='change' guard so programmatic resets don't reseed) and
setValue-based network/security cascades; useSecurityActions drives the
TLS/Reality keypair + scan through setValue. Hidden pass-through
Form.Items are dropped (their values ride in the reset object and
survive via shouldUnregister:false), so getValues() still returns the
settings.clients subtree untouched. accounts / certificates / tun lists
use useFieldArray; the shared FinalMask/Sniffing/Sockopt editors bind
through the value/onChange adapters.
Submit keeps the manual InboundFormSchema.safeParse + formatInboundValidation
toast + formValuesToWirePayload exactly. The golden link/full fixtures
pass byte-for-byte, confirming identical wire output. inbound-form-blocks
test harness rewritten from a Form.useForm harness to an RHF provider.
* refactor(frontend): retire antdRule; document the RHF form pattern
All forms now build on React Hook Form, so the AntD-Form Zod adapter
antdRule (src/utils/zodForm.ts) has no remaining callers — remove it.
Update frontend/CLAUDE.md: forms use useZodForm + FormField from
components/form/rhf with zodResolver/rhfZodValidate validation; AntD
<Form> is layout-only; the shared FinalMask/Sniffing/Sockopt editors
stay AntD islands wrapped as value/onChange adapters bound via a
Controller.
* chore(frontend): cover esbuild in the allowScripts allowlist
esbuild (pulled in transitively by Vite/Vitest/Storybook) ships a
postinstall that npm's allow-scripts flags as uncovered on every
install. Its platform binary is delivered through the @esbuild/<platform>
optionalDependencies, so the postinstall isn't needed here; deny it like
the other entries to silence the warning.
* fix(frontend): restore label layout in Sniffing/FinalMask field adapters
The value/onChange adapters that wrap the shared SniffingFields and
FinalMaskForm editors put them in their own isolated AntD Form, but that
Form was missing the label layout the fields used to inherit from the
inbound/outbound parent form. Their labels rendered full-width instead
of the compact right-aligned column, so the Sniffing tab and the TCP
Masks / QUIC Params sections looked broken. Give both adapter forms the
same colon=false, labelCol/wrapperCol span 8/14, labelWrap layout.
* ci: add least-privilege permissions to Docs CI workflow
The docs-ci workflow had no explicit permissions block, so it inherited
the repository default for GITHUB_TOKEN. The build job only checks out
and builds the docs, so restrict it to contents: read, resolving the
CodeQL actions/missing-workflow-permissions alert.
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.
CodeQL alert 99 (actions/cache-poisoning/poisonable-step): the workflow_run
job runs in the default branch's cache scope, so checking out
workflow_run.head_sha and executing a script from it is a cache-poisoning
surface. The if-guard (event == 'push') already kept fork PRs out, but the
checkout pin was never the load-bearing part of the release verification —
the version argument is, since install.sh downloads that exact release
binary. Run the smoke script from the default branch instead, which also
matches what real users execute.
* fix(mtproto): split the mtg fingerprint into structural and secrets parts
A reordered clients array in the stored settings used to read as a config
change because the fingerprint concatenated secrets in array order, and one
opaque fingerprint could not tell a restart-worthy change (bind address,
fronting, throttle) from a secret-set change a reload-capable mtg can absorb
in place. Sort the secret pairs so order stops mattering, and split the value
so the upcoming hot-reload path can decide between keeping, reloading, and
restarting the process.
* fix(mtproto): stop restarting mtg on every inbound edit
Saving an mtproto inbound tore down and respawned its mtg sidecar even when
nothing material changed, dropping every live Telegram connection: the update
path pushed DelInbound+AddInbound, and Remove deletes the manager's map entry,
so Ensure's fingerprint no-op gate could never fire. Route mtproto updates
through a single Ensure call so an edit that leaves the generated TOML alone
keeps the process, and only real config changes restart it.
Capturing the pre-edit protocol also fixes a latent leak: changing an
inbound's protocol away from mtproto never stopped the sidecar, because the
snapshot handed to the runtime already carried the new protocol and the
removal took the xray branch, leaving an orphaned mtg holding the port.
An mtproto push failure no longer requests an xray restart - xray cannot fix
the sidecar, and the 10s reconcile job self-heals it.
The regression test fakes mtg by re-executing the test binary, counting
spawns through a pid file: an unchanged save and a remark-only edit must keep
the process, a re-keyed secret must restart it.
* fix(mtproto): exclude depleted clients from the reconcile job to match the sync push
The 10s reconcile job derived mtg secret sets from raw inbound settings while
the interactive push filtered clients through buildRuntimeInboundForAPI, which
drops client_traffics-disabled (depleted or expired) clients. The two paths
therefore disagreed on the fingerprint - each disagreement one needless mtg
restart dropping live connections - and worse, the job kept serving depleted
clients' secrets indefinitely, so running out of traffic never actually cut an
mtproto client's access.
DesiredMtprotoInstances now builds the job's desired state with the same
depletion overlay the push uses (one bulk client_traffics query), drops
inbounds whose every secret is filtered away so their sidecar stops, and
AddInbound pushes the filtered payload too so an imported inbound carrying
disabled stats does not seed a fingerprint the next reconcile disagrees with.
* feat(mtproto): hot-reload mtg secrets in place instead of restarting
A client add, removal, re-key, or enable-toggle changes only the [secrets]
section of the generated config, yet the panel could apply it only by killing
and respawning the mtg sidecar, dropping every Telegram connection on that
inbound. Split the ensure decision three ways: an identical config is a no-op,
a secrets-only change rewrites the TOML on the same api port and asks mtg to
hot-swap it via POST /reload, and a structural change (or a failed reload)
falls back to the full stop-and-start.
The reload endpoint is served by the mhsanaei/mtg-multi fork; against an older
binary the POST 404s and the manager restarts exactly as before, so panel and
binary upgrades stay order-independent.
* feat(mtproto): apply single-client edits to the sidecar immediately
Client CRUD on an mtproto inbound was a runtime no-op, so an add, delete,
re-key, or enable-toggle only reached mtg on the next 10s reconcile. With the
sidecar now able to hot-reload, push the change straight after the edit commits:
applyLocalMtproto rebuilds the inbound's filtered client set and re-applies it,
so a new client works within a moment (and, on a reload-capable binary, without
disturbing the others) and deleting the last client stops the process.
The three interactive single-client paths (add, update, delete) call it; bulk
operations still ride the reconcile job, which converges to the same state.
* chore(mtproto): pin mtg-multi to the mhsanaei fork v1.13.3
The reload endpoint the panel now uses lives in the mhsanaei/mtg-multi fork, so
point the source-build pin (DockerInit.sh + both release.yml matrices) at it and
bump to v1.13.3. The install still produces the same mtg-multi binary name, so
the mtg-<os>-<arch> rename and everything downstream are unchanged. Docs and the
package comment note the hot-reload path and its restart fallback.
* feat(mtproto): apply live secret updates via the management API and add ad-tag
Two capabilities the mhsanaei/mtg-multi v1.13.3 fork exposes are now surfaced by
the sidecar manager.
Live updates go through PUT /secrets on the fork's management API instead of
POST /reload: the panel already holds the whole desired set per inbound, so it
sends secrets and the advertising tag as one JSON call that mtg applies
atomically, keeping every unchanged connection and closing only removed or
re-keyed ones. The config file is still written first so a restart or crash
recovery reproduces the state, and any non-200 (an older binary, a refused
connection) still falls back to a full restart.
Per-inbound ad-tag adds an optional 32-hex Telegram advertising tag plus
public-ipv4/public-ipv6 overrides. The ad-tag rides the reloadable secrets
fingerprint, so changing it hot-applies without dropping connections; the public
IPs are proxy-construction parameters and sit in the structural fingerprint, so a
change there restarts the process. Empty public IPs are omitted so mtg
auto-detects the reachable address.
* feat(inbounds): expose the mtproto ad-tag and public IP in the inbound form
Adds an Ad-tag field (validated as 32 hex characters) plus optional Public IPv4
and Public IPv6 overrides to the MTProto inbound form, backed by the same-named
settings the sidecar writes into the mtg config. The public IPs are optional —
left blank, mtg auto-detects the reachable address the ad-tag middle proxy needs.
English strings are added to every locale; the non-English ones carry the
English text until translated and fall back to it meanwhile.
* ci(mtproto): install mtg-multi from prebuilt release binaries
The fork now publishes release archives for every platform we package, so
download and unpack the matching mtg-multi-<ver>-<os>-<arch> binary instead of
compiling it from source with go install. Faster builds and no toolchain step,
and the archive's platform labels line up with our matrix; the produced
mtg-<os>-<arch> filenames are unchanged.
* i18n(mtproto): localize the ad-tag and public IP strings
The six mtgAdTag*/mtgPublicIp* keys shipped with English text in every locale as
a placeholder. Translate them into the twelve non-English locales (Arabic,
Spanish, Persian, Indonesian, Japanese, Portuguese-BR, Russian, Turkish,
Ukrainian, Vietnamese, and Simplified/Traditional Chinese); en-US is unchanged.
* retired goreportcard.com
go install refuses to run with GOBIN set when GOOS/GOARCH differ from the host, which failed the linux release build for every non-amd64 platform (386, arm64, armv7, armv6). Let it install into GOPATH/bin instead, where cross-compiled binaries land in a GOOS_GOARCH subdirectory, and locate the binary there. DockerInit.sh keeps GOBIN because buildx runs it under emulation for the target platform, making the install native.
Replace the upstream 9seconds/mtg sidecar with the dolonet/mtg-multi fork so a single MTProto inbound can serve many per-user secrets. Each panel client is now one named FakeTLS secret in the fork's [secrets] section: clients are first-class (attach/detach, limits, expiry, per-client tg:// links) exactly like every other protocol, mirroring the WireGuard multi-client model. Per-client traffic and online status come from the fork's /stats JSON API (its Prometheus output has no per-user label), fed into the existing email-keyed client_traffics accumulator; an optional throttle caps concurrent connections. A one-time seeder converts each legacy single-secret inbound into a one-client inbound.
The fork ships only linux/darwin amd64/arm64 binaries but is pure Go, so provisioning builds it from source for every supported platform (release.yml, DockerInit.sh) while keeping the panel-expected mtg-<os>-<arch> filename and the 'run' verb, so process.go is untouched. Also fixes a pre-existing update.sh gap that never renamed the mtg binary for armv6/armv7 updates.
The v3.4.2 tag push triggered the smoke workflow immediately, but
install.sh with no arguments resolves releases/latest, which still pointed
at v3.4.1 while release.yml was uploading the new assets. The green smoke
run therefore validated the previous release (#5756). A paths filter alone
cannot exclude tag pushes because a brand-new tag ref has no diff base.
Restrict the push trigger to branches so tag pushes no longer start the
unpinned job, and add a workflow_run job that fires after the release
workflow completes for a v* tag: it checks out the tagged commit, passes
the tag through smoke-noninteractive.sh into install.sh's explicit-version
path, and asserts the installed binary reports exactly that version.
Closes#5756
Update the xray-core Go module (infra/conf builders + gRPC command clients) and the bundled binary pin in DockerInit.sh and the release workflow from v26.6.22 to v26.6.27. No gRPC command-API breaking changes. The release's other inbound work rides along with the bump: TUN autoSystemRoutingTable/autoOutboundsInterface are already modeled in the frontend tun schema, while Hysteria vlessRoute (UUID-derived) and the TUN traffic counters are internal to xray-core and need no panel changes.
Update the GitHub Actions CI workflow to use golangci/golangci-lint-action@v9 instead of v8. This keeps the lint job aligned with the latest major version and ongoing action maintenance.
Add .golangci.yml (v2): the standard linters plus bodyclose, errorlint, noctx, misspell, rowserrcheck, sqlclosecheck, unconvert, usestdlibvars, with gofumpt + goimports formatters. Enable the std-error-handling exclusion preset for idiomatic Close/Remove/Setenv ignores; scope-exclude SA1019 (parser.ParseDir in tools/openapigen) and ST1005 (intentional capitalized user-facing error copy that tests assert verbatim). No inline nolint directives were introduced.
Resolve all 217 findings behavior-preserving: gofumpt/goimports formatting, explicit blank assignment on intentionally ignored errors, errors.Is/errors.As and %w wrapping, context-aware stdlib calls (CommandContext/QueryContext/NewRequestWithContext/Dialer), staticcheck simplifications, removed redundant conversions, http.StatusOK and http.MethodGet, inlined the go:fix intPtr helper, and deferred sql rows Close. Add a golangci CI job mirroring the existing Go jobs.
Remove the release-driven Packer AMI/qcow2 pipeline and everything that existed only to feed it: the image.yml workflow, deploy/packer, deploy/lightsail, deploy/firstboot, the AWS Marketplace checklist, and the first-boot smoke test/job.
Keep the cloud-agnostic unattended-install path (cloud-init + install.sh non-interactive) and the Hetzner notes, which never depended on the workflow. Hetzner's snapshot path is dropped too since it relied on firstboot to avoid admin/admin on clones; cloud-init regenerates per-instance credentials on its own.
Update deploy/README, the cloud-init and Hetzner docs, the root README plus its six translations, and .gitattributes to match.
Adds an opt-in Dev channel so panels running CI per-commit builds can self-update to the latest commit, mirroring the stable online-update flow.
CI publishes/overwrites a single fixed-tag pre-release (dev-latest), force-moved to the newest main commit and marked --latest=false so releases/latest stays the stable tag. Builds stamp the short commit via -ldflags; the panel compares the running commit to the dev release commit to detect an update, and update.sh honors XUI_UPDATE_TAG to install from that tag. Linux/systemd only.
Point CI workflow and DockerInit.sh to Xray v26.6.22 (update download URLs for Linux and Windows). Update go.mod to the matching github.com/xtls/xray-core pseudo-version and bump github.com/pion/stun to v3.1.6; refresh corresponding go.sum entries.
- handle-issue: use Sonnet 4.6 and raise max-turns 150 to 250
- handle-pr: use Opus 4.8; rewrite review as inline comments stating the problem plus a suggestion block, posted as one COMMENT review
- mention: use Opus 4.8; on issues do research only (never commit) with full comment/history context and feature-request feasibility analysis; PR commit-on-request behavior unchanged
- reformat the mention append-system-prompt into a readable multi-line block (verified it still parses as a single CLI argument)
claude-code-action checks out the PR head branch and pushes Claude's
commits with `git push origin ...`. For PRs opened from a fork the head
branch lives on the contributor's repo, and the workflow GITHUB_TOKEN
cannot push there, so commits ended up as a stray branch on this repo
and never landed on the PR.
Redirect origin's push URL to the PR head repository (the fork for fork
PRs, this repo otherwise) using a PAT secret (CLAUDE_BOT_PAT) that has
push access; fetches still come from origin. persist-credentials is
disabled so the PAT in the push URL is used instead of the GITHUB_TOKEN
auth header. Requires the fork PR to have "Allow edits by maintainers"
enabled.
Rename claude-issue-bot.yml to claude-bot.yml and broaden it beyond
issues:
- handle-pr: review pull requests on open (read diff, label, post one
grounded review comment); review-only, no code changes.
- mention: allow committing. Add Edit/Write and git tools, contents:
write, and instruct it to make the smallest correct change and commit
to the current branch only on an explicit code-change request. Kept
default user gating (no allowed_non_write_users) so only write-access
users can trigger commits.
- Refresh the repository map (add internal/eventbus and the
service/email subpackage) across all three prompts.
- Raise max-turns.
@