100 Commits
Author SHA1 Message Date
風雪 914053a7ac fix(i18n): declare listeners.httpHint and socksHint in zh-CN 2026-10-10 00:56:56 +08:00
風雪 d0f115011f fix(i18n): declare listeners.httpHint and socksHint in en 2026-10-10 00:56:55 +08:00
風雪 89ca102a25 fix(i18n): add zh-CN httpHint/socksHint for HTTP and SOCKS 2026-10-10 00:56:27 +08:00
風雪 6102c33f57 fix(i18n): add listeners.httpHint and socksHint for CI 2026-10-10 00:56:26 +08:00
風雪 97a445d5a1 fix(ui): restore ListenerConfigFields; optional TLS for HTTP/SOCKS 2026-10-10 00:45:18 +08:00
風雪 5d4430de0e fix(ui): restore ListenerConfigFields and enable HTTP/SOCKS optional TLS 2026-10-09 23:47:13 +08:00
風雪 d59b46a215 fix(ui): expose optional TLS form for HTTP and SOCKS listeners 2026-10-09 23:43:54 +08:00
風雪 5bb47282a0 Delete scripts/tests/test_ssh_menu.sh 2026-10-09 00:40:49 +08:00
風雪 c6cf289228 fix(scripts): make SSH menu config access indentation-agnostic (#101)
* fix(scripts): make SSH menu config access indentation-agnostic

Running the panel built from source exposed two defects that static review
could not see, because both depend on what the Go binary actually writes to
config.yaml.

The Go binary rewrites config.yaml through a YAML marshaller. `init` emits
two-space indentation, but any later rewrite — notably `reset-config`, the SSH
rescue path — emits four, and re-sorts the keys. Every shell helper matched on
a hardcoded two-space prefix, so the menu and the binary disagreed about the
file's shape.

1. Duplicate keys after an SSH rescue

After `reset-config`, `yaml_get '  port:'` matched nothing, so the menu showed
an empty current port. `yaml_set` then took its insert branch and appended a
two-space `port:` line alongside the marshaller's four-space one:

    server:
        mode: release
        port: 8080
        sub_port: 0
      port: 9090          <- inserted, duplicate

The menu then reported "Configured port: 9090" and restarted the service. The
failure mode matters more than the diff: SSH rescue is what an operator reaches
for when the panel is unreachable, which is exactly when the menu has to work.

2. `3m-ui config port <port>` left the panel unable to boot

That command hardcoded a two-space indent, producing mixed indentation inside
one block:

    server:
        mode: release
      port: 19090
        public_url: ""

The binary then refuses to start at all:

    failed to decode config YAML: yaml: line 12: did not find expected key

This command is documented in the README, so following the documentation
bricks the panel.

Both are fixed the same way: key lookup is now indentation-agnostic (matched
on $1 rather than a fixed prefix, which also removes any ambiguity between
`port:` and `sub_port:`), replacement preserves the indentation of the line it
rewrites, and insertion adopts the block's existing indent. Callers pass
"port:" instead of "  port:".

Also fixed: menu item 17 (Healthcheck) produced no output at all, because the
binary's healthcheck exits 0 silently on success. It now reports pass/fail
based on the exit status.

Verified against a panel compiled from source and running on loopback:
rescue -> change port -> restart yields a single `port:` key with consistent
indentation, config stays 0600, the panel serves HTTP 200, and the healthcheck
item reports OK. The regression script (scripts regression, 16 cases) covers
both defects by reproducing them against the old implementation first.

* fix(scripts): make SSH menu config access indentation-agnostic

Running the panel built from source exposed two defects that static review
could not see, because both depend on what the Go binary actually writes to
config.yaml.

The Go binary rewrites config.yaml through a YAML marshaller. `init` emits
two-space indentation, but any later rewrite — notably `reset-config`, the SSH
rescue path — emits four, and re-sorts the keys. Every shell helper matched on
a hardcoded two-space prefix, so the menu and the binary disagreed about the
file's shape.

1. Duplicate keys after an SSH rescue

After `reset-config`, `yaml_get '  port:'` matched nothing, so the menu showed
an empty current port. `yaml_set` then took its insert branch and appended a
two-space `port:` line alongside the marshaller's four-space one:

    server:
        mode: release
        port: 8080
        sub_port: 0
      port: 9090          <- inserted, duplicate

The menu then reported "Configured port: 9090" and restarted the service. The
failure mode matters more than the diff: SSH rescue is what an operator reaches
for when the panel is unreachable, which is exactly when the menu has to work.

2. `3m-ui config port <port>` left the panel unable to boot

That command hardcoded a two-space indent, producing mixed indentation inside
one block:

    server:
        mode: release
      port: 19090
        public_url: ""

The binary then refuses to start at all:

    failed to decode config YAML: yaml: line 12: did not find expected key

This command is documented in the README, so following the documentation
bricks the panel.

Both are fixed the same way: key lookup is now indentation-agnostic (matched
on $1 rather than a fixed prefix, which also removes any ambiguity between
`port:` and `sub_port:`), replacement preserves the indentation of the line it
rewrites, and insertion adopts the block's existing indent. Callers pass
"port:" instead of "  port:".

Also fixed: menu item 17 (Healthcheck) produced no output at all, because the
binary's healthcheck exits 0 silently on success. It now reports pass/fail
based on the exit status.

Verified against a panel compiled from source and running on loopback:
rescue -> change port -> restart yields a single `port:` key with consistent
indentation, config stays 0600, the panel serves HTTP 200, and the healthcheck
item reports OK. The regression script (scripts regression, 16 cases) covers
both defects by reproducing them against the old implementation first.

* test(scripts): add regression coverage for the SSH menu config helpers

The existing lifecycle tests exercise install.sh end to end but never touch
the menu's config read/write path, which is where both defects in the previous
commit lived. This adds 16 cases.

Each case runs the pre-fix implementation and the fixed one against the same
input and asserts both directions: the old one must reproduce the defect, the
new one must not exhibit it. A case whose "before" assertion passes is scored
FAIL — otherwise the suite could go green without ever having exercised the
bug.

Covers: block-scoped writes, 0600 preservation, `config port` with a missing
argument, the self-referencing entrypoint loop, insertion placement, missing
`server:` block, indentation-agnostic lookup, and indentation consistency.

The suite is self-contained: it needs no installed panel, writes only to a
temp dir, and the one case that touches /usr/local/bin/3m-ui skips itself if
that path already exists.
2026-10-09 00:39:10 +08:00
風雪 84eab7750c fix(scripts): scope SSH menu config writes to the server: block (#100)
* fix(scripts): scope SSH menu config writes to the server: block

The SSH management menu edited /etc/3m-ui/config.yaml with a global sed
substitution, which rewrote every identically-indented key in the file rather
than just the panel's. Changing the panel port therefore also rewrote unrelated
listener ports, and because the menu restarts the service right afterwards the
corruption went live immediately.

The same code path wrote the new file with `mv`, so a 0600 config holding the
JWT and credential-encryption keys came back as a umask-default 0644 file,
readable by every local user after any port or path change.

Both are fixed by scoping reads/writes to the `server:` block and by writing
back through the original inode (`cat > file`) instead of replacing it.

Three further defects found while auditing the menu:

- scripts/3m-ui.sh: `config port` read a bare "$2" under `set -u`, so running
  it without an argument aborted with "parameter not set" and the usage
  message below it was unreachable. Now "${2:-}".
- scripts/3m-ui.sh: the no-arg branch exec'd /usr/local/bin/3m-ui without
  checking whether it resolves back to this script, which spun in an endless
  exec loop on a broken entrypoint. Direct and indirect self-reference are now
  detected and reported with an actionable message.
- scripts/3m-ui: `enable_bbr` wrote into /etc/sysctl.d without creating it; on
  minimal/container images the redirect failed and `set -e` tore down the whole
  interactive menu. Now mkdir -p with a guarded failure.

Hardening and consistency:

- yaml_set reports an error instead of silently no-oping when neither the key
  nor a `server:` block exists; the menu no longer restarts the service for a
  change that never happened.
- yaml_get is scoped to the `server:` block so it matches yaml_set.
- temp files come from mktemp (0600) instead of a predictable .$$ name.
- open_firewall asks for confirmation before opening 22/80/443.
- status reports boot-time enablement on OpenRC, not only systemd.
- menu item 8 reports invalid input instead of returning silently.
- menu item 23 is relabelled: it updates all of 3m-ui, not only the script.
- the acme.sh helper is annotated as an example to review before running.

Verified locally: the listener port is left untouched when the panel port
changes, config mode stays 0600, `config port` with no argument prints usage,
the self-referencing entrypoint fails fast instead of looping, and both files
pass `sh -n`.

* fix(scripts): scope SSH menu config writes to the server: block

The SSH management menu edited /etc/3m-ui/config.yaml with a global sed
substitution, which rewrote every identically-indented key in the file rather
than just the panel's. Changing the panel port therefore also rewrote unrelated
listener ports, and because the menu restarts the service right afterwards the
corruption went live immediately.

The same code path wrote the new file with `mv`, so a 0600 config holding the
JWT and credential-encryption keys came back as a umask-default 0644 file,
readable by every local user after any port or path change.

Both are fixed by scoping reads/writes to the `server:` block and by writing
back through the original inode (`cat > file`) instead of replacing it.

Three further defects found while auditing the menu:

- scripts/3m-ui.sh: `config port` read a bare "$2" under `set -u`, so running
  it without an argument aborted with "parameter not set" and the usage
  message below it was unreachable. Now "${2:-}".
- scripts/3m-ui.sh: the no-arg branch exec'd /usr/local/bin/3m-ui without
  checking whether it resolves back to this script, which spun in an endless
  exec loop on a broken entrypoint. Direct and indirect self-reference are now
  detected and reported with an actionable message.
- scripts/3m-ui: `enable_bbr` wrote into /etc/sysctl.d without creating it; on
  minimal/container images the redirect failed and `set -e` tore down the whole
  interactive menu. Now mkdir -p with a guarded failure.

Hardening and consistency:

- yaml_set reports an error instead of silently no-oping when neither the key
  nor a `server:` block exists; the menu no longer restarts the service for a
  change that never happened.
- yaml_get is scoped to the `server:` block so it matches yaml_set.
- temp files come from mktemp (0600) instead of a predictable .$$ name.
- open_firewall asks for confirmation before opening 22/80/443.
- status reports boot-time enablement on OpenRC, not only systemd.
- menu item 8 reports invalid input instead of returning silently.
- menu item 23 is relabelled: it updates all of 3m-ui, not only the script.
- the acme.sh helper is annotated as an example to review before running.

Verified locally: the listener port is left untouched when the panel port
changes, config mode stays 0600, `config port` with no argument prints usage,
the self-referencing entrypoint fails fast instead of looping, and both files
pass `sh -n`.
2026-10-08 23:49:39 +08:00
7099315eb1 feat(proxy): expose per-proxy tfo / mptcp (#97)
* feat(proxy): expose per-proxy tfo / mptcp (TCP Fast Open / Multipath TCP)

Add Mihomo's proxy-level `tfo` and `mptcp` options (wiki.metacubex.one/config/proxies)
to the panel so clients can opt into TCP Fast Open / Multipath TCP on a per-node basis.

Mapping accuracy:
- These are PROXY-level keys, distinct from the global `inbound-tfo` / `inbound-mptcp`
  general switches. They travel through ProxyEntry.Options (the same catch-all that already
  feeds both the serving config and the client subscription document), so they surface in
  BOTH the server mihomo config and the generated client subscription config — no field is
  lost or misrouted.

Frontend (web + mobile share the same Config page / responsive Modal):
- Proxy editor gains TFO (TCP Fast Open) and MPTCP (Multipath TCP) switches.
- An info note states they only take effect on TCP transports.
- Modal body becomes scrollable on phones (maxHeight 78vh) so the added rows stay reachable
  on small screens; width/top already adapt via useIsMobile.

i18n: zh-CN / en strings for proxyTfo, proxyMptcp, proxyTcpOnly.

Tests:
- config: tfo/mptcp serialize into proxy YAML and round-trip; absent stays absent, explicit
  false is preserved.
- converter: client subscription document preserves tfo/mptcp from visual proxies.

* fix(i18n): add missing commas after proxy TFO/MPTCP keys

---------

Co-authored-by: 3m-ui dev <dev@local>
Co-authored-by: kazeyukiro <kazeyukiro@users.noreply.github.com>
2026-10-05 22:18:50 +08:00
d26b7e1534 feat(config): add inbound-tfo / inbound-mptcp global kernel settings (#96)
* feat(config): add inbound-tfo / inbound-mptcp global kernel settings

Expose Mihomo's top-level `inbound-tfo` and `inbound-mptcp` general keys
(TCP Fast Open / Multipath TCP applied to every inbound listener socket) in
the panel. These are GLOBAL switches, not per-listener fields, so they are
carried on VisualConfig — the same struct that feeds the serving Mihomo
config fragment — and never copied into client subscription documents.

Backend:
- VisualConfig gains InboundTfo / InboundMPTCP (json inboundTfo/inboundMptcp,
  yaml inbound-tfo/inbound-mptcp), matching Mihomo's exact key names.
- MihomoConfig gains the same keys (omitempty) for schema completeness.
- GenerateFinalConfig already merges the visual-config fragment over the
  template (only listeners/rules/proxies/groups are stripped), so the keys
  surface in the serving config exactly as Mihomo expects.

Frontend:
- Config page gains a 'General' card (mode, log level, allow-lan, ipv6,
  inbound-tfo, inbound-mptcp) loaded from /config/visual and saved via
  POST /config/visual. The full VisualConfig is preserved on save so existing
  proxies/groups/rules/DNS are not clobbered.
- i18n zh-CN / en strings added.

Tests:
- config package tests assert inbound-tfo / inbound-mptcp appear as `true`
  in GenerateFinalConfig output when enabled and never become `true` when off.

* fix: gofmt visual.go; type saveVisualConfig payload as VisualConfig

---------

Co-authored-by: 3m-ui dev <dev@local>
Co-authored-by: kazeyukiro <kazeyukiro@users.noreply.github.com>
2026-10-05 22:16:49 +08:00
風雪and3m-ui dev 9b1d243dc5 fix(ui): stop the port field from showing the required error twice (#95)
The port Form.Item had both a `required` rule and a custom validator
that rejected on empty with the same message. AntD renders every failed
rule, so an empty port produced the identical "Port is required"
message twice.

Let `required` own the empty case and have the validator only check the
format (returning early on empty). Add `whitespace: true` so a
spaces-only value still counts as empty.

Co-authored-by: 3m-ui dev <dev@local>
2026-10-05 20:06:25 +08:00
風雪and風雪 2ac3714619 fix(config): also let TCP+UDP coexist on the same port at generate time (#94)
PR #91 made the creation-time guards (ensureEndpointAvailable,
ensureBatchEndpointsAvailable) transport-aware so a TCP listener and a UDP
listener can share an address:port. But the third copy of the conflict check
inside Mihomo config generation — validateListenerEndpoints in
internal/mihomo/config/generator.go — was left untouched. So a TCP+UDP pair
that creation now permits was still rejected when the panel regenerated the
Mihomo config, surfacing as:

  warning: Mihomo config generate after listener change failed: ...
  listeners "节点2" and "节点5" have conflicting bind address/port ranges
  (0.0.0.0:16201 and 0.0.0.0:16201)

Make validateListenerEndpoints reuse protocol.ListenerTransports and only flag
a conflict when the two listeners share an L4 transport, matching the
creation-time guard. Same-protocol pairs on the same address:port still
conflict.

Tests:
- TestValidateListenerEndpointsAllowsTCPandUDPOnSamePort
- TestValidateListenerEndpointsRejectsTwoTCPOnSamePort
- TestValidateListenerEndpointsRejectsTwoUDPOnSamePort

Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
2026-10-05 19:09:18 +08:00
風雪and風雪 d73613387d fix(backup): snapshot the live DB with VACUUM INTO instead of a racy file copy (#92)
The in-app "Download backup" button exported the SQLite file with a plain
`os.Open` + `io.Copy` while the panel's GORM pool was still open and writing.
That copy races with concurrent writes (torn pages) and, even after the prior
WAL checkpoint, could leave committed frames in -wal that the copy never
replayed — an inconsistent or partially-lost backup.

Replace the checkpoint + file copy with SQLite's online backup primitive
VACUUM INTO, which reads through the WAL and in-flight transactions to produce
a self-contained, fully-replayed snapshot without stopping the panel.

Also stop bundling 3m-ui.db-wal / 3m-ui.db-shm into the zip. The VACUUM INTO
result is already complete (WAL merged in), so the sidecars were dead weight —
and they were never restored on import anyway, which silently dropped
un-checkpointed writes on restore.

Tests:
- TestWriteZipSnapshotIsConsistentAndSelfContained: a WAL-mode db with 200 rows
  held open by a live connection; WriteZip produces a zip whose 3m-ui.db opens
  standalone with all 200 rows and contains no -wal/-shm entries.
- go vet + go test -tags sqlite_modernc ./... green.

Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
2026-10-05 17:17:27 +08:00
風雪and風雪 f18419da8b fix(listener): allow TCP and UDP listeners to share a port (#91)
The endpoint conflict guard in ensureEndpointAvailable (and its batch
counterpart ensureBatchEndpointsAvailable) compared only port ranges and
bind addresses — it never looked at the L4 protocol. A TCP listener on
443 and a UDP listener on 443 are independent sockets at the OS level and
Mihomo serves both fine, but 3m-ui rejected the second one with
"conflicts on 0.0.0.0:443". So you could not run e.g. VLESS (TCP) and
TUIC/Hysteria2 (UDP) on the same port.

Add protocol.ListenerTransports, which maps a listener's protocol kind to
the L4 transport(s) its inbound binds (UDP for tuic / hysteria /
hysteria2 / shadowquic / mieru; TCP for everything else). The proxy `udp`
toggle is UDP relay over an established connection and is intentionally
ignored — it does not open a UDP listening socket. The conflict check now
also requires the two listeners to share an L4 transport, so a TCP+UDP
pair on the same address:port no longer collides while two TCP or two UDP
listeners still do.

Tests:
- protocol.ListenerTransports unit tests (udp/tcp/case-insensitive).
- listener.listenersShareTransport unit tests.
- End-to-end: a TCP+UDP pair on 443 coexists and the UDP listener stays on
  443 through Create (it is not silently reassigned to a free port), while
  two TCP or two UDP on 443 still conflict.

Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
2026-10-05 16:51:18 +08:00
風雪and風雪 ee14b06680 fix(ui): stop reusing the port help text as the validation error (#90)
The create/edit listener form set the port field's `tooltip` and its
`required`/format validation `message` to the same i18n key (`portHint`).
When the field was empty or malformed, the identical "监听端口(wiki:
port)。单端口或官方 ports 语法。" string showed up twice — once as the
hover help and once as the red validation error — which read as two
duplicate hints.

Split the validation copy out of the help string: `portRequired` for the
empty case and `portFormat` for the syntax case, while `portHint` stays
purely as the hover help. The "ports syntax" phrasing itself is correct
per the Mihomo listener docs (port supports single port or
comma/hyphen ranges), so only the duplication is fixed.

Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
2026-10-05 16:51:03 +08:00
風雪 96dc3b38a2 style: align VMess test fields 2026-09-30 22:06:49 +08:00
風雪 e75a5847d7 style: format VMess changes 2026-09-30 22:04:42 +08:00
風雪 0f2da42e02 style: format VMess changes 2026-09-30 22:04:38 +08:00
風雪 82a75716aa style: format VMess changes 2026-09-30 22:04:33 +08:00
風雪 8c0779afa6 ci: diagnose VMess gofmt failure 2026-09-30 22:02:05 +08:00
風雪 35886e6b02 test: cover VMess mux client export 2026-09-30 22:00:18 +08:00
風雪 fcd5fb8dee test: preserve VMess per-user alterId 2026-09-30 22:00:11 +08:00
風雪 3b966b3245 feat: expose complete VMess TLS Mirror options 2026-09-30 21:59:59 +08:00
風雪 72b03cd029 fix: export VMess listener mux as client smux 2026-09-30 21:59:41 +08:00
風雪 caff433bcc fix: preserve VMess per-user alterId on compile 2026-09-30 21:59:34 +08:00
風雪 8fe2f9ebb3 test: provide required listener fields 2026-09-30 21:48:17 +08:00
風雪 8ad38ca2d5 ci: restore format check 2026-09-30 21:45:38 +08:00
風雪 f75619f0d1 style: gofmt URI extended file 2026-09-30 21:45:35 +08:00
風雪 e830b02440 ci: diagnose remaining gofmt difference 2026-09-30 21:43:55 +08:00
風雪 1f6f3b57b0 ci: restore format check 2026-09-30 21:42:09 +08:00
風雪 b061b1436e fix: import strings for Sudoku URI serialization 2026-09-30 21:42:05 +08:00
風雪 faf53ae934 fix: remove duplicate Mieru validation case 2026-09-30 21:42:02 +08:00
風雪 31b9e110df ci: print gofmt offenders for debugging 2026-09-30 21:40:09 +08:00
風雪 266a3d20b0 fix: merge protocol-specific listener validation cases 2026-09-30 21:38:25 +08:00
風雪 8417c61645 test: format listener schema regression tests 2026-09-30 21:38:23 +08:00
風雪 e36d44bb4b test: format Sudoku URI regression test 2026-09-30 21:38:19 +08:00
風雪 af609b746b fix(tests): restore validator tests and append listener coverage 2026-09-30 21:32:01 +08:00
風雪 4f864d8859 test(listener): validate documented protocol values 2026-09-30 21:31:35 +08:00
風雪 5c98af6ca4 fix(listener): validate Sudoku Mieru and TrustTunnel values 2026-09-30 21:31:24 +08:00
風雪 984ba9a7d8 fix(listener): persist Hysteria2 Realm TLS options 2026-09-30 21:31:16 +08:00
風雪 e91cbb923e feat(listener): expose Hysteria2 Realm TLS options 2026-09-30 21:31:03 +08:00
風雪 3d4c2a8288 test(converter): keep Hysteria2 realm options server-side 2026-09-30 21:30:47 +08:00
風雪 cccd13784d fix(converter): do not export Hysteria2 server realm options to clients 2026-09-30 21:30:43 +08:00
風雪 5792bd1978 test(uri): cover Sudoku nested URI serialization 2026-09-30 21:30:03 +08:00
風雪 726781b7be fix(uri): serialize Sudoku listener options without object coercion 2026-09-30 21:30:00 +08:00
風雪 3d0b9d40be fix(listener): correct Sudoku server fields and custom table serialization 2026-09-30 21:29:56 +08:00
風雪 d1e49a6ad3 Update singbox.go
Update singbox.go
Update singbox.go
Update singbox.go
Update singbox.go
Update singbox.go
Update singbox.go
Update singbox.go
Update singbox.go
Update singbox.go
Update singbox.go
Update singbox.go
2026-09-28 23:44:33 +08:00
風雪and風雪 35f4c521c3 fix(vless): stop exporting flow on transports that cannot carry it (#87)
* fix(vless): stop exporting flow on transports that cannot carry it
The form promised that flow is cleared for ws/grpc/xhttp and the tooltip still
says so, but exported links kept carrying flow=xtls-rprx-vision. Two separate
reasons, both fixed.
The clear came too late to matter. formValuesToConfig wrote `flow` into the
config inside the protocol switch, and only afterwards did the "Vision is
TCP-only" rule run — where it mutated the form state, not the config it had
already been copied into. The transport is now resolved before anything writes,
and the drop also removes a flow inherited from the previous config, so changing
a listener's transport actually takes effect.
The URI generators never asked. Every one of them emitted whatever flow it found:
- node: vlessURIs decided flow before resolving the transport, so the type it
had just computed was irrelevant to the decision. It now resolves the
transport first. ClientURIsWithCredentials also merged the listener-level
flow onto every user on protocol alone, which reintroduced the value the
form had dropped.
- protocol: the share URI and the client YAML both wrote spec.Flow unguarded,
even though TransportSpec knows the network. Same for the server-side
compile, which pushed the listener default onto users — Mihomo rejects that
combination outright, so this one takes the listener down.
- mui: BuildVLESSShare set flow unconditionally, and bridge.go injected the
listener default into users before it was known to be applicable.
- converter: copyTransport resolved `network` but left a stale flow behind.
The rule is now one predicate per representation: TransportSpec.CarriesFlow,
VLESSHandlerSpec.CarriesFlow, and protocol.TransportCarriesFlow for raw configs.
TCP/raw keeps Vision; ws, grpc, xhttp, mkcp and mekya never emit it.
Tests pin each path in both directions — a stale flow disappears on ws/grpc/
xhttp, and a genuine TCP listener keeps its flow. The frontend one runs the real
form builder and fails, in three of five cases, against the previous ordering.
* fix(uri): carry vmess ws Host and align hysteria2 TLS flags across user shapes
Auditing the remaining exporters after the flow fix turned up two more places
where the link disagreed with the listener.
vmessURIs built its JSON by hand instead of going through transportParams, so a
ws listener that pins a Host header exported only the path: the client had
nothing to send as Host and the listener was unreachable through the link, even
though the client YAML built from the same config worked. It also dropped
allowInsecure, which vless and trojan both carry — a listener whose certificate
the client cannot verify exported a link that failed the handshake. The repeated
tlsParams() calls were hoisted while touching this.
hysteria2URIs reads users either as rows or as a map, and the two branches
disagreed: one asked certutil.ShouldSkipCertVerify and emitted only `insecure`,
the other asked clientSkipCert and emitted `insecure` plus `allowInsecure`. The
same listener therefore produced different links depending on how its users were
stored. Both now use clientSkipCert, the helper intended for share URIs, and
emit both keys.
Not fixed, for the record: an SS listener with simple-obfs exports a bare ss://
link with no `plugin=` entry, so the obfs configuration is lost. The client YAML
does translate it (plugin: obfs). Fixing the URI means committing to a SIP002
plugin encoding that varies between clients, which is worth deciding separately
rather than guessing at here.
* i18n: complete zh-CN and guard against the locale regressions already seen
zh-CN was missing seven listener hints that en.ts declares, so those tooltips
rendered in English for the primary UI language. Translated: client_auth_cert,
client_auth_type, ech_key, enable_pure_downlink, mux, padding and table_type.
The rest of the catalogue is deliberately left alone. Seventeen locales each lack
65 keys that en.ts has; the provider already resolves those from en.ts, so they
degrade to English rather than breaking, and filling 1100 strings with
unreviewed machine translation would be worse than the fallback. The test prints
the per-locale gap so it stays visible.
The new locale test guards the regressions this directory has actually hit: a
duplicated key inside one object (which breaks the type check), and a key renamed
in en.ts leaving an orphan behind. It also asserts zh-CN stays complete, since a
gap there reads as a defect rather than as translation lag.
* i18n: declare the 24 keys the UI asks for but no locale ever defined
Twenty-four keys were referenced from components without being declared in any
locale, so t() fell through to its hardcoded fallback. Where a fallback existed
that merely meant the string could never be translated; where none existed —
protocolHint, access_sniHint, jlsHint, shadowTlsHint and the rest — the tooltip
either rendered the raw key, "listeners.protocolHint", or disappeared entirely,
because fieldTip() suppresses a value that equals the key it looked up.
en.ts takes the text the code was already falling back to, so nothing changes on
screen; the strings just become translatable. The few without a fallback were
written from the surrounding context — the field they annotate, its placeholder,
and the wording of the neighbouring hints. trafficMultiplierHint is the exception:
its fallback was Chinese, which is wrong for the English catalogue, so it now has
a real English string.
zh-CN gets all twenty-four, which keeps it complete against en.ts.
The locale test now reads through the TypeScript AST. It previously parsed line by
line and these files contain minified regions — several "key":"value" pairs on one
line — so a whole block of keys was invisible to it, which is how this gap stayed
hidden. It also gained the assertion that would have caught the bug directly:
every dotted key handed to t() or fieldTip() must be declared in en.ts.
The remaining languages still lag, now by 89 keys rather than 65, and still fall
back to English; the test prints the per-locale figure.
* fix(uri): drop the vmess allowInsecure field the reference client ignores
The vmess skip-certificate field was added on internal consistency alone —
vless and trojan carry allowInsecure in their query strings, so vmess looked
like an oversight. Checked against the client that actually parses these links,
it is not one.
v2rayN's VmessQRCode model declares exactly: v, ps, add, port, id, aid, scy,
net, type, host, path, tls, sni, alpn, fp. There is no skip-certificate field
at all — neither allowInsecure nor insecure — so the field was dead weight in a
payload that v2rayN reads field by field. v2rayNG reads `insecure` instead, and
Xray-core has removed allowInsecure in favour of pinnedPeerCertSha256, so there
is no spelling that the reference clients agree on. Emitting one would have
produced a link that looks configured but is not.
The ws `host` fix stands: v2rayN's own documentation lists "ws host" as the
meaning of that field, so dropping it broke ws listeners that pin a Host.
The conformance test now pins the emitted payload to v2rayN's field set, so a
future field copied over from another protocol fails instead of being silently
ignored by every client.
Deliberately left alone, both confirmed against the official scheme and worth
deciding separately:
- hysteria2 links carry up/down. The Hysteria 2 URI specification is explicit
that the parameters "should never include client modes ... or bandwidth
values". Removing them changes behaviour for anyone relying on the link, so
it is flagged rather than removed. `insecure` is the official parameter;
allowInsecure is kept alongside it purely for clients that read that name.
- an SS listener with simple-obfs still exports a bare ss:// link with no
plugin entry, losing the obfuscation the client YAML does translate.
Also noted while checking: tuicURIs defaults congestion_control to bbr where
sing-box documents cubic as its default. Ours is set explicitly, which is
valid, but the comment claiming it is what "most TUIC clients" use is not
sourced.
* fix(uri): keep vmess allowInsecure after all
Reverts the revert. Dropping it was correct on the evidence and wrong on the
consequences: v2rayN ignores the field, v2rayNG spells it `insecure`, and
Xray-core has moved to pinnedPeerCertSha256 — but the clients that do read
allowInsecure are precisely the ones whose handshake fails without it, and an
ignored extra key costs the others nothing. Removing it would have traded a
working link for a tidy payload.
The conformance test keeps its guard, with allowInsecure recorded as an
explicit, justified exception rather than a silent inclusion: the field set is
still pinned to v2rayN's model, so any further field copied over from another
protocol has to argue its way in.
* fix(uri): restore the vmess ws Host in registry YAML, and stop dropping share links
Two leftovers from auditing the exporters end to end.
The registry's VMess compiler built ws-opts with the path only, so a listener
that pins a Host exported a client config with nothing to send as Host. The
vless YAML path in the same file already nests it under `headers` per the mihomo
wiki, so this just mirrors that — and the converter has always carried it, which
is how the difference went unnoticed.
The node URI share endpoint also had a tier that could not be reached. It walks
three tiers — m-ui, the registry, then the legacy builders — and the second one
gates the third on having neither a URI nor client YAML. But the registry
implements URI export for a different protocol set than the legacy builders: it
compiles vmess, snell, shadowquic, mieru, sudoku and trusttunnel to client YAML
without ever emitting a URI, so those listeners arrived at the third tier with a
YAML in hand and the condition already false, and reported no link at all even
though ClientURIsWithCredentials exports one. The third tier now runs whenever
the URI is still missing, and only fills in YAML that earlier tiers did not
already provide.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
fix(vless): stop exporting flow on transports that cannot carry it (#87)
* fix(vless): stop exporting flow on transports that cannot carry it
The form promised that flow is cleared for ws/grpc/xhttp and the tooltip still
says so, but exported links kept carrying flow=xtls-rprx-vision. Two separate
reasons, both fixed.
The clear came too late to matter. formValuesToConfig wrote `flow` into the
config inside the protocol switch, and only afterwards did the "Vision is
TCP-only" rule run — where it mutated the form state, not the config it had
already been copied into. The transport is now resolved before anything writes,
and the drop also removes a flow inherited from the previous config, so changing
a listener's transport actually takes effect.
The URI generators never asked. Every one of them emitted whatever flow it found:
- node: vlessURIs decided flow before resolving the transport, so the type it
had just computed was irrelevant to the decision. It now resolves the
transport first. ClientURIsWithCredentials also merged the listener-level
flow onto every user on protocol alone, which reintroduced the value the
form had dropped.
- protocol: the share URI and the client YAML both wrote spec.Flow unguarded,
even though TransportSpec knows the network. Same for the server-side
compile, which pushed the listener default onto users — Mihomo rejects that
combination outright, so this one takes the listener down.
- mui: BuildVLESSShare set flow unconditionally, and bridge.go injected the
listener default into users before it was known to be applicable.
- converter: copyTransport resolved `network` but left a stale flow behind.
The rule is now one predicate per representation: TransportSpec.CarriesFlow,
VLESSHandlerSpec.CarriesFlow, and protocol.TransportCarriesFlow for raw configs.
TCP/raw keeps Vision; ws, grpc, xhttp, mkcp and mekya never emit it.
Tests pin each path in both directions — a stale flow disappears on ws/grpc/
xhttp, and a genuine TCP listener keeps its flow. The frontend one runs the real
form builder and fails, in three of five cases, against the previous ordering.
* fix(uri): carry vmess ws Host and align hysteria2 TLS flags across user shapes
Auditing the remaining exporters after the flow fix turned up two more places
where the link disagreed with the listener.
vmessURIs built its JSON by hand instead of going through transportParams, so a
ws listener that pins a Host header exported only the path: the client had
nothing to send as Host and the listener was unreachable through the link, even
though the client YAML built from the same config worked. It also dropped
allowInsecure, which vless and trojan both carry — a listener whose certificate
the client cannot verify exported a link that failed the handshake. The repeated
tlsParams() calls were hoisted while touching this.
hysteria2URIs reads users either as rows or as a map, and the two branches
disagreed: one asked certutil.ShouldSkipCertVerify and emitted only `insecure`,
the other asked clientSkipCert and emitted `insecure` plus `allowInsecure`. The
same listener therefore produced different links depending on how its users were
stored. Both now use clientSkipCert, the helper intended for share URIs, and
emit both keys.
Not fixed, for the record: an SS listener with simple-obfs exports a bare ss://
link with no `plugin=` entry, so the obfs configuration is lost. The client YAML
does translate it (plugin: obfs). Fixing the URI means committing to a SIP002
plugin encoding that varies between clients, which is worth deciding separately
rather than guessing at here.
* i18n: complete zh-CN and guard against the locale regressions already seen
zh-CN was missing seven listener hints that en.ts declares, so those tooltips
rendered in English for the primary UI language. Translated: client_auth_cert,
client_auth_type, ech_key, enable_pure_downlink, mux, padding and table_type.
The rest of the catalogue is deliberately left alone. Seventeen locales each lack
65 keys that en.ts has; the provider already resolves those from en.ts, so they
degrade to English rather than breaking, and filling 1100 strings with
unreviewed machine translation would be worse than the fallback. The test prints
the per-locale gap so it stays visible.
The new locale test guards the regressions this directory has actually hit: a
duplicated key inside one object (which breaks the type check), and a key renamed
in en.ts leaving an orphan behind. It also asserts zh-CN stays complete, since a
gap there reads as a defect rather than as translation lag.
* i18n: declare the 24 keys the UI asks for but no locale ever defined
Twenty-four keys were referenced from components without being declared in any
locale, so t() fell through to its hardcoded fallback. Where a fallback existed
that merely meant the string could never be translated; where none existed —
protocolHint, access_sniHint, jlsHint, shadowTlsHint and the rest — the tooltip
either rendered the raw key, "listeners.protocolHint", or disappeared entirely,
because fieldTip() suppresses a value that equals the key it looked up.
en.ts takes the text the code was already falling back to, so nothing changes on
screen; the strings just become translatable. The few without a fallback were
written from the surrounding context — the field they annotate, its placeholder,
and the wording of the neighbouring hints. trafficMultiplierHint is the exception:
its fallback was Chinese, which is wrong for the English catalogue, so it now has
a real English string.
zh-CN gets all twenty-four, which keeps it complete against en.ts.
The locale test now reads through the TypeScript AST. It previously parsed line by
line and these files contain minified regions — several "key":"value" pairs on one
line — so a whole block of keys was invisible to it, which is how this gap stayed
hidden. It also gained the assertion that would have caught the bug directly:
every dotted key handed to t() or fieldTip() must be declared in en.ts.
The remaining languages still lag, now by 89 keys rather than 65, and still fall
back to English; the test prints the per-locale figure.
* fix(uri): drop the vmess allowInsecure field the reference client ignores
The vmess skip-certificate field was added on internal consistency alone —
vless and trojan carry allowInsecure in their query strings, so vmess looked
like an oversight. Checked against the client that actually parses these links,
it is not one.
v2rayN's VmessQRCode model declares exactly: v, ps, add, port, id, aid, scy,
net, type, host, path, tls, sni, alpn, fp. There is no skip-certificate field
at all — neither allowInsecure nor insecure — so the field was dead weight in a
payload that v2rayN reads field by field. v2rayNG reads `insecure` instead, and
Xray-core has removed allowInsecure in favour of pinnedPeerCertSha256, so there
is no spelling that the reference clients agree on. Emitting one would have
produced a link that looks configured but is not.
The ws `host` fix stands: v2rayN's own documentation lists "ws host" as the
meaning of that field, so dropping it broke ws listeners that pin a Host.
The conformance test now pins the emitted payload to v2rayN's field set, so a
future field copied over from another protocol fails instead of being silently
ignored by every client.
Deliberately left alone, both confirmed against the official scheme and worth
deciding separately:
- hysteria2 links carry up/down. The Hysteria 2 URI specification is explicit
that the parameters "should never include client modes ... or bandwidth
values". Removing them changes behaviour for anyone relying on the link, so
it is flagged rather than removed. `insecure` is the official parameter;
allowInsecure is kept alongside it purely for clients that read that name.
- an SS listener with simple-obfs still exports a bare ss:// link with no
plugin entry, losing the obfuscation the client YAML does translate.
Also noted while checking: tuicURIs defaults congestion_control to bbr where
sing-box documents cubic as its default. Ours is set explicitly, which is
valid, but the comment claiming it is what "most TUIC clients" use is not
sourced.
* fix(uri): keep vmess allowInsecure after all
Reverts the revert. Dropping it was correct on the evidence and wrong on the
consequences: v2rayN ignores the field, v2rayNG spells it `insecure`, and
Xray-core has moved to pinnedPeerCertSha256 — but the clients that do read
allowInsecure are precisely the ones whose handshake fails without it, and an
ignored extra key costs the others nothing. Removing it would have traded a
working link for a tidy payload.
The conformance test keeps its guard, with allowInsecure recorded as an
explicit, justified exception rather than a silent inclusion: the field set is
still pinned to v2rayN's model, so any further field copied over from another
protocol has to argue its way in.
* fix(uri): restore the vmess ws Host in registry YAML, and stop dropping share links
Two leftovers from auditing the exporters end to end.
The registry's VMess compiler built ws-opts with the path only, so a listener
that pins a Host exported a client config with nothing to send as Host. The
vless YAML path in the same file already nests it under `headers` per the mihomo
wiki, so this just mirrors that — and the converter has always carried it, which
is how the difference went unnoticed.
The node URI share endpoint also had a tier that could not be reached. It walks
three tiers — m-ui, the registry, then the legacy builders — and the second one
gates the third on having neither a URI nor client YAML. But the registry
implements URI export for a different protocol set than the legacy builders: it
compiles vmess, snell, shadowquic, mieru, sudoku and trusttunnel to client YAML
without ever emitting a URI, so those listeners arrived at the third tier with a
YAML in hand and the condition already false, and reported no link at all even
though ClientURIsWithCredentials exports one. The third tier now runs whenever
the URI is still missing, and only fills in YAML that earlier tiers did not
already provide.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
fix(vless): stop exporting flow on transports that cannot carry it (#87)
* fix(vless): stop exporting flow on transports that cannot carry it
The form promised that flow is cleared for ws/grpc/xhttp and the tooltip still
says so, but exported links kept carrying flow=xtls-rprx-vision. Two separate
reasons, both fixed.
The clear came too late to matter. formValuesToConfig wrote `flow` into the
config inside the protocol switch, and only afterwards did the "Vision is
TCP-only" rule run — where it mutated the form state, not the config it had
already been copied into. The transport is now resolved before anything writes,
and the drop also removes a flow inherited from the previous config, so changing
a listener's transport actually takes effect.
The URI generators never asked. Every one of them emitted whatever flow it found:
- node: vlessURIs decided flow before resolving the transport, so the type it
had just computed was irrelevant to the decision. It now resolves the
transport first. ClientURIsWithCredentials also merged the listener-level
flow onto every user on protocol alone, which reintroduced the value the
form had dropped.
- protocol: the share URI and the client YAML both wrote spec.Flow unguarded,
even though TransportSpec knows the network. Same for the server-side
compile, which pushed the listener default onto users — Mihomo rejects that
combination outright, so this one takes the listener down.
- mui: BuildVLESSShare set flow unconditionally, and bridge.go injected the
listener default into users before it was known to be applicable.
- converter: copyTransport resolved `network` but left a stale flow behind.
The rule is now one predicate per representation: TransportSpec.CarriesFlow,
VLESSHandlerSpec.CarriesFlow, and protocol.TransportCarriesFlow for raw configs.
TCP/raw keeps Vision; ws, grpc, xhttp, mkcp and mekya never emit it.
Tests pin each path in both directions — a stale flow disappears on ws/grpc/
xhttp, and a genuine TCP listener keeps its flow. The frontend one runs the real
form builder and fails, in three of five cases, against the previous ordering.
* fix(uri): carry vmess ws Host and align hysteria2 TLS flags across user shapes
Auditing the remaining exporters after the flow fix turned up two more places
where the link disagreed with the listener.
vmessURIs built its JSON by hand instead of going through transportParams, so a
ws listener that pins a Host header exported only the path: the client had
nothing to send as Host and the listener was unreachable through the link, even
though the client YAML built from the same config worked. It also dropped
allowInsecure, which vless and trojan both carry — a listener whose certificate
the client cannot verify exported a link that failed the handshake. The repeated
tlsParams() calls were hoisted while touching this.
hysteria2URIs reads users either as rows or as a map, and the two branches
disagreed: one asked certutil.ShouldSkipCertVerify and emitted only `insecure`,
the other asked clientSkipCert and emitted `insecure` plus `allowInsecure`. The
same listener therefore produced different links depending on how its users were
stored. Both now use clientSkipCert, the helper intended for share URIs, and
emit both keys.
Not fixed, for the record: an SS listener with simple-obfs exports a bare ss://
link with no `plugin=` entry, so the obfs configuration is lost. The client YAML
does translate it (plugin: obfs). Fixing the URI means committing to a SIP002
plugin encoding that varies between clients, which is worth deciding separately
rather than guessing at here.
* i18n: complete zh-CN and guard against the locale regressions already seen
zh-CN was missing seven listener hints that en.ts declares, so those tooltips
rendered in English for the primary UI language. Translated: client_auth_cert,
client_auth_type, ech_key, enable_pure_downlink, mux, padding and table_type.
The rest of the catalogue is deliberately left alone. Seventeen locales each lack
65 keys that en.ts has; the provider already resolves those from en.ts, so they
degrade to English rather than breaking, and filling 1100 strings with
unreviewed machine translation would be worse than the fallback. The test prints
the per-locale gap so it stays visible.
The new locale test guards the regressions this directory has actually hit: a
duplicated key inside one object (which breaks the type check), and a key renamed
in en.ts leaving an orphan behind. It also asserts zh-CN stays complete, since a
gap there reads as a defect rather than as translation lag.
* i18n: declare the 24 keys the UI asks for but no locale ever defined
Twenty-four keys were referenced from components without being declared in any
locale, so t() fell through to its hardcoded fallback. Where a fallback existed
that merely meant the string could never be translated; where none existed —
protocolHint, access_sniHint, jlsHint, shadowTlsHint and the rest — the tooltip
either rendered the raw key, "listeners.protocolHint", or disappeared entirely,
because fieldTip() suppresses a value that equals the key it looked up.
en.ts takes the text the code was already falling back to, so nothing changes on
screen; the strings just become translatable. The few without a fallback were
written from the surrounding context — the field they annotate, its placeholder,
and the wording of the neighbouring hints. trafficMultiplierHint is the exception:
its fallback was Chinese, which is wrong for the English catalogue, so it now has
a real English string.
zh-CN gets all twenty-four, which keeps it complete against en.ts.
The locale test now reads through the TypeScript AST. It previously parsed line by
line and these files contain minified regions — several "key":"value" pairs on one
line — so a whole block of keys was invisible to it, which is how this gap stayed
hidden. It also gained the assertion that would have caught the bug directly:
every dotted key handed to t() or fieldTip() must be declared in en.ts.
The remaining languages still lag, now by 89 keys rather than 65, and still fall
back to English; the test prints the per-locale figure.
* fix(uri): drop the vmess allowInsecure field the reference client ignores
The vmess skip-certificate field was added on internal consistency alone —
vless and trojan carry allowInsecure in their query strings, so vmess looked
like an oversight. Checked against the client that actually parses these links,
it is not one.
v2rayN's VmessQRCode model declares exactly: v, ps, add, port, id, aid, scy,
net, type, host, path, tls, sni, alpn, fp. There is no skip-certificate field
at all — neither allowInsecure nor insecure — so the field was dead weight in a
payload that v2rayN reads field by field. v2rayNG reads `insecure` instead, and
Xray-core has removed allowInsecure in favour of pinnedPeerCertSha256, so there
is no spelling that the reference clients agree on. Emitting one would have
produced a link that looks configured but is not.
The ws `host` fix stands: v2rayN's own documentation lists "ws host" as the
meaning of that field, so dropping it broke ws listeners that pin a Host.
The conformance test now pins the emitted payload to v2rayN's field set, so a
future field copied over from another protocol fails instead of being silently
ignored by every client.
Deliberately left alone, both confirmed against the official scheme and worth
deciding separately:
- hysteria2 links carry up/down. The Hysteria 2 URI specification is explicit
that the parameters "should never include client modes ... or bandwidth
values". Removing them changes behaviour for anyone relying on the link, so
it is flagged rather than removed. `insecure` is the official parameter;
allowInsecure is kept alongside it purely for clients that read that name.
- an SS listener with simple-obfs still exports a bare ss:// link with no
plugin entry, losing the obfuscation the client YAML does translate.
Also noted while checking: tuicURIs defaults congestion_control to bbr where
sing-box documents cubic as its default. Ours is set explicitly, which is
valid, but the comment claiming it is what "most TUIC clients" use is not
sourced.
* fix(uri): keep vmess allowInsecure after all
Reverts the revert. Dropping it was correct on the evidence and wrong on the
consequences: v2rayN ignores the field, v2rayNG spells it `insecure`, and
Xray-core has moved to pinnedPeerCertSha256 — but the clients that do read
allowInsecure are precisely the ones whose handshake fails without it, and an
ignored extra key costs the others nothing. Removing it would have traded a
working link for a tidy payload.
The conformance test keeps its guard, with allowInsecure recorded as an
explicit, justified exception rather than a silent inclusion: the field set is
still pinned to v2rayN's model, so any further field copied over from another
protocol has to argue its way in.
* fix(uri): restore the vmess ws Host in registry YAML, and stop dropping share links
Two leftovers from auditing the exporters end to end.
The registry's VMess compiler built ws-opts with the path only, so a listener
that pins a Host exported a client config with nothing to send as Host. The
vless YAML path in the same file already nests it under `headers` per the mihomo
wiki, so this just mirrors that — and the converter has always carried it, which
is how the difference went unnoticed.
The node URI share endpoint also had a tier that could not be reached. It walks
three tiers — m-ui, the registry, then the legacy builders — and the second one
gates the third on having neither a URI nor client YAML. But the registry
implements URI export for a different protocol set than the legacy builders: it
compiles vmess, snell, shadowquic, mieru, sudoku and trusttunnel to client YAML
without ever emitting a URI, so those listeners arrived at the third tier with a
YAML in hand and the condition already false, and reported no link at all even
though ClientURIsWithCredentials exports one. The third tier now runs whenever
the URI is still missing, and only fills in YAML that earlier tiers did not
already provide.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
fix(vless): stop exporting flow on transports that cannot carry it (#87)
* fix(vless): stop exporting flow on transports that cannot carry it
The form promised that flow is cleared for ws/grpc/xhttp and the tooltip still
says so, but exported links kept carrying flow=xtls-rprx-vision. Two separate
reasons, both fixed.
The clear came too late to matter. formValuesToConfig wrote `flow` into the
config inside the protocol switch, and only afterwards did the "Vision is
TCP-only" rule run — where it mutated the form state, not the config it had
already been copied into. The transport is now resolved before anything writes,
and the drop also removes a flow inherited from the previous config, so changing
a listener's transport actually takes effect.
The URI generators never asked. Every one of them emitted whatever flow it found:
- node: vlessURIs decided flow before resolving the transport, so the type it
had just computed was irrelevant to the decision. It now resolves the
transport first. ClientURIsWithCredentials also merged the listener-level
flow onto every user on protocol alone, which reintroduced the value the
form had dropped.
- protocol: the share URI and the client YAML both wrote spec.Flow unguarded,
even though TransportSpec knows the network. Same for the server-side
compile, which pushed the listener default onto users — Mihomo rejects that
combination outright, so this one takes the listener down.
- mui: BuildVLESSShare set flow unconditionally, and bridge.go injected the
listener default into users before it was known to be applicable.
- converter: copyTransport resolved `network` but left a stale flow behind.
The rule is now one predicate per representation: TransportSpec.CarriesFlow,
VLESSHandlerSpec.CarriesFlow, and protocol.TransportCarriesFlow for raw configs.
TCP/raw keeps Vision; ws, grpc, xhttp, mkcp and mekya never emit it.
Tests pin each path in both directions — a stale flow disappears on ws/grpc/
xhttp, and a genuine TCP listener keeps its flow. The frontend one runs the real
form builder and fails, in three of five cases, against the previous ordering.
* fix(uri): carry vmess ws Host and align hysteria2 TLS flags across user shapes
Auditing the remaining exporters after the flow fix turned up two more places
where the link disagreed with the listener.
vmessURIs built its JSON by hand instead of going through transportParams, so a
ws listener that pins a Host header exported only the path: the client had
nothing to send as Host and the listener was unreachable through the link, even
though the client YAML built from the same config worked. It also dropped
allowInsecure, which vless and trojan both carry — a listener whose certificate
the client cannot verify exported a link that failed the handshake. The repeated
tlsParams() calls were hoisted while touching this.
hysteria2URIs reads users either as rows or as a map, and the two branches
disagreed: one asked certutil.ShouldSkipCertVerify and emitted only `insecure`,
the other asked clientSkipCert and emitted `insecure` plus `allowInsecure`. The
same listener therefore produced different links depending on how its users were
stored. Both now use clientSkipCert, the helper intended for share URIs, and
emit both keys.
Not fixed, for the record: an SS listener with simple-obfs exports a bare ss://
link with no `plugin=` entry, so the obfs configuration is lost. The client YAML
does translate it (plugin: obfs). Fixing the URI means committing to a SIP002
plugin encoding that varies between clients, which is worth deciding separately
rather than guessing at here.
* i18n: complete zh-CN and guard against the locale regressions already seen
zh-CN was missing seven listener hints that en.ts declares, so those tooltips
rendered in English for the primary UI language. Translated: client_auth_cert,
client_auth_type, ech_key, enable_pure_downlink, mux, padding and table_type.
The rest of the catalogue is deliberately left alone. Seventeen locales each lack
65 keys that en.ts has; the provider already resolves those from en.ts, so they
degrade to English rather than breaking, and filling 1100 strings with
unreviewed machine translation would be worse than the fallback. The test prints
the per-locale gap so it stays visible.
The new locale test guards the regressions this directory has actually hit: a
duplicated key inside one object (which breaks the type check), and a key renamed
in en.ts leaving an orphan behind. It also asserts zh-CN stays complete, since a
gap there reads as a defect rather than as translation lag.
* i18n: declare the 24 keys the UI asks for but no locale ever defined
Twenty-four keys were referenced from components without being declared in any
locale, so t() fell through to its hardcoded fallback. Where a fallback existed
that merely meant the string could never be translated; where none existed —
protocolHint, access_sniHint, jlsHint, shadowTlsHint and the rest — the tooltip
either rendered the raw key, "listeners.protocolHint", or disappeared entirely,
because fieldTip() suppresses a value that equals the key it looked up.
en.ts takes the text the code was already falling back to, so nothing changes on
screen; the strings just become translatable. The few without a fallback were
written from the surrounding context — the field they annotate, its placeholder,
and the wording of the neighbouring hints. trafficMultiplierHint is the exception:
its fallback was Chinese, which is wrong for the English catalogue, so it now has
a real English string.
zh-CN gets all twenty-four, which keeps it complete against en.ts.
The locale test now reads through the TypeScript AST. It previously parsed line by
line and these files contain minified regions — several "key":"value" pairs on one
line — so a whole block of keys was invisible to it, which is how this gap stayed
hidden. It also gained the assertion that would have caught the bug directly:
every dotted key handed to t() or fieldTip() must be declared in en.ts.
The remaining languages still lag, now by 89 keys rather than 65, and still fall
back to English; the test prints the per-locale figure.
* fix(uri): drop the vmess allowInsecure field the reference client ignores
The vmess skip-certificate field was added on internal consistency alone —
vless and trojan carry allowInsecure in their query strings, so vmess looked
like an oversight. Checked against the client that actually parses these links,
it is not one.
v2rayN's VmessQRCode model declares exactly: v, ps, add, port, id, aid, scy,
net, type, host, path, tls, sni, alpn, fp. There is no skip-certificate field
at all — neither allowInsecure nor insecure — so the field was dead weight in a
payload that v2rayN reads field by field. v2rayNG reads `insecure` instead, and
Xray-core has removed allowInsecure in favour of pinnedPeerCertSha256, so there
is no spelling that the reference clients agree on. Emitting one would have
produced a link that looks configured but is not.
The ws `host` fix stands: v2rayN's own documentation lists "ws host" as the
meaning of that field, so dropping it broke ws listeners that pin a Host.
The conformance test now pins the emitted payload to v2rayN's field set, so a
future field copied over from another protocol fails instead of being silently
ignored by every client.
Deliberately left alone, both confirmed against the official scheme and worth
deciding separately:
- hysteria2 links carry up/down. The Hysteria 2 URI specification is explicit
that the parameters "should never include client modes ... or bandwidth
values". Removing them changes behaviour for anyone relying on the link, so
it is flagged rather than removed. `insecure` is the official parameter;
allowInsecure is kept alongside it purely for clients that read that name.
- an SS listener with simple-obfs still exports a bare ss:// link with no
plugin entry, losing the obfuscation the client YAML does translate.
Also noted while checking: tuicURIs defaults congestion_control to bbr where
sing-box documents cubic as its default. Ours is set explicitly, which is
valid, but the comment claiming it is what "most TUIC clients" use is not
sourced.
* fix(uri): keep vmess allowInsecure after all
Reverts the revert. Dropping it was correct on the evidence and wrong on the
consequences: v2rayN ignores the field, v2rayNG spells it `insecure`, and
Xray-core has moved to pinnedPeerCertSha256 — but the clients that do read
allowInsecure are precisely the ones whose handshake fails without it, and an
ignored extra key costs the others nothing. Removing it would have traded a
working link for a tidy payload.
The conformance test keeps its guard, with allowInsecure recorded as an
explicit, justified exception rather than a silent inclusion: the field set is
still pinned to v2rayN's model, so any further field copied over from another
protocol has to argue its way in.
* fix(uri): restore the vmess ws Host in registry YAML, and stop dropping share links
Two leftovers from auditing the exporters end to end.
The registry's VMess compiler built ws-opts with the path only, so a listener
that pins a Host exported a client config with nothing to send as Host. The
vless YAML path in the same file already nests it under `headers` per the mihomo
wiki, so this just mirrors that — and the converter has always carried it, which
is how the difference went unnoticed.
The node URI share endpoint also had a tier that could not be reached. It walks
three tiers — m-ui, the registry, then the legacy builders — and the second one
gates the third on having neither a URI nor client YAML. But the registry
implements URI export for a different protocol set than the legacy builders: it
compiles vmess, snell, shadowquic, mieru, sudoku and trusttunnel to client YAML
without ever emitting a URI, so those listeners arrived at the third tier with a
YAML in hand and the condition already false, and reported no link at all even
though ClientURIsWithCredentials exports one. The third tier now runs whenever
the URI is still missing, and only fills in YAML that earlier tiers did not
already provide.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
fix(vless): stop exporting flow on transports that cannot carry it (#87)
* fix(vless): stop exporting flow on transports that cannot carry it
The form promised that flow is cleared for ws/grpc/xhttp and the tooltip still
says so, but exported links kept carrying flow=xtls-rprx-vision. Two separate
reasons, both fixed.
The clear came too late to matter. formValuesToConfig wrote `flow` into the
config inside the protocol switch, and only afterwards did the "Vision is
TCP-only" rule run — where it mutated the form state, not the config it had
already been copied into. The transport is now resolved before anything writes,
and the drop also removes a flow inherited from the previous config, so changing
a listener's transport actually takes effect.
The URI generators never asked. Every one of them emitted whatever flow it found:
- node: vlessURIs decided flow before resolving the transport, so the type it
had just computed was irrelevant to the decision. It now resolves the
transport first. ClientURIsWithCredentials also merged the listener-level
flow onto every user on protocol alone, which reintroduced the value the
form had dropped.
- protocol: the share URI and the client YAML both wrote spec.Flow unguarded,
even though TransportSpec knows the network. Same for the server-side
compile, which pushed the listener default onto users — Mihomo rejects that
combination outright, so this one takes the listener down.
- mui: BuildVLESSShare set flow unconditionally, and bridge.go injected the
listener default into users before it was known to be applicable.
- converter: copyTransport resolved `network` but left a stale flow behind.
The rule is now one predicate per representation: TransportSpec.CarriesFlow,
VLESSHandlerSpec.CarriesFlow, and protocol.TransportCarriesFlow for raw configs.
TCP/raw keeps Vision; ws, grpc, xhttp, mkcp and mekya never emit it.
Tests pin each path in both directions — a stale flow disappears on ws/grpc/
xhttp, and a genuine TCP listener keeps its flow. The frontend one runs the real
form builder and fails, in three of five cases, against the previous ordering.
* fix(uri): carry vmess ws Host and align hysteria2 TLS flags across user shapes
Auditing the remaining exporters after the flow fix turned up two more places
where the link disagreed with the listener.
vmessURIs built its JSON by hand instead of going through transportParams, so a
ws listener that pins a Host header exported only the path: the client had
nothing to send as Host and the listener was unreachable through the link, even
though the client YAML built from the same config worked. It also dropped
allowInsecure, which vless and trojan both carry — a listener whose certificate
the client cannot verify exported a link that failed the handshake. The repeated
tlsParams() calls were hoisted while touching this.
hysteria2URIs reads users either as rows or as a map, and the two branches
disagreed: one asked certutil.ShouldSkipCertVerify and emitted only `insecure`,
the other asked clientSkipCert and emitted `insecure` plus `allowInsecure`. The
same listener therefore produced different links depending on how its users were
stored. Both now use clientSkipCert, the helper intended for share URIs, and
emit both keys.
Not fixed, for the record: an SS listener with simple-obfs exports a bare ss://
link with no `plugin=` entry, so the obfs configuration is lost. The client YAML
does translate it (plugin: obfs). Fixing the URI means committing to a SIP002
plugin encoding that varies between clients, which is worth deciding separately
rather than guessing at here.
* i18n: complete zh-CN and guard against the locale regressions already seen
zh-CN was missing seven listener hints that en.ts declares, so those tooltips
rendered in English for the primary UI language. Translated: client_auth_cert,
client_auth_type, ech_key, enable_pure_downlink, mux, padding and table_type.
The rest of the catalogue is deliberately left alone. Seventeen locales each lack
65 keys that en.ts has; the provider already resolves those from en.ts, so they
degrade to English rather than breaking, and filling 1100 strings with
unreviewed machine translation would be worse than the fallback. The test prints
the per-locale gap so it stays visible.
The new locale test guards the regressions this directory has actually hit: a
duplicated key inside one object (which breaks the type check), and a key renamed
in en.ts leaving an orphan behind. It also asserts zh-CN stays complete, since a
gap there reads as a defect rather than as translation lag.
* i18n: declare the 24 keys the UI asks for but no locale ever defined
Twenty-four keys were referenced from components without being declared in any
locale, so t() fell through to its hardcoded fallback. Where a fallback existed
that merely meant the string could never be translated; where none existed —
protocolHint, access_sniHint, jlsHint, shadowTlsHint and the rest — the tooltip
either rendered the raw key, "listeners.protocolHint", or disappeared entirely,
because fieldTip() suppresses a value that equals the key it looked up.
en.ts takes the text the code was already falling back to, so nothing changes on
screen; the strings just become translatable. The few without a fallback were
written from the surrounding context — the field they annotate, its placeholder,
and the wording of the neighbouring hints. trafficMultiplierHint is the exception:
its fallback was Chinese, which is wrong for the English catalogue, so it now has
a real English string.
zh-CN gets all twenty-four, which keeps it complete against en.ts.
The locale test now reads through the TypeScript AST. It previously parsed line by
line and these files contain minified regions — several "key":"value" pairs on one
line — so a whole block of keys was invisible to it, which is how this gap stayed
hidden. It also gained the assertion that would have caught the bug directly:
every dotted key handed to t() or fieldTip() must be declared in en.ts.
The remaining languages still lag, now by 89 keys rather than 65, and still fall
back to English; the test prints the per-locale figure.
* fix(uri): drop the vmess allowInsecure field the reference client ignores
The vmess skip-certificate field was added on internal consistency alone —
vless and trojan carry allowInsecure in their query strings, so vmess looked
like an oversight. Checked against the client that actually parses these links,
it is not one.
v2rayN's VmessQRCode model declares exactly: v, ps, add, port, id, aid, scy,
net, type, host, path, tls, sni, alpn, fp. There is no skip-certificate field
at all — neither allowInsecure nor insecure — so the field was dead weight in a
payload that v2rayN reads field by field. v2rayNG reads `insecure` instead, and
Xray-core has removed allowInsecure in favour of pinnedPeerCertSha256, so there
is no spelling that the reference clients agree on. Emitting one would have
produced a link that looks configured but is not.
The ws `host` fix stands: v2rayN's own documentation lists "ws host" as the
meaning of that field, so dropping it broke ws listeners that pin a Host.
The conformance test now pins the emitted payload to v2rayN's field set, so a
future field copied over from another protocol fails instead of being silently
ignored by every client.
Deliberately left alone, both confirmed against the official scheme and worth
deciding separately:
- hysteria2 links carry up/down. The Hysteria 2 URI specification is explicit
that the parameters "should never include client modes ... or bandwidth
values". Removing them changes behaviour for anyone relying on the link, so
it is flagged rather than removed. `insecure` is the official parameter;
allowInsecure is kept alongside it purely for clients that read that name.
- an SS listener with simple-obfs still exports a bare ss:// link with no
plugin entry, losing the obfuscation the client YAML does translate.
Also noted while checking: tuicURIs defaults congestion_control to bbr where
sing-box documents cubic as its default. Ours is set explicitly, which is
valid, but the comment claiming it is what "most TUIC clients" use is not
sourced.
* fix(uri): keep vmess allowInsecure after all
Reverts the revert. Dropping it was correct on the evidence and wrong on the
consequences: v2rayN ignores the field, v2rayNG spells it `insecure`, and
Xray-core has moved to pinnedPeerCertSha256 — but the clients that do read
allowInsecure are precisely the ones whose handshake fails without it, and an
ignored extra key costs the others nothing. Removing it would have traded a
working link for a tidy payload.
The conformance test keeps its guard, with allowInsecure recorded as an
explicit, justified exception rather than a silent inclusion: the field set is
still pinned to v2rayN's model, so any further field copied over from another
protocol has to argue its way in.
* fix(uri): restore the vmess ws Host in registry YAML, and stop dropping share links
Two leftovers from auditing the exporters end to end.
The registry's VMess compiler built ws-opts with the path only, so a listener
that pins a Host exported a client config with nothing to send as Host. The
vless YAML path in the same file already nests it under `headers` per the mihomo
wiki, so this just mirrors that — and the converter has always carried it, which
is how the difference went unnoticed.
The node URI share endpoint also had a tier that could not be reached. It walks
three tiers — m-ui, the registry, then the legacy builders — and the second one
gates the third on having neither a URI nor client YAML. But the registry
implements URI export for a different protocol set than the legacy builders: it
compiles vmess, snell, shadowquic, mieru, sudoku and trusttunnel to client YAML
without ever emitting a URI, so those listeners arrived at the third tier with a
YAML in hand and the condition already false, and reported no link at all even
though ClientURIsWithCredentials exports one. The third tier now runs whenever
the URI is still missing, and only fills in YAML that earlier tiers did not
already provide.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
fix(vless): stop exporting flow on transports that cannot carry it (#87)
* fix(vless): stop exporting flow on transports that cannot carry it
The form promised that flow is cleared for ws/grpc/xhttp and the tooltip still
says so, but exported links kept carrying flow=xtls-rprx-vision. Two separate
reasons, both fixed.
The clear came too late to matter. formValuesToConfig wrote `flow` into the
config inside the protocol switch, and only afterwards did the "Vision is
TCP-only" rule run — where it mutated the form state, not the config it had
already been copied into. The transport is now resolved before anything writes,
and the drop also removes a flow inherited from the previous config, so changing
a listener's transport actually takes effect.
The URI generators never asked. Every one of them emitted whatever flow it found:
- node: vlessURIs decided flow before resolving the transport, so the type it
had just computed was irrelevant to the decision. It now resolves the
transport first. ClientURIsWithCredentials also merged the listener-level
flow onto every user on protocol alone, which reintroduced the value the
form had dropped.
- protocol: the share URI and the client YAML both wrote spec.Flow unguarded,
even though TransportSpec knows the network. Same for the server-side
compile, which pushed the listener default onto users — Mihomo rejects that
combination outright, so this one takes the listener down.
- mui: BuildVLESSShare set flow unconditionally, and bridge.go injected the
listener default into users before it was known to be applicable.
- converter: copyTransport resolved `network` but left a stale flow behind.
The rule is now one predicate per representation: TransportSpec.CarriesFlow,
VLESSHandlerSpec.CarriesFlow, and protocol.TransportCarriesFlow for raw configs.
TCP/raw keeps Vision; ws, grpc, xhttp, mkcp and mekya never emit it.
Tests pin each path in both directions — a stale flow disappears on ws/grpc/
xhttp, and a genuine TCP listener keeps its flow. The frontend one runs the real
form builder and fails, in three of five cases, against the previous ordering.
* fix(uri): carry vmess ws Host and align hysteria2 TLS flags across user shapes
Auditing the remaining exporters after the flow fix turned up two more places
where the link disagreed with the listener.
vmessURIs built its JSON by hand instead of going through transportParams, so a
ws listener that pins a Host header exported only the path: the client had
nothing to send as Host and the listener was unreachable through the link, even
though the client YAML built from the same config worked. It also dropped
allowInsecure, which vless and trojan both carry — a listener whose certificate
the client cannot verify exported a link that failed the handshake. The repeated
tlsParams() calls were hoisted while touching this.
hysteria2URIs reads users either as rows or as a map, and the two branches
disagreed: one asked certutil.ShouldSkipCertVerify and emitted only `insecure`,
the other asked clientSkipCert and emitted `insecure` plus `allowInsecure`. The
same listener therefore produced different links depending on how its users were
stored. Both now use clientSkipCert, the helper intended for share URIs, and
emit both keys.
Not fixed, for the record: an SS listener with simple-obfs exports a bare ss://
link with no `plugin=` entry, so the obfs configuration is lost. The client YAML
does translate it (plugin: obfs). Fixing the URI means committing to a SIP002
plugin encoding that varies between clients, which is worth deciding separately
rather than guessing at here.
* i18n: complete zh-CN and guard against the locale regressions already seen
zh-CN was missing seven listener hints that en.ts declares, so those tooltips
rendered in English for the primary UI language. Translated: client_auth_cert,
client_auth_type, ech_key, enable_pure_downlink, mux, padding and table_type.
The rest of the catalogue is deliberately left alone. Seventeen locales each lack
65 keys that en.ts has; the provider already resolves those from en.ts, so they
degrade to English rather than breaking, and filling 1100 strings with
unreviewed machine translation would be worse than the fallback. The test prints
the per-locale gap so it stays visible.
The new locale test guards the regressions this directory has actually hit: a
duplicated key inside one object (which breaks the type check), and a key renamed
in en.ts leaving an orphan behind. It also asserts zh-CN stays complete, since a
gap there reads as a defect rather than as translation lag.
* i18n: declare the 24 keys the UI asks for but no locale ever defined
Twenty-four keys were referenced from components without being declared in any
locale, so t() fell through to its hardcoded fallback. Where a fallback existed
that merely meant the string could never be translated; where none existed —
protocolHint, access_sniHint, jlsHint, shadowTlsHint and the rest — the tooltip
either rendered the raw key, "listeners.protocolHint", or disappeared entirely,
because fieldTip() suppresses a value that equals the key it looked up.
en.ts takes the text the code was already falling back to, so nothing changes on
screen; the strings just become translatable. The few without a fallback were
written from the surrounding context — the field they annotate, its placeholder,
and the wording of the neighbouring hints. trafficMultiplierHint is the exception:
its fallback was Chinese, which is wrong for the English catalogue, so it now has
a real English string.
zh-CN gets all twenty-four, which keeps it complete against en.ts.
The locale test now reads through the TypeScript AST. It previously parsed line by
line and these files contain minified regions — several "key":"value" pairs on one
line — so a whole block of keys was invisible to it, which is how this gap stayed
hidden. It also gained the assertion that would have caught the bug directly:
every dotted key handed to t() or fieldTip() must be declared in en.ts.
The remaining languages still lag, now by 89 keys rather than 65, and still fall
back to English; the test prints the per-locale figure.
* fix(uri): drop the vmess allowInsecure field the reference client ignores
The vmess skip-certificate field was added on internal consistency alone —
vless and trojan carry allowInsecure in their query strings, so vmess looked
like an oversight. Checked against the client that actually parses these links,
it is not one.
v2rayN's VmessQRCode model declares exactly: v, ps, add, port, id, aid, scy,
net, type, host, path, tls, sni, alpn, fp. There is no skip-certificate field
at all — neither allowInsecure nor insecure — so the field was dead weight in a
payload that v2rayN reads field by field. v2rayNG reads `insecure` instead, and
Xray-core has removed allowInsecure in favour of pinnedPeerCertSha256, so there
is no spelling that the reference clients agree on. Emitting one would have
produced a link that looks configured but is not.
The ws `host` fix stands: v2rayN's own documentation lists "ws host" as the
meaning of that field, so dropping it broke ws listeners that pin a Host.
The conformance test now pins the emitted payload to v2rayN's field set, so a
future field copied over from another protocol fails instead of being silently
ignored by every client.
Deliberately left alone, both confirmed against the official scheme and worth
deciding separately:
- hysteria2 links carry up/down. The Hysteria 2 URI specification is explicit
that the parameters "should never include client modes ... or bandwidth
values". Removing them changes behaviour for anyone relying on the link, so
it is flagged rather than removed. `insecure` is the official parameter;
allowInsecure is kept alongside it purely for clients that read that name.
- an SS listener with simple-obfs still exports a bare ss:// link with no
plugin entry, losing the obfuscation the client YAML does translate.
Also noted while checking: tuicURIs defaults congestion_control to bbr where
sing-box documents cubic as its default. Ours is set explicitly, which is
valid, but the comment claiming it is what "most TUIC clients" use is not
sourced.
* fix(uri): keep vmess allowInsecure after all
Reverts the revert. Dropping it was correct on the evidence and wrong on the
consequences: v2rayN ignores the field, v2rayNG spells it `insecure`, and
Xray-core has moved to pinnedPeerCertSha256 — but the clients that do read
allowInsecure are precisely the ones whose handshake fails without it, and an
ignored extra key costs the others nothing. Removing it would have traded a
working link for a tidy payload.
The conformance test keeps its guard, with allowInsecure recorded as an
explicit, justified exception rather than a silent inclusion: the field set is
still pinned to v2rayN's model, so any further field copied over from another
protocol has to argue its way in.
* fix(uri): restore the vmess ws Host in registry YAML, and stop dropping share links
Two leftovers from auditing the exporters end to end.
The registry's VMess compiler built ws-opts with the path only, so a listener
that pins a Host exported a client config with nothing to send as Host. The
vless YAML path in the same file already nests it under `headers` per the mihomo
wiki, so this just mirrors that — and the converter has always carried it, which
is how the difference went unnoticed.
The node URI share endpoint also had a tier that could not be reached. It walks
three tiers — m-ui, the registry, then the legacy builders — and the second one
gates the third on having neither a URI nor client YAML. But the registry
implements URI export for a different protocol set than the legacy builders: it
compiles vmess, snell, shadowquic, mieru, sudoku and trusttunnel to client YAML
without ever emitting a URI, so those listeners arrived at the third tier with a
YAML in hand and the condition already false, and reported no link at all even
though ClientURIsWithCredentials exports one. The third tier now runs whenever
the URI is still missing, and only fills in YAML that earlier tiers did not
already provide.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
fix(vless): stop exporting flow on transports that cannot carry it (#87)
* fix(vless): stop exporting flow on transports that cannot carry it
The form promised that flow is cleared for ws/grpc/xhttp and the tooltip still
says so, but exported links kept carrying flow=xtls-rprx-vision. Two separate
reasons, both fixed.
The clear came too late to matter. formValuesToConfig wrote `flow` into the
config inside the protocol switch, and only afterwards did the "Vision is
TCP-only" rule run — where it mutated the form state, not the config it had
already been copied into. The transport is now resolved before anything writes,
and the drop also removes a flow inherited from the previous config, so changing
a listener's transport actually takes effect.
The URI generators never asked. Every one of them emitted whatever flow it found:
- node: vlessURIs decided flow before resolving the transport, so the type it
had just computed was irrelevant to the decision. It now resolves the
transport first. ClientURIsWithCredentials also merged the listener-level
flow onto every user on protocol alone, which reintroduced the value the
form had dropped.
- protocol: the share URI and the client YAML both wrote spec.Flow unguarded,
even though TransportSpec knows the network. Same for the server-side
compile, which pushed the listener default onto users — Mihomo rejects that
combination outright, so this one takes the listener down.
- mui: BuildVLESSShare set flow unconditionally, and bridge.go injected the
listener default into users before it was known to be applicable.
- converter: copyTransport resolved `network` but left a stale flow behind.
The rule is now one predicate per representation: TransportSpec.CarriesFlow,
VLESSHandlerSpec.CarriesFlow, and protocol.TransportCarriesFlow for raw configs.
TCP/raw keeps Vision; ws, grpc, xhttp, mkcp and mekya never emit it.
Tests pin each path in both directions — a stale flow disappears on ws/grpc/
xhttp, and a genuine TCP listener keeps its flow. The frontend one runs the real
form builder and fails, in three of five cases, against the previous ordering.
* fix(uri): carry vmess ws Host and align hysteria2 TLS flags across user shapes
Auditing the remaining exporters after the flow fix turned up two more places
where the link disagreed with the listener.
vmessURIs built its JSON by hand instead of going through transportParams, so a
ws listener that pins a Host header exported only the path: the client had
nothing to send as Host and the listener was unreachable through the link, even
though the client YAML built from the same config worked. It also dropped
allowInsecure, which vless and trojan both carry — a listener whose certificate
the client cannot verify exported a link that failed the handshake. The repeated
tlsParams() calls were hoisted while touching this.
hysteria2URIs reads users either as rows or as a map, and the two branches
disagreed: one asked certutil.ShouldSkipCertVerify and emitted only `insecure`,
the other asked clientSkipCert and emitted `insecure` plus `allowInsecure`. The
same listener therefore produced different links depending on how its users were
stored. Both now use clientSkipCert, the helper intended for share URIs, and
emit both keys.
Not fixed, for the record: an SS listener with simple-obfs exports a bare ss://
link with no `plugin=` entry, so the obfs configuration is lost. The client YAML
does translate it (plugin: obfs). Fixing the URI means committing to a SIP002
plugin encoding that varies between clients, which is worth deciding separately
rather than guessing at here.
* i18n: complete zh-CN and guard against the locale regressions already seen
zh-CN was missing seven listener hints that en.ts declares, so those tooltips
rendered in English for the primary UI language. Translated: client_auth_cert,
client_auth_type, ech_key, enable_pure_downlink, mux, padding and table_type.
The rest of the catalogue is deliberately left alone. Seventeen locales each lack
65 keys that en.ts has; the provider already resolves those from en.ts, so they
degrade to English rather than breaking, and filling 1100 strings with
unreviewed machine translation would be worse than the fallback. The test prints
the per-locale gap so it stays visible.
The new locale test guards the regressions this directory has actually hit: a
duplicated key inside one object (which breaks the type check), and a key renamed
in en.ts leaving an orphan behind. It also asserts zh-CN stays complete, since a
gap there reads as a defect rather than as translation lag.
* i18n: declare the 24 keys the UI asks for but no locale ever defined
Twenty-four keys were referenced from components without being declared in any
locale, so t() fell through to its hardcoded fallback. Where a fallback existed
that merely meant the string could never be translated; where none existed —
protocolHint, access_sniHint, jlsHint, shadowTlsHint and the rest — the tooltip
either rendered the raw key, "listeners.protocolHint", or disappeared entirely,
because fieldTip() suppresses a value that equals the key it looked up.
en.ts takes the text the code was already falling back to, so nothing changes on
screen; the strings just become translatable. The few without a fallback were
written from the surrounding context — the field they annotate, its placeholder,
and the wording of the neighbouring hints. trafficMultiplierHint is the exception:
its fallback was Chinese, which is wrong for the English catalogue, so it now has
a real English string.
zh-CN gets all twenty-four, which keeps it complete against en.ts.
The locale test now reads through the TypeScript AST. It previously parsed line by
line and these files contain minified regions — several "key":"value" pairs on one
line — so a whole block of keys was invisible to it, which is how this gap stayed
hidden. It also gained the assertion that would have caught the bug directly:
every dotted key handed to t() or fieldTip() must be declared in en.ts.
The remaining languages still lag, now by 89 keys rather than 65, and still fall
back to English; the test prints the per-locale figure.
* fix(uri): drop the vmess allowInsecure field the reference client ignores
The vmess skip-certificate field was added on internal consistency alone —
vless and trojan carry allowInsecure in their query strings, so vmess looked
like an oversight. Checked against the client that actually parses these links,
it is not one.
v2rayN's VmessQRCode model declares exactly: v, ps, add, port, id, aid, scy,
net, type, host, path, tls, sni, alpn, fp. There is no skip-certificate field
at all — neither allowInsecure nor insecure — so the field was dead weight in a
payload that v2rayN reads field by field. v2rayNG reads `insecure` instead, and
Xray-core has removed allowInsecure in favour of pinnedPeerCertSha256, so there
is no spelling that the reference clients agree on. Emitting one would have
produced a link that looks configured but is not.
The ws `host` fix stands: v2rayN's own documentation lists "ws host" as the
meaning of that field, so dropping it broke ws listeners that pin a Host.
The conformance test now pins the emitted payload to v2rayN's field set, so a
future field copied over from another protocol fails instead of being silently
ignored by every client.
Deliberately left alone, both confirmed against the official scheme and worth
deciding separately:
- hysteria2 links carry up/down. The Hysteria 2 URI specification is explicit
that the parameters "should never include client modes ... or bandwidth
values". Removing them changes behaviour for anyone relying on the link, so
it is flagged rather than removed. `insecure` is the official parameter;
allowInsecure is kept alongside it purely for clients that read that name.
- an SS listener with simple-obfs still exports a bare ss:// link with no
plugin entry, losing the obfuscation the client YAML does translate.
Also noted while checking: tuicURIs defaults congestion_control to bbr where
sing-box documents cubic as its default. Ours is set explicitly, which is
valid, but the comment claiming it is what "most TUIC clients" use is not
sourced.
* fix(uri): keep vmess allowInsecure after all
Reverts the revert. Dropping it was correct on the evidence and wrong on the
consequences: v2rayN ignores the field, v2rayNG spells it `insecure`, and
Xray-core has moved to pinnedPeerCertSha256 — but the clients that do read
allowInsecure are precisely the ones whose handshake fails without it, and an
ignored extra key costs the others nothing. Removing it would have traded a
working link for a tidy payload.
The conformance test keeps its guard, with allowInsecure recorded as an
explicit, justified exception rather than a silent inclusion: the field set is
still pinned to v2rayN's model, so any further field copied over from another
protocol has to argue its way in.
* fix(uri): restore the vmess ws Host in registry YAML, and stop dropping share links
Two leftovers from auditing the exporters end to end.
The registry's VMess compiler built ws-opts with the path only, so a listener
that pins a Host exported a client config with nothing to send as Host. The
vless YAML path in the same file already nests it under `headers` per the mihomo
wiki, so this just mirrors that — and the converter has always carried it, which
is how the difference went unnoticed.
The node URI share endpoint also had a tier that could not be reached. It walks
three tiers — m-ui, the registry, then the legacy builders — and the second one
gates the third on having neither a URI nor client YAML. But the registry
implements URI export for a different protocol set than the legacy builders: it
compiles vmess, snell, shadowquic, mieru, sudoku and trusttunnel to client YAML
without ever emitting a URI, so those listeners arrived at the third tier with a
YAML in hand and the condition already false, and reported no link at all even
though ClientURIsWithCredentials exports one. The third tier now runs whenever
the URI is still missing, and only fills in YAML that earlier tiers did not
already provide.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
fix(vless): stop exporting flow on transports that cannot carry it (#87)
* fix(vless): stop exporting flow on transports that cannot carry it
The form promised that flow is cleared for ws/grpc/xhttp and the tooltip still
says so, but exported links kept carrying flow=xtls-rprx-vision. Two separate
reasons, both fixed.
The clear came too late to matter. formValuesToConfig wrote `flow` into the
config inside the protocol switch, and only afterwards did the "Vision is
TCP-only" rule run — where it mutated the form state, not the config it had
already been copied into. The transport is now resolved before anything writes,
and the drop also removes a flow inherited from the previous config, so changing
a listener's transport actually takes effect.
The URI generators never asked. Every one of them emitted whatever flow it found:
- node: vlessURIs decided flow before resolving the transport, so the type it
had just computed was irrelevant to the decision. It now resolves the
transport first. ClientURIsWithCredentials also merged the listener-level
flow onto every user on protocol alone, which reintroduced the value the
form had dropped.
- protocol: the share URI and the client YAML both wrote spec.Flow unguarded,
even though TransportSpec knows the network. Same for the server-side
compile, which pushed the listener default onto users — Mihomo rejects that
combination outright, so this one takes the listener down.
- mui: BuildVLESSShare set flow unconditionally, and bridge.go injected the
listener default into users before it was known to be applicable.
- converter: copyTransport resolved `network` but left a stale flow behind.
The rule is now one predicate per representation: TransportSpec.CarriesFlow,
VLESSHandlerSpec.CarriesFlow, and protocol.TransportCarriesFlow for raw configs.
TCP/raw keeps Vision; ws, grpc, xhttp, mkcp and mekya never emit it.
Tests pin each path in both directions — a stale flow disappears on ws/grpc/
xhttp, and a genuine TCP listener keeps its flow. The frontend one runs the real
form builder and fails, in three of five cases, against the previous ordering.
* fix(uri): carry vmess ws Host and align hysteria2 TLS flags across user shapes
Auditing the remaining exporters after the flow fix turned up two more places
where the link disagreed with the listener.
vmessURIs built its JSON by hand instead of going through transportParams, so a
ws listener that pins a Host header exported only the path: the client had
nothing to send as Host and the listener was unreachable through the link, even
though the client YAML built from the same config worked. It also dropped
allowInsecure, which vless and trojan both carry — a listener whose certificate
the client cannot verify exported a link that failed the handshake. The repeated
tlsParams() calls were hoisted while touching this.
hysteria2URIs reads users either as rows or as a map, and the two branches
disagreed: one asked certutil.ShouldSkipCertVerify and emitted only `insecure`,
the other asked clientSkipCert and emitted `insecure` plus `allowInsecure`. The
same listener therefore produced different links depending on how its users were
stored. Both now use clientSkipCert, the helper intended for share URIs, and
emit both keys.
Not fixed, for the record: an SS listener with simple-obfs exports a bare ss://
link with no `plugin=` entry, so the obfs configuration is lost. The client YAML
does translate it (plugin: obfs). Fixing the URI means committing to a SIP002
plugin encoding that varies between clients, which is worth deciding separately
rather than guessing at here.
* i18n: complete zh-CN and guard against the locale regressions already seen
zh-CN was missing seven listener hints that en.ts declares, so those tooltips
rendered in English for the primary UI language. Translated: client_auth_cert,
client_auth_type, ech_key, enable_pure_downlink, mux, padding and table_type.
The rest of the catalogue is deliberately left alone. Seventeen locales each lack
65 keys that en.ts has; the provider already resolves those from en.ts, so they
degrade to English rather than breaking, and filling 1100 strings with
unreviewed machine translation would be worse than the fallback. The test prints
the per-locale gap so it stays visible.
The new locale test guards the regressions this directory has actually hit: a
duplicated key inside one object (which breaks the type check), and a key renamed
in en.ts leaving an orphan behind. It also asserts zh-CN stays complete, since a
gap there reads as a defect rather than as translation lag.
* i18n: declare the 24 keys the UI asks for but no locale ever defined
Twenty-four keys were referenced from components without being declared in any
locale, so t() fell through to its hardcoded fallback. Where a fallback existed
that merely meant the string could never be translated; where none existed —
protocolHint, access_sniHint, jlsHint, shadowTlsHint and the rest — the tooltip
either rendered the raw key, "listeners.protocolHint", or disappeared entirely,
because fieldTip() suppresses a value that equals the key it looked up.
en.ts takes the text the code was already falling back to, so nothing changes on
screen; the strings just become translatable. The few without a fallback were
written from the surrounding context — the field they annotate, its placeholder,
and the wording of the neighbouring hints. trafficMultiplierHint is the exception:
its fallback was Chinese, which is wrong for the English catalogue, so it now has
a real English string.
zh-CN gets all twenty-four, which keeps it complete against en.ts.
The locale test now reads through the TypeScript AST. It previously parsed line by
line and these files contain minified regions — several "key":"value" pairs on one
line — so a whole block of keys was invisible to it, which is how this gap stayed
hidden. It also gained the assertion that would have caught the bug directly:
every dotted key handed to t() or fieldTip() must be declared in en.ts.
The remaining languages still lag, now by 89 keys rather than 65, and still fall
back to English; the test prints the per-locale figure.
* fix(uri): drop the vmess allowInsecure field the reference client ignores
The vmess skip-certificate field was added on internal consistency alone —
vless and trojan carry allowInsecure in their query strings, so vmess looked
like an oversight. Checked against the client that actually parses these links,
it is not one.
v2rayN's VmessQRCode model declares exactly: v, ps, add, port, id, aid, scy,
net, type, host, path, tls, sni, alpn, fp. There is no skip-certificate field
at all — neither allowInsecure nor insecure — so the field was dead weight in a
payload that v2rayN reads field by field. v2rayNG reads `insecure` instead, and
Xray-core has removed allowInsecure in favour of pinnedPeerCertSha256, so there
is no spelling that the reference clients agree on. Emitting one would have
produced a link that looks configured but is not.
The ws `host` fix stands: v2rayN's own documentation lists "ws host" as the
meaning of that field, so dropping it broke ws listeners that pin a Host.
The conformance test now pins the emitted payload to v2rayN's field set, so a
future field copied over from another protocol fails instead of being silently
ignored by every client.
Deliberately left alone, both confirmed against the official scheme and worth
deciding separately:
- hysteria2 links carry up/down. The Hysteria 2 URI specification is explicit
that the parameters "should never include client modes ... or bandwidth
values". Removing them changes behaviour for anyone relying on the link, so
it is flagged rather than removed. `insecure` is the official parameter;
allowInsecure is kept alongside it purely for clients that read that name.
- an SS listener with simple-obfs still exports a bare ss:// link with no
plugin entry, losing the obfuscation the client YAML does translate.
Also noted while checking: tuicURIs defaults congestion_control to bbr where
sing-box documents cubic as its default. Ours is set explicitly, which is
valid, but the comment claiming it is what "most TUIC clients" use is not
sourced.
* fix(uri): keep vmess allowInsecure after all
Reverts the revert. Dropping it was correct on the evidence and wrong on the
consequences: v2rayN ignores the field, v2rayNG spells it `insecure`, and
Xray-core has moved to pinnedPeerCertSha256 — but the clients that do read
allowInsecure are precisely the ones whose handshake fails without it, and an
ignored extra key costs the others nothing. Removing it would have traded a
working link for a tidy payload.
The conformance test keeps its guard, with allowInsecure recorded as an
explicit, justified exception rather than a silent inclusion: the field set is
still pinned to v2rayN's model, so any further field copied over from another
protocol has to argue its way in.
* fix(uri): restore the vmess ws Host in registry YAML, and stop dropping share links
Two leftovers from auditing the exporters end to end.
The registry's VMess compiler built ws-opts with the path only, so a listener
that pins a Host exported a client config with nothing to send as Host. The
vless YAML path in the same file already nests it under `headers` per the mihomo
wiki, so this just mirrors that — and the converter has always carried it, which
is how the difference went unnoticed.
The node URI share endpoint also had a tier that could not be reached. It walks
three tiers — m-ui, the registry, then the legacy builders — and the second one
gates the third on having neither a URI nor client YAML. But the registry
implements URI export for a different protocol set than the legacy builders: it
compiles vmess, snell, shadowquic, mieru, sudoku and trusttunnel to client YAML
without ever emitting a URI, so those listeners arrived at the third tier with a
YAML in hand and the condition already false, and reported no link at all even
though ClientURIsWithCredentials exports one. The third tier now runs whenever
the URI is still missing, and only fills in YAML that earlier tiers did not
already provide.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
fix(vless): stop exporting flow on transports that cannot carry it (#87)
* fix(vless): stop exporting flow on transports that cannot carry it
The form promised that flow is cleared for ws/grpc/xhttp and the tooltip still
says so, but exported links kept carrying flow=xtls-rprx-vision. Two separate
reasons, both fixed.
The clear came too late to matter. formValuesToConfig wrote `flow` into the
config inside the protocol switch, and only afterwards did the "Vision is
TCP-only" rule run — where it mutated the form state, not the config it had
already been copied into. The transport is now resolved before anything writes,
and the drop also removes a flow inherited from the previous config, so changing
a listener's transport actually takes effect.
The URI generators never asked. Every one of them emitted whatever flow it found:
- node: vlessURIs decided flow before resolving the transport, so the type it
had just computed was irrelevant to the decision. It now resolves the
transport first. ClientURIsWithCredentials also merged the listener-level
flow onto every user on protocol alone, which reintroduced the value the
form had dropped.
- protocol: the share URI and the client YAML both wrote spec.Flow unguarded,
even though TransportSpec knows the network. Same for the server-side
compile, which pushed the listener default onto users — Mihomo rejects that
combination outright, so this one takes the listener down.
- mui: BuildVLESSShare set flow unconditionally, and bridge.go injected the
listener default into users before it was known to be applicable.
- converter: copyTransport resolved `network` but left a stale flow behind.
The rule is now one predicate per representation: TransportSpec.CarriesFlow,
VLESSHandlerSpec.CarriesFlow, and protocol.TransportCarriesFlow for raw configs.
TCP/raw keeps Vision; ws, grpc, xhttp, mkcp and mekya never emit it.
Tests pin each path in both directions — a stale flow disappears on ws/grpc/
xhttp, and a genuine TCP listener keeps its flow. The frontend one runs the real
form builder and fails, in three of five cases, against the previous ordering.
* fix(uri): carry vmess ws Host and align hysteria2 TLS flags across user shapes
Auditing the remaining exporters after the flow fix turned up two more places
where the link disagreed with the listener.
vmessURIs built its JSON by hand instead of going through transportParams, so a
ws listener that pins a Host header exported only the path: the client had
nothing to send as Host and the listener was unreachable through the link, even
though the client YAML built from the same config worked. It also dropped
allowInsecure, which vless and trojan both carry — a listener whose certificate
the client cannot verify exported a link that failed the handshake. The repeated
tlsParams() calls were hoisted while touching this.
hysteria2URIs reads users either as rows or as a map, and the two branches
disagreed: one asked certutil.ShouldSkipCertVerify and emitted only `insecure`,
the other asked clientSkipCert and emitted `insecure` plus `allowInsecure`. The
same listener therefore produced different links depending on how its users were
stored. Both now use clientSkipCert, the helper intended for share URIs, and
emit both keys.
Not fixed, for the record: an SS listener with simple-obfs exports a bare ss://
link with no `plugin=` entry, so the obfs configuration is lost. The client YAML
does translate it (plugin: obfs). Fixing the URI means committing to a SIP002
plugin encoding that varies between clients, which is worth deciding separately
rather than guessing at here.
* i18n: complete zh-CN and guard against the locale regressions already seen
zh-CN was missing seven listener hints that en.ts declares, so those tooltips
rendered in English for the primary UI language. Translated: client_auth_cert,
client_auth_type, ech_key, enable_pure_downlink, mux, padding and table_type.
The rest of the catalogue is deliberately left alone. Seventeen locales each lack
65 keys that en.ts has; the provider already resolves those from en.ts, so they
degrade to English rather than breaking, and filling 1100 strings with
unreviewed machine translation would be worse than the fallback. The test prints
the per-locale gap so it stays visible.
The new locale test guards the regressions this directory has actually hit: a
duplicated key inside one object (which breaks the type check), and a key renamed
in en.ts leaving an orphan behind. It also asserts zh-CN stays complete, since a
gap there reads as a defect rather than as translation lag.
* i18n: declare the 24 keys the UI asks for but no locale ever defined
Twenty-four keys were referenced from components without being declared in any
locale, so t() fell through to its hardcoded fallback. Where a fallback existed
that merely meant the string could never be translated; where none existed —
protocolHint, access_sniHint, jlsHint, shadowTlsHint and the rest — the tooltip
either rendered the raw key, "listeners.protocolHint", or disappeared entirely,
because fieldTip() suppresses a value that equals the key it looked up.
en.ts takes the text the code was already falling back to, so nothing changes on
screen; the strings just become translatable. The few without a fallback were
written from the surrounding context — the field they annotate, its placeholder,
and the wording of the neighbouring hints. trafficMultiplierHint is the exception:
its fallback was Chinese, which is wrong for the English catalogue, so it now has
a real English string.
zh-CN gets all twenty-four, which keeps it complete against en.ts.
The locale test now reads through the TypeScript AST. It previously parsed line by
line and these files contain minified regions — several "key":"value" pairs on one
line — so a whole block of keys was invisible to it, which is how this gap stayed
hidden. It also gained the assertion that would have caught the bug directly:
every dotted key handed to t() or fieldTip() must be declared in en.ts.
The remaining languages still lag, now by 89 keys rather than 65, and still fall
back to English; the test prints the per-locale figure.
* fix(uri): drop the vmess allowInsecure field the reference client ignores
The vmess skip-certificate field was added on internal consistency alone —
vless and trojan carry allowInsecure in their query strings, so vmess looked
like an oversight. Checked against the client that actually parses these links,
it is not one.
v2rayN's VmessQRCode model declares exactly: v, ps, add, port, id, aid, scy,
net, type, host, path, tls, sni, alpn, fp. There is no skip-certificate field
at all — neither allowInsecure nor insecure — so the field was dead weight in a
payload that v2rayN reads field by field. v2rayNG reads `insecure` instead, and
Xray-core has removed allowInsecure in favour of pinnedPeerCertSha256, so there
is no spelling that the reference clients agree on. Emitting one would have
produced a link that looks configured but is not.
The ws `host` fix stands: v2rayN's own documentation lists "ws host" as the
meaning of that field, so dropping it broke ws listeners that pin a Host.
The conformance test now pins the emitted payload to v2rayN's field set, so a
future field copied over from another protocol fails instead of being silently
ignored by every client.
Deliberately left alone, both confirmed against the official scheme and worth
deciding separately:
- hysteria2 links carry up/down. The Hysteria 2 URI specification is explicit
that the parameters "should never include client modes ... or bandwidth
values". Removing them changes behaviour for anyone relying on the link, so
it is flagged rather than removed. `insecure` is the official parameter;
allowInsecure is kept alongside it purely for clients that read that name.
- an SS listener with simple-obfs still exports a bare ss:// link with no
plugin entry, losing the obfuscation the client YAML does translate.
Also noted while checking: tuicURIs defaults congestion_control to bbr where
sing-box documents cubic as its default. Ours is set explicitly, which is
valid, but the comment claiming it is what "most TUIC clients" use is not
sourced.
* fix(uri): keep vmess allowInsecure after all
Reverts the revert. Dropping it was correct on the evidence and wrong on the
consequences: v2rayN ignores the field, v2rayNG spells it `insecure`, and
Xray-core has moved to pinnedPeerCertSha256 — but the clients that do read
allowInsecure are precisely the ones whose handshake fails without it, and an
ignored extra key costs the others nothing. Removing it would have traded a
working link for a tidy payload.
The conformance test keeps its guard, with allowInsecure recorded as an
explicit, justified exception rather than a silent inclusion: the field set is
still pinned to v2rayN's model, so any further field copied over from another
protocol has to argue its way in.
* fix(uri): restore the vmess ws Host in registry YAML, and stop dropping share links
Two leftovers from auditing the exporters end to end.
The registry's VMess compiler built ws-opts with the path only, so a listener
that pins a Host exported a client config with nothing to send as Host. The
vless YAML path in the same file already nests it under `headers` per the mihomo
wiki, so this just mirrors that — and the converter has always carried it, which
is how the difference went unnoticed.
The node URI share endpoint also had a tier that could not be reached. It walks
three tiers — m-ui, the registry, then the legacy builders — and the second one
gates the third on having neither a URI nor client YAML. But the registry
implements URI export for a different protocol set than the legacy builders: it
compiles vmess, snell, shadowquic, mieru, sudoku and trusttunnel to client YAML
without ever emitting a URI, so those listeners arrived at the third tier with a
YAML in hand and the condition already false, and reported no link at all even
though ClientURIsWithCredentials exports one. The third tier now runs whenever
the URI is still missing, and only fills in YAML that earlier tiers did not
already provide.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
fix(vless): stop exporting flow on transports that cannot carry it (#87)
* fix(vless): stop exporting flow on transports that cannot carry it
The form promised that flow is cleared for ws/grpc/xhttp and the tooltip still
says so, but exported links kept carrying flow=xtls-rprx-vision. Two separate
reasons, both fixed.
The clear came too late to matter. formValuesToConfig wrote `flow` into the
config inside the protocol switch, and only afterwards did the "Vision is
TCP-only" rule run — where it mutated the form state, not the config it had
already been copied into. The transport is now resolved before anything writes,
and the drop also removes a flow inherited from the previous config, so changing
a listener's transport actually takes effect.
The URI generators never asked. Every one of them emitted whatever flow it found:
- node: vlessURIs decided flow before resolving the transport, so the type it
had just computed was irrelevant to the decision. It now resolves the
transport first. ClientURIsWithCredentials also merged the listener-level
flow onto every user on protocol alone, which reintroduced the value the
form had dropped.
- protocol: the share URI and the client YAML both wrote spec.Flow unguarded,
even though TransportSpec knows the network. Same for the server-side
compile, which pushed the listener default onto users — Mihomo rejects that
combination outright, so this one takes the listener down.
- mui: BuildVLESSShare set flow unconditionally, and bridge.go injected the
listener default into users before it was known to be applicable.
- converter: copyTransport resolved `network` but left a stale flow behind.
The rule is now one predicate per representation: TransportSpec.CarriesFlow,
VLESSHandlerSpec.CarriesFlow, and protocol.TransportCarriesFlow for raw configs.
TCP/raw keeps Vision; ws, grpc, xhttp, mkcp and mekya never emit it.
Tests pin each path in both directions — a stale flow disappears on ws/grpc/
xhttp, and a genuine TCP listener keeps its flow. The frontend one runs the real
form builder and fails, in three of five cases, against the previous ordering.
* fix(uri): carry vmess ws Host and align hysteria2 TLS flags across user shapes
Auditing the remaining exporters after the flow fix turned up two more places
where the link disagreed with the listener.
vmessURIs built its JSON by hand instead of going through transportParams, so a
ws listener that pins a Host header exported only the path: the client had
nothing to send as Host and the listener was unreachable through the link, even
though the client YAML built from the same config worked. It also dropped
allowInsecure, which vless and trojan both carry — a listener whose certificate
the client cannot verify exported a link that failed the handshake. The repeated
tlsParams() calls were hoisted while touching this.
hysteria2URIs reads users either as rows or as a map, and the two branches
disagreed: one asked certutil.ShouldSkipCertVerify and emitted only `insecure`,
the other asked clientSkipCert and emitted `insecure` plus `allowInsecure`. The
same listener therefore produced different links depending on how its users were
stored. Both now use clientSkipCert, the helper intended for share URIs, and
emit both keys.
Not fixed, for the record: an SS listener with simple-obfs exports a bare ss://
link with no `plugin=` entry, so the obfs configuration is lost. The client YAML
does translate it (plugin: obfs). Fixing the URI means committing to a SIP002
plugin encoding that varies between clients, which is worth deciding separately
rather than guessing at here.
* i18n: complete zh-CN and guard against the locale regressions already seen
zh-CN was missing seven listener hints that en.ts declares, so those tooltips
rendered in English for the primary UI language. Translated: client_auth_cert,
client_auth_type, ech_key, enable_pure_downlink, mux, padding and table_type.
The rest of the catalogue is deliberately left alone. Seventeen locales each lack
65 keys that en.ts has; the provider already resolves those from en.ts, so they
degrade to English rather than breaking, and filling 1100 strings with
unreviewed machine translation would be worse than the fallback. The test prints
the per-locale gap so it stays visible.
The new locale test guards the regressions this directory has actually hit: a
duplicated key inside one object (which breaks the type check), and a key renamed
in en.ts leaving an orphan behind. It also asserts zh-CN stays complete, since a
gap there reads as a defect rather than as translation lag.
* i18n: declare the 24 keys the UI asks for but no locale ever defined
Twenty-four keys were referenced from components without being declared in any
locale, so t() fell through to its hardcoded fallback. Where a fallback existed
that merely meant the string could never be translated; where none existed —
protocolHint, access_sniHint, jlsHint, shadowTlsHint and the rest — the tooltip
either rendered the raw key, "listeners.protocolHint", or disappeared entirely,
because fieldTip() suppresses a value that equals the key it looked up.
en.ts takes the text the code was already falling back to, so nothing changes on
screen; the strings just become translatable. The few without a fallback were
written from the surrounding context — the field they annotate, its placeholder,
and the wording of the neighbouring hints. trafficMultiplierHint is the exception:
its fallback was Chinese, which is wrong for the English catalogue, so it now has
a real English string.
zh-CN gets all twenty-four, which keeps it complete against en.ts.
The locale test now reads through the TypeScript AST. It previously parsed line by
line and these files contain minified regions — several "key":"value" pairs on one
line — so a whole block of keys was invisible to it, which is how this gap stayed
hidden. It also gained the assertion that would have caught the bug directly:
every dotted key handed to t() or fieldTip() must be declared in en.ts.
The remaining languages still lag, now by 89 keys rather than 65, and still fall
back to English; the test prints the per-locale figure.
* fix(uri): drop the vmess allowInsecure field the reference client ignores
The vmess skip-certificate field was added on internal consistency alone —
vless and trojan carry allowInsecure in their query strings, so vmess looked
like an oversight. Checked against the client that actually parses these links,
it is not one.
v2rayN's VmessQRCode model declares exactly: v, ps, add, port, id, aid, scy,
net, type, host, path, tls, sni, alpn, fp. There is no skip-certificate field
at all — neither allowInsecure nor insecure — so the field was dead weight in a
payload that v2rayN reads field by field. v2rayNG reads `insecure` instead, and
Xray-core has removed allowInsecure in favour of pinnedPeerCertSha256, so there
is no spelling that the reference clients agree on. Emitting one would have
produced a link that looks configured but is not.
The ws `host` fix stands: v2rayN's own documentation lists "ws host" as the
meaning of that field, so dropping it broke ws listeners that pin a Host.
The conformance test now pins the emitted payload to v2rayN's field set, so a
future field copied over from another protocol fails instead of being silently
ignored by every client.
Deliberately left alone, both confirmed against the official scheme and worth
deciding separately:
- hysteria2 links carry up/down. The Hysteria 2 URI specification is explicit
that the parameters "should never include client modes ... or bandwidth
values". Removing them changes behaviour for anyone relying on the link, so
it is flagged rather than removed. `insecure` is the official parameter;
allowInsecure is kept alongside it purely for clients that read that name.
- an SS listener with simple-obfs still exports a bare ss:// link with no
plugin entry, losing the obfuscation the client YAML does translate.
Also noted while checking: tuicURIs defaults congestion_control to bbr where
sing-box documents cubic as its default. Ours is set explicitly, which is
valid, but the comment claiming it is what "most TUIC clients" use is not
sourced.
* fix(uri): keep vmess allowInsecure after all
Reverts the revert. Dropping it was correct on the evidence and wrong on the
consequences: v2rayN ignores the field, v2rayNG spells it `insecure`, and
Xray-core has moved to pinnedPeerCertSha256 — but the clients that do read
allowInsecure are precisely the ones whose handshake fails without it, and an
ignored extra key costs the others nothing. Removing it would have traded a
working link for a tidy payload.
The conformance test keeps its guard, with allowInsecure recorded as an
explicit, justified exception rather than a silent inclusion: the field set is
still pinned to v2rayN's model, so any further field copied over from another
protocol has to argue its way in.
* fix(uri): restore the vmess ws Host in registry YAML, and stop dropping share links
Two leftovers from auditing the exporters end to end.
The registry's VMess compiler built ws-opts with the path only, so a listener
that pins a Host exported a client config with nothing to send as Host. The
vless YAML path in the same file already nests it under `headers` per the mihomo
wiki, so this just mirrors that — and the converter has always carried it, which
is how the difference went unnoticed.
The node URI share endpoint also had a tier that could not be reached. It walks
three tiers — m-ui, the registry, then the legacy builders — and the second one
gates the third on having neither a URI nor client YAML. But the registry
implements URI export for a different protocol set than the legacy builders: it
compiles vmess, snell, shadowquic, mieru, sudoku and trusttunnel to client YAML
without ever emitting a URI, so those listeners arrived at the third tier with a
YAML in hand and the condition already false, and reported no link at all even
though ClientURIsWithCredentials exports one. The third tier now runs whenever
the URI is still missing, and only fills in YAML that earlier tiers did not
already provide.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
fix(vless): stop exporting flow on transports that cannot carry it (#87)
* fix(vless): stop exporting flow on transports that cannot carry it
The form promised that flow is cleared for ws/grpc/xhttp and the tooltip still
says so, but exported links kept carrying flow=xtls-rprx-vision. Two separate
reasons, both fixed.
The clear came too late to matter. formValuesToConfig wrote `flow` into the
config inside the protocol switch, and only afterwards did the "Vision is
TCP-only" rule run — where it mutated the form state, not the config it had
already been copied into. The transport is now resolved before anything writes,
and the drop also removes a flow inherited from the previous config, so changing
a listener's transport actually takes effect.
The URI generators never asked. Every one of them emitted whatever flow it found:
- node: vlessURIs decided flow before resolving the transport, so the type it
had just computed was irrelevant to the decision. It now resolves the
transport first. ClientURIsWithCredentials also merged the listener-level
flow onto every user on protocol alone, which reintroduced the value the
form had dropped.
- protocol: the share URI and the client YAML both wrote spec.Flow unguarded,
even though TransportSpec knows the network. Same for the server-side
compile, which pushed the listener default onto users — Mihomo rejects that
combination outright, so this one takes the listener down.
- mui: BuildVLESSShare set flow unconditionally, and bridge.go injected the
listener default into users before it was known to be applicable.
- converter: copyTransport resolved `network` but left a stale flow behind.
The rule is now one predicate per representation: TransportSpec.CarriesFlow,
VLESSHandlerSpec.CarriesFlow, and protocol.TransportCarriesFlow for raw configs.
TCP/raw keeps Vision; ws, grpc, xhttp, mkcp and mekya never emit it.
Tests pin each path in both directions — a stale flow disappears on ws/grpc/
xhttp, and a genuine TCP listener keeps its flow. The frontend one runs the real
form builder and fails, in three of five cases, against the previous ordering.
* fix(uri): carry vmess ws Host and align hysteria2 TLS flags across user shapes
Auditing the remaining exporters after the flow fix turned up two more places
where the link disagreed with the listener.
vmessURIs built its JSON by hand instead of going through transportParams, so a
ws listener that pins a Host header exported only the path: the client had
nothing to send as Host and the listener was unreachable through the link, even
though the client YAML built from the same config worked. It also dropped
allowInsecure, which vless and trojan both carry — a listener whose certificate
the client cannot verify exported a link that failed the handshake. The repeated
tlsParams() calls were hoisted while touching this.
hysteria2URIs reads users either as rows or as a map, and the two branches
disagreed: one asked certutil.ShouldSkipCertVerify and emitted only `insecure`,
the other asked clientSkipCert and emitted `insecure` plus `allowInsecure`. The
same listener therefore produced different links depending on how its users were
stored. Both now use clientSkipCert, the helper intended for share URIs, and
emit both keys.
Not fixed, for the record: an SS listener with simple-obfs exports a bare ss://
link with no `plugin=` entry, so the obfs configuration is lost. The client YAML
does translate it (plugin: obfs). Fixing the URI means committing to a SIP002
plugin encoding that varies between clients, which is worth deciding separately
rather than guessing at here.
* i18n: complete zh-CN and guard against the locale regressions already seen
zh-CN was missing seven listener hints that en.ts declares, so those tooltips
rendered in English for the primary UI language. Translated: client_auth_cert,
client_auth_type, ech_key, enable_pure_downlink, mux, padding and table_type.
The rest of the catalogue is deliberately left alone. Seventeen locales each lack
65 keys that en.ts has; the provider already resolves those from en.ts, so they
degrade to English rather than breaking, and filling 1100 strings with
unreviewed machine translation would be worse than the fallback. The test prints
the per-locale gap so it stays visible.
The new locale test guards the regressions this directory has actually hit: a
duplicated key inside one object (which breaks the type check), and a key renamed
in en.ts leaving an orphan behind. It also asserts zh-CN stays complete, since a
gap there reads as a defect rather than as translation lag.
* i18n: declare the 24 keys the UI asks for but no locale ever defined
Twenty-four keys were referenced from components without being declared in any
locale, so t() fell through to its hardcoded fallback. Where a fallback existed
that merely meant the string could never be translated; where none existed —
protocolHint, access_sniHint, jlsHint, shadowTlsHint and the rest — the tooltip
either rendered the raw key, "listeners.protocolHint", or disappeared entirely,
because fieldTip() suppresses a value that equals the key it looked up.
en.ts takes the text the code was already falling back to, so nothing changes on
screen; the strings just become translatable. The few without a fallback were
written from the surrounding context — the field they annotate, its placeholder,
and the wording of the neighbouring hints. trafficMultiplierHint is the exception:
its fallback was Chinese, which is wrong for the English catalogue, so it now has
a real English string.
zh-CN gets all twenty-four, which keeps it complete against en.ts.
The locale test now reads through the TypeScript AST. It previously parsed line by
line and these files contain minified regions — several "key":"value" pairs on one
line — so a whole block of keys was invisible to it, which is how this gap stayed
hidden. It also gained the assertion that would have caught the bug directly:
every dotted key handed to t() or fieldTip() must be declared in en.ts.
The remaining languages still lag, now by 89 keys rather than 65, and still fall
back to English; the test prints the per-locale figure.
* fix(uri): drop the vmess allowInsecure field the reference client ignores
The vmess skip-certificate field was added on internal consistency alone —
vless and trojan carry allowInsecure in their query strings, so vmess looked
like an oversight. Checked against the client that actually parses these links,
it is not one.
v2rayN's VmessQRCode model declares exactly: v, ps, add, port, id, aid, scy,
net, type, host, path, tls, sni, alpn, fp. There is no skip-certificate field
at all — neither allowInsecure nor insecure — so the field was dead weight in a
payload that v2rayN reads field by field. v2rayNG reads `insecure` instead, and
Xray-core has removed allowInsecure in favour of pinnedPeerCertSha256, so there
is no spelling that the reference clients agree on. Emitting one would have
produced a link that looks configured but is not.
The ws `host` fix stands: v2rayN's own documentation lists "ws host" as the
meaning of that field, so dropping it broke ws listeners that pin a Host.
The conformance test now pins the emitted payload to v2rayN's field set, so a
future field copied over from another protocol fails instead of being silently
ignored by every client.
Deliberately left alone, both confirmed against the official scheme and worth
deciding separately:
- hysteria2 links carry up/down. The Hysteria 2 URI specification is explicit
that the parameters "should never include client modes ... or bandwidth
values". Removing them changes behaviour for anyone relying on the link, so
it is flagged rather than removed. `insecure` is the official parameter;
allowInsecure is kept alongside it purely for clients that read that name.
- an SS listener with simple-obfs still exports a bare ss:// link with no
plugin entry, losing the obfuscation the client YAML does translate.
Also noted while checking: tuicURIs defaults congestion_control to bbr where
sing-box documents cubic as its default. Ours is set explicitly, which is
valid, but the comment claiming it is what "most TUIC clients" use is not
sourced.
* fix(uri): keep vmess allowInsecure after all
Reverts the revert. Dropping it was correct on the evidence and wrong on the
consequences: v2rayN ignores the field, v2rayNG spells it `insecure`, and
Xray-core has moved to pinnedPeerCertSha256 — but the clients that do read
allowInsecure are precisely the ones whose handshake fails without it, and an
ignored extra key costs the others nothing. Removing it would have traded a
working link for a tidy payload.
The conformance test keeps its guard, with allowInsecure recorded as an
explicit, justified exception rather than a silent inclusion: the field set is
still pinned to v2rayN's model, so any further field copied over from another
protocol has to argue its way in.
* fix(uri): restore the vmess ws Host in registry YAML, and stop dropping share links
Two leftovers from auditing the exporters end to end.
The registry's VMess compiler built ws-opts with the path only, so a listener
that pins a Host exported a client config with nothing to send as Host. The
vless YAML path in the same file already nests it under `headers` per the mihomo
wiki, so this just mirrors that — and the converter has always carried it, which
is how the difference went unnoticed.
The node URI share endpoint also had a tier that could not be reached. It walks
three tiers — m-ui, the registry, then the legacy builders — and the second one
gates the third on having neither a URI nor client YAML. But the registry
implements URI export for a different protocol set than the legacy builders: it
compiles vmess, snell, shadowquic, mieru, sudoku and trusttunnel to client YAML
without ever emitting a URI, so those listeners arrived at the third tier with a
YAML in hand and the condition already false, and reported no link at all even
though ClientURIsWithCredentials exports one. The third tier now runs whenever
the URI is still missing, and only fills in YAML that earlier tiers did not
already provide.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
fix(vless): stop exporting flow on transports that cannot carry it (#87)
* fix(vless): stop exporting flow on transports that cannot carry it
The form promised that flow is cleared for ws/grpc/xhttp and the tooltip still
says so, but exported links kept carrying flow=xtls-rprx-vision. Two separate
reasons, both fixed.
The clear came too late to matter. formValuesToConfig wrote `flow` into the
config inside the protocol switch, and only afterwards did the "Vision is
TCP-only" rule run — where it mutated the form state, not the config it had
already been copied into. The transport is now resolved before anything writes,
and the drop also removes a flow inherited from the previous config, so changing
a listener's transport actually takes effect.
The URI generators never asked. Every one of them emitted whatever flow it found:
- node: vlessURIs decided flow before resolving the transport, so the type it
had just computed was irrelevant to the decision. It now resolves the
transport first. ClientURIsWithCredentials also merged the listener-level
flow onto every user on protocol alone, which reintroduced the value the
form had dropped.
- protocol: the share URI and the client YAML both wrote spec.Flow unguarded,
even though TransportSpec knows the network. Same for the server-side
compile, which pushed the listener default onto users — Mihomo rejects that
combination outright, so this one takes the listener down.
- mui: BuildVLESSShare set flow unconditionally, and bridge.go injected the
listener default into users before it was known to be applicable.
- converter: copyTransport resolved `network` but left a stale flow behind.
The rule is now one predicate per representation: TransportSpec.CarriesFlow,
VLESSHandlerSpec.CarriesFlow, and protocol.TransportCarriesFlow for raw configs.
TCP/raw keeps Vision; ws, grpc, xhttp, mkcp and mekya never emit it.
Tests pin each path in both directions — a stale flow disappears on ws/grpc/
xhttp, and a genuine TCP listener keeps its flow. The frontend one runs the real
form builder and fails, in three of five cases, against the previous ordering.
* fix(uri): carry vmess ws Host and align hysteria2 TLS flags across user shapes
Auditing the remaining exporters after the flow fix turned up two more places
where the link disagreed with the listener.
vmessURIs built its JSON by hand instead of going through transportParams, so a
ws listener that pins a Host header exported only the path: the client had
nothing to send as Host and the listener was unreachable through the link, even
though the client YAML built from the same config worked. It also dropped
allowInsecure, which vless and trojan both carry — a listener whose certificate
the client cannot verify exported a link that failed the handshake. The repeated
tlsParams() calls were hoisted while touching this.
hysteria2URIs reads users either as rows or as a map, and the two branches
disagreed: one asked certutil.ShouldSkipCertVerify and emitted only `insecure`,
the other asked clientSkipCert and emitted `insecure` plus `allowInsecure`. The
same listener therefore produced different links depending on how its users were
stored. Both now use clientSkipCert, the helper intended for share URIs, and
emit both keys.
Not fixed, for the record: an SS listener with simple-obfs exports a bare ss://
link with no `plugin=` entry, so the obfs configuration is lost. The client YAML
does translate it (plugin: obfs). Fixing the URI means committing to a SIP002
plugin encoding that varies between clients, which is worth deciding separately
rather than guessing at here.
* i18n: complete zh-CN and guard against the locale regressions already seen
zh-CN was missing seven listener hints that en.ts declares, so those tooltips
rendered in English for the primary UI language. Translated: client_auth_cert,
client_auth_type, ech_key, enable_pure_downlink, mux, padding and table_type.
The rest of the catalogue is deliberately left alone. Seventeen locales each lack
65 keys that en.ts has; the provider already resolves those from en.ts, so they
degrade to English rather than breaking, and filling 1100 strings with
unreviewed machine translation would be worse than the fallback. The test prints
the per-locale gap so it stays visible.
The new locale test guards the regressions this directory has actually hit: a
duplicated key inside one object (which breaks the type check), and a key renamed
in en.ts leaving an orphan behind. It also asserts zh-CN stays complete, since a
gap there reads as a defect rather than as translation lag.
* i18n: declare the 24 keys the UI asks for but no locale ever defined
Twenty-four keys were referenced from components without being declared in any
locale, so t() fell through to its hardcoded fallback. Where a fallback existed
that merely meant the string could never be translated; where none existed —
protocolHint, access_sniHint, jlsHint, shadowTlsHint and the rest — the tooltip
either rendered the raw key, "listeners.protocolHint", or disappeared entirely,
because fieldTip() suppresses a value that equals the key it looked up.
en.ts takes the text the code was already falling back to, so nothing changes on
screen; the strings just become translatable. The few without a fallback were
written from the surrounding context — the field they annotate, its placeholder,
and the wording of the neighbouring hints. trafficMultiplierHint is the exception:
its fallback was Chinese, which is wrong for the English catalogue, so it now has
a real English string.
zh-CN gets all twenty-four, which keeps it complete against en.ts.
The locale test now reads through the TypeScript AST. It previously parsed line by
line and these files contain minified regions — several "key":"value" pairs on one
line — so a whole block of keys was invisible to it, which is how this gap stayed
hidden. It also gained the assertion that would have caught the bug directly:
every dotted key handed to t() or fieldTip() must be declared in en.ts.
The remaining languages still lag, now by 89 keys rather than 65, and still fall
back to English; the test prints the per-locale figure.
* fix(uri): drop the vmess allowInsecure field the reference client ignores
The vmess skip-certificate field was added on internal consistency alone —
vless and trojan carry allowInsecure in their query strings, so vmess looked
like an oversight. Checked against the client that actually parses these links,
it is not one.
v2rayN's VmessQRCode model declares exactly: v, ps, add, port, id, aid, scy,
net, type, host, path, tls, sni, alpn, fp. There is no skip-certificate field
at all — neither allowInsecure nor insecure — so the field was dead weight in a
payload that v2rayN reads field by field. v2rayNG reads `insecure` instead, and
Xray-core has removed allowInsecure in favour of pinnedPeerCertSha256, so there
is no spelling that the reference clients agree on. Emitting one would have
produced a link that looks configured but is not.
The ws `host` fix stands: v2rayN's own documentation lists "ws host" as the
meaning of that field, so dropping it broke ws listeners that pin a Host.
The conformance test now pins the emitted payload to v2rayN's field set, so a
future field copied over from another protocol fails instead of being silently
ignored by every client.
Deliberately left alone, both confirmed against the official scheme and worth
deciding separately:
- hysteria2 links carry up/down. The Hysteria 2 URI specification is explicit
that the parameters "should never include client modes ... or bandwidth
values". Removing them changes behaviour for anyone relying on the link, so
it is flagged rather than removed. `insecure` is the official parameter;
allowInsecure is kept alongside it purely for clients that read that name.
- an SS listener with simple-obfs still exports a bare ss:// link with no
plugin entry, losing the obfuscation the client YAML does translate.
Also noted while checking: tuicURIs defaults congestion_control to bbr where
sing-box documents cubic as its default. Ours is set explicitly, which is
valid, but the comment claiming it is what "most TUIC clients" use is not
sourced.
* fix(uri): keep vmess allowInsecure after all
Reverts the revert. Dropping it was correct on the evidence and wrong on the
consequences: v2rayN ignores the field, v2rayNG spells it `insecure`, and
Xray-core has moved to pinnedPeerCertSha256 — but the clients that do read
allowInsecure are precisely the ones whose handshake fails without it, and an
ignored extra key costs the others nothing. Removing it would have traded a
working link for a tidy payload.
The conformance test keeps its guard, with allowInsecure recorded as an
explicit, justified exception rather than a silent inclusion: the field set is
still pinned to v2rayN's model, so any further field copied over from another
protocol has to argue its way in.
* fix(uri): restore the vmess ws Host in registry YAML, and stop dropping share links
Two leftovers from auditing the exporters end to end.
The registry's VMess compiler built ws-opts with the path only, so a listener
that pins a Host exported a client config with nothing to send as Host. The
vless YAML path in the same file already nests it under `headers` per the mihomo
wiki, so this just mirrors that — and the converter has always carried it, which
is how the difference went unnoticed.
The node URI share endpoint also had a tier that could not be reached. It walks
three tiers — m-ui, the registry, then the legacy builders — and the second one
gates the third on having neither a URI nor client YAML. But the registry
implements URI export for a different protocol set than the legacy builders: it
compiles vmess, snell, shadowquic, mieru, sudoku and trusttunnel to client YAML
without ever emitting a URI, so those listeners arrived at the third tier with a
YAML in hand and the condition already false, and reported no link at all even
though ClientURIsWithCredentials exports one. The third tier now runs whenever
the URI is still missing, and only fills in YAML that earlier tiers did not
already provide.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
2026-09-28 00:36:07 +08:00
風雪and風雪 232a4432d8 fix(metrics): resolve the real cgroup on LXC for memory and CPU (#85)
* fix(metrics): resolve the real cgroup on LXC, not just the hierarchy root
On LXC the memory cards still disagreed. Path resolution assumed
/sys/fs/cgroup is the whole hierarchy and simply appended the path from
/proc/self/cgroup to it. LXC's default (cgroup:mixed) mounts a tmpfs there and
bind-mounts only the container's own cgroup into a directory below it, so that
path names directories relative to the host hierarchy and does not exist from
inside the container. Resolution then failed, readServiceCgroupMemory() had no
fallback, and the per-process card silently degraded to raw RSS while the
system card read a container-scoped cgroup — two different sources, which is
exactly the inconsistency being reported.
The path is now translated through the mount's root from /proc/self/mountinfo:
if /sys/fs/cgroup exposes only a subtree, the part of the reported path that
the mount hides is dropped. That resolves both the container's own cgroup and
any unit nested inside it, so the two cards land on one accounting domain
instead of "working by accident" on hosts where the mount happens to be the
root.
readServiceCgroupMemory() also gained the root fallback it was missing, so a
layout that cannot be resolved at all still reads the domain the process is
accounted in rather than falling back to RSS. Adopting the root is gated on a
finite cap being enforced there: on a bare-metal host the root is unbounded and
attributing the machine's whole memory to two processes would be a
fabrication, so those hosts keep the previous RSS behaviour.
Both readers now share cgroupMemoryFiles(), so the v2/v1 field probing exists
once instead of being copied per caller — the duplication is how the missing
fallback went unnoticed. Memory path constants are dropped with it, and the
v1 PAGE_COUNTER_MAX sentinel is named rather than open-coded.
Tests cover the layouts directly, with /proc and /sys pointed at fixtures
through named path variables: plain hierarchy root, bind-mounted container
cgroup, a unit nested below it, cgroup v1, the bounded and unbounded fallback,
sentinel rejection, and the invariant that both cards resolve to the same
usage. A malformed mountinfo degrades to "unresolved", never to a wrong path.
* fix(metrics): read CPU accounting from the resolved cgroup
cpu.stat, cpu.max and cpuset.cpus.effective were read from hard-coded
hierarchy-root paths, so they had the same flaw the memory files had: in a
container the accounting for this process is not at the root. On LXC the root
is a tmpfs holding nothing readable, so the CPU card silently fell back to
host-wide /proc/stat and reported the machine instead of the container — the
container could be fully throttled while the bar sat near idle.
They now go through cpuAccountingDir(), which picks the process' own cgroup
when the kernel enforces a cap there (a memory limit or a CPU quota) and
otherwise keeps the hierarchy root. That preserves bare-metal semantics, where
the card is meant to describe the machine rather than the panel's own unit,
while a container — the case where the root is simply the wrong place to look —
is read from the cgroup that actually confines it.
Usage is taken from the first directory that has it, never mixed across two:
a delta between two different cgroups would be meaningless. Caps are probed
across the directories instead, since a quota on a broader domain still applies
to us, and cpuCapacity() already narrows to the tightest limit found.
The three fixed CPU path constants go away with the hard-coded reads; every
accounting file is now read relative to a resolved directory.
* test(system): stop comparing memory samples taken at different times
Two tests guarded the invariant that the system and process cards read the same
memory pool, and both failed intermittently — reproducibly under -race, and
occasionally in a plain run, which the backend job executes:
system memory used (2874142720) is smaller than this process' RSS (2875170816)
panel memory 2874384384 exceeds system used 2874142720
They compared a possibly 500ms-stale cached system reading against a live
process reading of the test binary itself. Where the panel is most of its own
accounting domain — a container, or a sandbox running the test alone — those two
numbers are nearly identical, so the comparison has no margin of its own and any
allocation in between flips it.
The process is now sampled first, so the reference reading is the newer one, and
a tolerance absorbs the remaining skew. What these tests guard is a mismatch of
sources, which was out by multiples (host /proc/meminfo against the cgroup), so
a percent-wide tolerance keeps the assertion meaningful.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
fix(metrics): resolve the real cgroup on LXC for memory and CPU (#85)
* fix(metrics): resolve the real cgroup on LXC, not just the hierarchy root
On LXC the memory cards still disagreed. Path resolution assumed
/sys/fs/cgroup is the whole hierarchy and simply appended the path from
/proc/self/cgroup to it. LXC's default (cgroup:mixed) mounts a tmpfs there and
bind-mounts only the container's own cgroup into a directory below it, so that
path names directories relative to the host hierarchy and does not exist from
inside the container. Resolution then failed, readServiceCgroupMemory() had no
fallback, and the per-process card silently degraded to raw RSS while the
system card read a container-scoped cgroup — two different sources, which is
exactly the inconsistency being reported.
The path is now translated through the mount's root from /proc/self/mountinfo:
if /sys/fs/cgroup exposes only a subtree, the part of the reported path that
the mount hides is dropped. That resolves both the container's own cgroup and
any unit nested inside it, so the two cards land on one accounting domain
instead of "working by accident" on hosts where the mount happens to be the
root.
readServiceCgroupMemory() also gained the root fallback it was missing, so a
layout that cannot be resolved at all still reads the domain the process is
accounted in rather than falling back to RSS. Adopting the root is gated on a
finite cap being enforced there: on a bare-metal host the root is unbounded and
attributing the machine's whole memory to two processes would be a
fabrication, so those hosts keep the previous RSS behaviour.
Both readers now share cgroupMemoryFiles(), so the v2/v1 field probing exists
once instead of being copied per caller — the duplication is how the missing
fallback went unnoticed. Memory path constants are dropped with it, and the
v1 PAGE_COUNTER_MAX sentinel is named rather than open-coded.
Tests cover the layouts directly, with /proc and /sys pointed at fixtures
through named path variables: plain hierarchy root, bind-mounted container
cgroup, a unit nested below it, cgroup v1, the bounded and unbounded fallback,
sentinel rejection, and the invariant that both cards resolve to the same
usage. A malformed mountinfo degrades to "unresolved", never to a wrong path.
* fix(metrics): read CPU accounting from the resolved cgroup
cpu.stat, cpu.max and cpuset.cpus.effective were read from hard-coded
hierarchy-root paths, so they had the same flaw the memory files had: in a
container the accounting for this process is not at the root. On LXC the root
is a tmpfs holding nothing readable, so the CPU card silently fell back to
host-wide /proc/stat and reported the machine instead of the container — the
container could be fully throttled while the bar sat near idle.
They now go through cpuAccountingDir(), which picks the process' own cgroup
when the kernel enforces a cap there (a memory limit or a CPU quota) and
otherwise keeps the hierarchy root. That preserves bare-metal semantics, where
the card is meant to describe the machine rather than the panel's own unit,
while a container — the case where the root is simply the wrong place to look —
is read from the cgroup that actually confines it.
Usage is taken from the first directory that has it, never mixed across two:
a delta between two different cgroups would be meaningless. Caps are probed
across the directories instead, since a quota on a broader domain still applies
to us, and cpuCapacity() already narrows to the tightest limit found.
The three fixed CPU path constants go away with the hard-coded reads; every
accounting file is now read relative to a resolved directory.
* test(system): stop comparing memory samples taken at different times
Two tests guarded the invariant that the system and process cards read the same
memory pool, and both failed intermittently — reproducibly under -race, and
occasionally in a plain run, which the backend job executes:
system memory used (2874142720) is smaller than this process' RSS (2875170816)
panel memory 2874384384 exceeds system used 2874142720
They compared a possibly 500ms-stale cached system reading against a live
process reading of the test binary itself. Where the panel is most of its own
accounting domain — a container, or a sandbox running the test alone — those two
numbers are nearly identical, so the comparison has no margin of its own and any
allocation in between flips it.
The process is now sampled first, so the reference reading is the newer one, and
a tolerance absorbs the remaining skew. What these tests guard is a mismatch of
sources, which was out by multiples (host /proc/meminfo against the cgroup), so
a percent-wide tolerance keeps the assertion meaningful.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
fix(metrics): resolve the real cgroup on LXC for memory and CPU (#85)
* fix(metrics): resolve the real cgroup on LXC, not just the hierarchy root
On LXC the memory cards still disagreed. Path resolution assumed
/sys/fs/cgroup is the whole hierarchy and simply appended the path from
/proc/self/cgroup to it. LXC's default (cgroup:mixed) mounts a tmpfs there and
bind-mounts only the container's own cgroup into a directory below it, so that
path names directories relative to the host hierarchy and does not exist from
inside the container. Resolution then failed, readServiceCgroupMemory() had no
fallback, and the per-process card silently degraded to raw RSS while the
system card read a container-scoped cgroup — two different sources, which is
exactly the inconsistency being reported.
The path is now translated through the mount's root from /proc/self/mountinfo:
if /sys/fs/cgroup exposes only a subtree, the part of the reported path that
the mount hides is dropped. That resolves both the container's own cgroup and
any unit nested inside it, so the two cards land on one accounting domain
instead of "working by accident" on hosts where the mount happens to be the
root.
readServiceCgroupMemory() also gained the root fallback it was missing, so a
layout that cannot be resolved at all still reads the domain the process is
accounted in rather than falling back to RSS. Adopting the root is gated on a
finite cap being enforced there: on a bare-metal host the root is unbounded and
attributing the machine's whole memory to two processes would be a
fabrication, so those hosts keep the previous RSS behaviour.
Both readers now share cgroupMemoryFiles(), so the v2/v1 field probing exists
once instead of being copied per caller — the duplication is how the missing
fallback went unnoticed. Memory path constants are dropped with it, and the
v1 PAGE_COUNTER_MAX sentinel is named rather than open-coded.
Tests cover the layouts directly, with /proc and /sys pointed at fixtures
through named path variables: plain hierarchy root, bind-mounted container
cgroup, a unit nested below it, cgroup v1, the bounded and unbounded fallback,
sentinel rejection, and the invariant that both cards resolve to the same
usage. A malformed mountinfo degrades to "unresolved", never to a wrong path.
* fix(metrics): read CPU accounting from the resolved cgroup
cpu.stat, cpu.max and cpuset.cpus.effective were read from hard-coded
hierarchy-root paths, so they had the same flaw the memory files had: in a
container the accounting for this process is not at the root. On LXC the root
is a tmpfs holding nothing readable, so the CPU card silently fell back to
host-wide /proc/stat and reported the machine instead of the container — the
container could be fully throttled while the bar sat near idle.
They now go through cpuAccountingDir(), which picks the process' own cgroup
when the kernel enforces a cap there (a memory limit or a CPU quota) and
otherwise keeps the hierarchy root. That preserves bare-metal semantics, where
the card is meant to describe the machine rather than the panel's own unit,
while a container — the case where the root is simply the wrong place to look —
is read from the cgroup that actually confines it.
Usage is taken from the first directory that has it, never mixed across two:
a delta between two different cgroups would be meaningless. Caps are probed
across the directories instead, since a quota on a broader domain still applies
to us, and cpuCapacity() already narrows to the tightest limit found.
The three fixed CPU path constants go away with the hard-coded reads; every
accounting file is now read relative to a resolved directory.
* test(system): stop comparing memory samples taken at different times
Two tests guarded the invariant that the system and process cards read the same
memory pool, and both failed intermittently — reproducibly under -race, and
occasionally in a plain run, which the backend job executes:
system memory used (2874142720) is smaller than this process' RSS (2875170816)
panel memory 2874384384 exceeds system used 2874142720
They compared a possibly 500ms-stale cached system reading against a live
process reading of the test binary itself. Where the panel is most of its own
accounting domain — a container, or a sandbox running the test alone — those two
numbers are nearly identical, so the comparison has no margin of its own and any
allocation in between flips it.
The process is now sampled first, so the reference reading is the newer one, and
a tolerance absorbs the remaining skew. What these tests guard is a mismatch of
sources, which was out by multiples (host /proc/meminfo against the cgroup), so
a percent-wide tolerance keeps the assertion meaningful.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
fix(metrics): resolve the real cgroup on LXC for memory and CPU (#85)
* fix(metrics): resolve the real cgroup on LXC, not just the hierarchy root
On LXC the memory cards still disagreed. Path resolution assumed
/sys/fs/cgroup is the whole hierarchy and simply appended the path from
/proc/self/cgroup to it. LXC's default (cgroup:mixed) mounts a tmpfs there and
bind-mounts only the container's own cgroup into a directory below it, so that
path names directories relative to the host hierarchy and does not exist from
inside the container. Resolution then failed, readServiceCgroupMemory() had no
fallback, and the per-process card silently degraded to raw RSS while the
system card read a container-scoped cgroup — two different sources, which is
exactly the inconsistency being reported.
The path is now translated through the mount's root from /proc/self/mountinfo:
if /sys/fs/cgroup exposes only a subtree, the part of the reported path that
the mount hides is dropped. That resolves both the container's own cgroup and
any unit nested inside it, so the two cards land on one accounting domain
instead of "working by accident" on hosts where the mount happens to be the
root.
readServiceCgroupMemory() also gained the root fallback it was missing, so a
layout that cannot be resolved at all still reads the domain the process is
accounted in rather than falling back to RSS. Adopting the root is gated on a
finite cap being enforced there: on a bare-metal host the root is unbounded and
attributing the machine's whole memory to two processes would be a
fabrication, so those hosts keep the previous RSS behaviour.
Both readers now share cgroupMemoryFiles(), so the v2/v1 field probing exists
once instead of being copied per caller — the duplication is how the missing
fallback went unnoticed. Memory path constants are dropped with it, and the
v1 PAGE_COUNTER_MAX sentinel is named rather than open-coded.
Tests cover the layouts directly, with /proc and /sys pointed at fixtures
through named path variables: plain hierarchy root, bind-mounted container
cgroup, a unit nested below it, cgroup v1, the bounded and unbounded fallback,
sentinel rejection, and the invariant that both cards resolve to the same
usage. A malformed mountinfo degrades to "unresolved", never to a wrong path.
* fix(metrics): read CPU accounting from the resolved cgroup
cpu.stat, cpu.max and cpuset.cpus.effective were read from hard-coded
hierarchy-root paths, so they had the same flaw the memory files had: in a
container the accounting for this process is not at the root. On LXC the root
is a tmpfs holding nothing readable, so the CPU card silently fell back to
host-wide /proc/stat and reported the machine instead of the container — the
container could be fully throttled while the bar sat near idle.
They now go through cpuAccountingDir(), which picks the process' own cgroup
when the kernel enforces a cap there (a memory limit or a CPU quota) and
otherwise keeps the hierarchy root. That preserves bare-metal semantics, where
the card is meant to describe the machine rather than the panel's own unit,
while a container — the case where the root is simply the wrong place to look —
is read from the cgroup that actually confines it.
Usage is taken from the first directory that has it, never mixed across two:
a delta between two different cgroups would be meaningless. Caps are probed
across the directories instead, since a quota on a broader domain still applies
to us, and cpuCapacity() already narrows to the tightest limit found.
The three fixed CPU path constants go away with the hard-coded reads; every
accounting file is now read relative to a resolved directory.
* test(system): stop comparing memory samples taken at different times
Two tests guarded the invariant that the system and process cards read the same
memory pool, and both failed intermittently — reproducibly under -race, and
occasionally in a plain run, which the backend job executes:
system memory used (2874142720) is smaller than this process' RSS (2875170816)
panel memory 2874384384 exceeds system used 2874142720
They compared a possibly 500ms-stale cached system reading against a live
process reading of the test binary itself. Where the panel is most of its own
accounting domain — a container, or a sandbox running the test alone — those two
numbers are nearly identical, so the comparison has no margin of its own and any
allocation in between flips it.
The process is now sampled first, so the reference reading is the newer one, and
a tolerance absorbs the remaining skew. What these tests guard is a mismatch of
sources, which was out by multiples (host /proc/meminfo against the cgroup), so
a percent-wide tolerance keeps the assertion meaningful.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
fix(metrics): resolve the real cgroup on LXC for memory and CPU (#85)
* fix(metrics): resolve the real cgroup on LXC, not just the hierarchy root
On LXC the memory cards still disagreed. Path resolution assumed
/sys/fs/cgroup is the whole hierarchy and simply appended the path from
/proc/self/cgroup to it. LXC's default (cgroup:mixed) mounts a tmpfs there and
bind-mounts only the container's own cgroup into a directory below it, so that
path names directories relative to the host hierarchy and does not exist from
inside the container. Resolution then failed, readServiceCgroupMemory() had no
fallback, and the per-process card silently degraded to raw RSS while the
system card read a container-scoped cgroup — two different sources, which is
exactly the inconsistency being reported.
The path is now translated through the mount's root from /proc/self/mountinfo:
if /sys/fs/cgroup exposes only a subtree, the part of the reported path that
the mount hides is dropped. That resolves both the container's own cgroup and
any unit nested inside it, so the two cards land on one accounting domain
instead of "working by accident" on hosts where the mount happens to be the
root.
readServiceCgroupMemory() also gained the root fallback it was missing, so a
layout that cannot be resolved at all still reads the domain the process is
accounted in rather than falling back to RSS. Adopting the root is gated on a
finite cap being enforced there: on a bare-metal host the root is unbounded and
attributing the machine's whole memory to two processes would be a
fabrication, so those hosts keep the previous RSS behaviour.
Both readers now share cgroupMemoryFiles(), so the v2/v1 field probing exists
once instead of being copied per caller — the duplication is how the missing
fallback went unnoticed. Memory path constants are dropped with it, and the
v1 PAGE_COUNTER_MAX sentinel is named rather than open-coded.
Tests cover the layouts directly, with /proc and /sys pointed at fixtures
through named path variables: plain hierarchy root, bind-mounted container
cgroup, a unit nested below it, cgroup v1, the bounded and unbounded fallback,
sentinel rejection, and the invariant that both cards resolve to the same
usage. A malformed mountinfo degrades to "unresolved", never to a wrong path.
* fix(metrics): read CPU accounting from the resolved cgroup
cpu.stat, cpu.max and cpuset.cpus.effective were read from hard-coded
hierarchy-root paths, so they had the same flaw the memory files had: in a
container the accounting for this process is not at the root. On LXC the root
is a tmpfs holding nothing readable, so the CPU card silently fell back to
host-wide /proc/stat and reported the machine instead of the container — the
container could be fully throttled while the bar sat near idle.
They now go through cpuAccountingDir(), which picks the process' own cgroup
when the kernel enforces a cap there (a memory limit or a CPU quota) and
otherwise keeps the hierarchy root. That preserves bare-metal semantics, where
the card is meant to describe the machine rather than the panel's own unit,
while a container — the case where the root is simply the wrong place to look —
is read from the cgroup that actually confines it.
Usage is taken from the first directory that has it, never mixed across two:
a delta between two different cgroups would be meaningless. Caps are probed
across the directories instead, since a quota on a broader domain still applies
to us, and cpuCapacity() already narrows to the tightest limit found.
The three fixed CPU path constants go away with the hard-coded reads; every
accounting file is now read relative to a resolved directory.
* test(system): stop comparing memory samples taken at different times
Two tests guarded the invariant that the system and process cards read the same
memory pool, and both failed intermittently — reproducibly under -race, and
occasionally in a plain run, which the backend job executes:
system memory used (2874142720) is smaller than this process' RSS (2875170816)
panel memory 2874384384 exceeds system used 2874142720
They compared a possibly 500ms-stale cached system reading against a live
process reading of the test binary itself. Where the panel is most of its own
accounting domain — a container, or a sandbox running the test alone — those two
numbers are nearly identical, so the comparison has no margin of its own and any
allocation in between flips it.
The process is now sampled first, so the reference reading is the newer one, and
a tolerance absorbs the remaining skew. What these tests guard is a mismatch of
sources, which was out by multiples (host /proc/meminfo against the cgroup), so
a percent-wide tolerance keeps the assertion meaningful.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
fix(metrics): resolve the real cgroup on LXC for memory and CPU (#85)
* fix(metrics): resolve the real cgroup on LXC, not just the hierarchy root
On LXC the memory cards still disagreed. Path resolution assumed
/sys/fs/cgroup is the whole hierarchy and simply appended the path from
/proc/self/cgroup to it. LXC's default (cgroup:mixed) mounts a tmpfs there and
bind-mounts only the container's own cgroup into a directory below it, so that
path names directories relative to the host hierarchy and does not exist from
inside the container. Resolution then failed, readServiceCgroupMemory() had no
fallback, and the per-process card silently degraded to raw RSS while the
system card read a container-scoped cgroup — two different sources, which is
exactly the inconsistency being reported.
The path is now translated through the mount's root from /proc/self/mountinfo:
if /sys/fs/cgroup exposes only a subtree, the part of the reported path that
the mount hides is dropped. That resolves both the container's own cgroup and
any unit nested inside it, so the two cards land on one accounting domain
instead of "working by accident" on hosts where the mount happens to be the
root.
readServiceCgroupMemory() also gained the root fallback it was missing, so a
layout that cannot be resolved at all still reads the domain the process is
accounted in rather than falling back to RSS. Adopting the root is gated on a
finite cap being enforced there: on a bare-metal host the root is unbounded and
attributing the machine's whole memory to two processes would be a
fabrication, so those hosts keep the previous RSS behaviour.
Both readers now share cgroupMemoryFiles(), so the v2/v1 field probing exists
once instead of being copied per caller — the duplication is how the missing
fallback went unnoticed. Memory path constants are dropped with it, and the
v1 PAGE_COUNTER_MAX sentinel is named rather than open-coded.
Tests cover the layouts directly, with /proc and /sys pointed at fixtures
through named path variables: plain hierarchy root, bind-mounted container
cgroup, a unit nested below it, cgroup v1, the bounded and unbounded fallback,
sentinel rejection, and the invariant that both cards resolve to the same
usage. A malformed mountinfo degrades to "unresolved", never to a wrong path.
* fix(metrics): read CPU accounting from the resolved cgroup
cpu.stat, cpu.max and cpuset.cpus.effective were read from hard-coded
hierarchy-root paths, so they had the same flaw the memory files had: in a
container the accounting for this process is not at the root. On LXC the root
is a tmpfs holding nothing readable, so the CPU card silently fell back to
host-wide /proc/stat and reported the machine instead of the container — the
container could be fully throttled while the bar sat near idle.
They now go through cpuAccountingDir(), which picks the process' own cgroup
when the kernel enforces a cap there (a memory limit or a CPU quota) and
otherwise keeps the hierarchy root. That preserves bare-metal semantics, where
the card is meant to describe the machine rather than the panel's own unit,
while a container — the case where the root is simply the wrong place to look —
is read from the cgroup that actually confines it.
Usage is taken from the first directory that has it, never mixed across two:
a delta between two different cgroups would be meaningless. Caps are probed
across the directories instead, since a quota on a broader domain still applies
to us, and cpuCapacity() already narrows to the tightest limit found.
The three fixed CPU path constants go away with the hard-coded reads; every
accounting file is now read relative to a resolved directory.
* test(system): stop comparing memory samples taken at different times
Two tests guarded the invariant that the system and process cards read the same
memory pool, and both failed intermittently — reproducibly under -race, and
occasionally in a plain run, which the backend job executes:
system memory used (2874142720) is smaller than this process' RSS (2875170816)
panel memory 2874384384 exceeds system used 2874142720
They compared a possibly 500ms-stale cached system reading against a live
process reading of the test binary itself. Where the panel is most of its own
accounting domain — a container, or a sandbox running the test alone — those two
numbers are nearly identical, so the comparison has no margin of its own and any
allocation in between flips it.
The process is now sampled first, so the reference reading is the newer one, and
a tolerance absorbs the remaining skew. What these tests guard is a mismatch of
sources, which was out by multiples (host /proc/meminfo against the cgroup), so
a percent-wide tolerance keeps the assertion meaningful.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
fix(metrics): resolve the real cgroup on LXC for memory and CPU (#85)
* fix(metrics): resolve the real cgroup on LXC, not just the hierarchy root
On LXC the memory cards still disagreed. Path resolution assumed
/sys/fs/cgroup is the whole hierarchy and simply appended the path from
/proc/self/cgroup to it. LXC's default (cgroup:mixed) mounts a tmpfs there and
bind-mounts only the container's own cgroup into a directory below it, so that
path names directories relative to the host hierarchy and does not exist from
inside the container. Resolution then failed, readServiceCgroupMemory() had no
fallback, and the per-process card silently degraded to raw RSS while the
system card read a container-scoped cgroup — two different sources, which is
exactly the inconsistency being reported.
The path is now translated through the mount's root from /proc/self/mountinfo:
if /sys/fs/cgroup exposes only a subtree, the part of the reported path that
the mount hides is dropped. That resolves both the container's own cgroup and
any unit nested inside it, so the two cards land on one accounting domain
instead of "working by accident" on hosts where the mount happens to be the
root.
readServiceCgroupMemory() also gained the root fallback it was missing, so a
layout that cannot be resolved at all still reads the domain the process is
accounted in rather than falling back to RSS. Adopting the root is gated on a
finite cap being enforced there: on a bare-metal host the root is unbounded and
attributing the machine's whole memory to two processes would be a
fabrication, so those hosts keep the previous RSS behaviour.
Both readers now share cgroupMemoryFiles(), so the v2/v1 field probing exists
once instead of being copied per caller — the duplication is how the missing
fallback went unnoticed. Memory path constants are dropped with it, and the
v1 PAGE_COUNTER_MAX sentinel is named rather than open-coded.
Tests cover the layouts directly, with /proc and /sys pointed at fixtures
through named path variables: plain hierarchy root, bind-mounted container
cgroup, a unit nested below it, cgroup v1, the bounded and unbounded fallback,
sentinel rejection, and the invariant that both cards resolve to the same
usage. A malformed mountinfo degrades to "unresolved", never to a wrong path.
* fix(metrics): read CPU accounting from the resolved cgroup
cpu.stat, cpu.max and cpuset.cpus.effective were read from hard-coded
hierarchy-root paths, so they had the same flaw the memory files had: in a
container the accounting for this process is not at the root. On LXC the root
is a tmpfs holding nothing readable, so the CPU card silently fell back to
host-wide /proc/stat and reported the machine instead of the container — the
container could be fully throttled while the bar sat near idle.
They now go through cpuAccountingDir(), which picks the process' own cgroup
when the kernel enforces a cap there (a memory limit or a CPU quota) and
otherwise keeps the hierarchy root. That preserves bare-metal semantics, where
the card is meant to describe the machine rather than the panel's own unit,
while a container — the case where the root is simply the wrong place to look —
is read from the cgroup that actually confines it.
Usage is taken from the first directory that has it, never mixed across two:
a delta between two different cgroups would be meaningless. Caps are probed
across the directories instead, since a quota on a broader domain still applies
to us, and cpuCapacity() already narrows to the tightest limit found.
The three fixed CPU path constants go away with the hard-coded reads; every
accounting file is now read relative to a resolved directory.
* test(system): stop comparing memory samples taken at different times
Two tests guarded the invariant that the system and process cards read the same
memory pool, and both failed intermittently — reproducibly under -race, and
occasionally in a plain run, which the backend job executes:
system memory used (2874142720) is smaller than this process' RSS (2875170816)
panel memory 2874384384 exceeds system used 2874142720
They compared a possibly 500ms-stale cached system reading against a live
process reading of the test binary itself. Where the panel is most of its own
accounting domain — a container, or a sandbox running the test alone — those two
numbers are nearly identical, so the comparison has no margin of its own and any
allocation in between flips it.
The process is now sampled first, so the reference reading is the newer one, and
a tolerance absorbs the remaining skew. What these tests guard is a mismatch of
sources, which was out by multiples (host /proc/meminfo against the cgroup), so
a percent-wide tolerance keeps the assertion meaningful.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
fix(metrics): resolve the real cgroup on LXC for memory and CPU (#85)
* fix(metrics): resolve the real cgroup on LXC, not just the hierarchy root
On LXC the memory cards still disagreed. Path resolution assumed
/sys/fs/cgroup is the whole hierarchy and simply appended the path from
/proc/self/cgroup to it. LXC's default (cgroup:mixed) mounts a tmpfs there and
bind-mounts only the container's own cgroup into a directory below it, so that
path names directories relative to the host hierarchy and does not exist from
inside the container. Resolution then failed, readServiceCgroupMemory() had no
fallback, and the per-process card silently degraded to raw RSS while the
system card read a container-scoped cgroup — two different sources, which is
exactly the inconsistency being reported.
The path is now translated through the mount's root from /proc/self/mountinfo:
if /sys/fs/cgroup exposes only a subtree, the part of the reported path that
the mount hides is dropped. That resolves both the container's own cgroup and
any unit nested inside it, so the two cards land on one accounting domain
instead of "working by accident" on hosts where the mount happens to be the
root.
readServiceCgroupMemory() also gained the root fallback it was missing, so a
layout that cannot be resolved at all still reads the domain the process is
accounted in rather than falling back to RSS. Adopting the root is gated on a
finite cap being enforced there: on a bare-metal host the root is unbounded and
attributing the machine's whole memory to two processes would be a
fabrication, so those hosts keep the previous RSS behaviour.
Both readers now share cgroupMemoryFiles(), so the v2/v1 field probing exists
once instead of being copied per caller — the duplication is how the missing
fallback went unnoticed. Memory path constants are dropped with it, and the
v1 PAGE_COUNTER_MAX sentinel is named rather than open-coded.
Tests cover the layouts directly, with /proc and /sys pointed at fixtures
through named path variables: plain hierarchy root, bind-mounted container
cgroup, a unit nested below it, cgroup v1, the bounded and unbounded fallback,
sentinel rejection, and the invariant that both cards resolve to the same
usage. A malformed mountinfo degrades to "unresolved", never to a wrong path.
* fix(metrics): read CPU accounting from the resolved cgroup
cpu.stat, cpu.max and cpuset.cpus.effective were read from hard-coded
hierarchy-root paths, so they had the same flaw the memory files had: in a
container the accounting for this process is not at the root. On LXC the root
is a tmpfs holding nothing readable, so the CPU card silently fell back to
host-wide /proc/stat and reported the machine instead of the container — the
container could be fully throttled while the bar sat near idle.
They now go through cpuAccountingDir(), which picks the process' own cgroup
when the kernel enforces a cap there (a memory limit or a CPU quota) and
otherwise keeps the hierarchy root. That preserves bare-metal semantics, where
the card is meant to describe the machine rather than the panel's own unit,
while a container — the case where the root is simply the wrong place to look —
is read from the cgroup that actually confines it.
Usage is taken from the first directory that has it, never mixed across two:
a delta between two different cgroups would be meaningless. Caps are probed
across the directories instead, since a quota on a broader domain still applies
to us, and cpuCapacity() already narrows to the tightest limit found.
The three fixed CPU path constants go away with the hard-coded reads; every
accounting file is now read relative to a resolved directory.
* test(system): stop comparing memory samples taken at different times
Two tests guarded the invariant that the system and process cards read the same
memory pool, and both failed intermittently — reproducibly under -race, and
occasionally in a plain run, which the backend job executes:
system memory used (2874142720) is smaller than this process' RSS (2875170816)
panel memory 2874384384 exceeds system used 2874142720
They compared a possibly 500ms-stale cached system reading against a live
process reading of the test binary itself. Where the panel is most of its own
accounting domain — a container, or a sandbox running the test alone — those two
numbers are nearly identical, so the comparison has no margin of its own and any
allocation in between flips it.
The process is now sampled first, so the reference reading is the newer one, and
a tolerance absorbs the remaining skew. What these tests guard is a mismatch of
sources, which was out by multiples (host /proc/meminfo against the cgroup), so
a percent-wide tolerance keeps the assertion meaningful.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
fix(metrics): resolve the real cgroup on LXC for memory and CPU (#85)
* fix(metrics): resolve the real cgroup on LXC, not just the hierarchy root
On LXC the memory cards still disagreed. Path resolution assumed
/sys/fs/cgroup is the whole hierarchy and simply appended the path from
/proc/self/cgroup to it. LXC's default (cgroup:mixed) mounts a tmpfs there and
bind-mounts only the container's own cgroup into a directory below it, so that
path names directories relative to the host hierarchy and does not exist from
inside the container. Resolution then failed, readServiceCgroupMemory() had no
fallback, and the per-process card silently degraded to raw RSS while the
system card read a container-scoped cgroup — two different sources, which is
exactly the inconsistency being reported.
The path is now translated through the mount's root from /proc/self/mountinfo:
if /sys/fs/cgroup exposes only a subtree, the part of the reported path that
the mount hides is dropped. That resolves both the container's own cgroup and
any unit nested inside it, so the two cards land on one accounting domain
instead of "working by accident" on hosts where the mount happens to be the
root.
readServiceCgroupMemory() also gained the root fallback it was missing, so a
layout that cannot be resolved at all still reads the domain the process is
accounted in rather than falling back to RSS. Adopting the root is gated on a
finite cap being enforced there: on a bare-metal host the root is unbounded and
attributing the machine's whole memory to two processes would be a
fabrication, so those hosts keep the previous RSS behaviour.
Both readers now share cgroupMemoryFiles(), so the v2/v1 field probing exists
once instead of being copied per caller — the duplication is how the missing
fallback went unnoticed. Memory path constants are dropped with it, and the
v1 PAGE_COUNTER_MAX sentinel is named rather than open-coded.
Tests cover the layouts directly, with /proc and /sys pointed at fixtures
through named path variables: plain hierarchy root, bind-mounted container
cgroup, a unit nested below it, cgroup v1, the bounded and unbounded fallback,
sentinel rejection, and the invariant that both cards resolve to the same
usage. A malformed mountinfo degrades to "unresolved", never to a wrong path.
* fix(metrics): read CPU accounting from the resolved cgroup
cpu.stat, cpu.max and cpuset.cpus.effective were read from hard-coded
hierarchy-root paths, so they had the same flaw the memory files had: in a
container the accounting for this process is not at the root. On LXC the root
is a tmpfs holding nothing readable, so the CPU card silently fell back to
host-wide /proc/stat and reported the machine instead of the container — the
container could be fully throttled while the bar sat near idle.
They now go through cpuAccountingDir(), which picks the process' own cgroup
when the kernel enforces a cap there (a memory limit or a CPU quota) and
otherwise keeps the hierarchy root. That preserves bare-metal semantics, where
the card is meant to describe the machine rather than the panel's own unit,
while a container — the case where the root is simply the wrong place to look —
is read from the cgroup that actually confines it.
Usage is taken from the first directory that has it, never mixed across two:
a delta between two different cgroups would be meaningless. Caps are probed
across the directories instead, since a quota on a broader domain still applies
to us, and cpuCapacity() already narrows to the tightest limit found.
The three fixed CPU path constants go away with the hard-coded reads; every
accounting file is now read relative to a resolved directory.
* test(system): stop comparing memory samples taken at different times
Two tests guarded the invariant that the system and process cards read the same
memory pool, and both failed intermittently — reproducibly under -race, and
occasionally in a plain run, which the backend job executes:
system memory used (2874142720) is smaller than this process' RSS (2875170816)
panel memory 2874384384 exceeds system used 2874142720
They compared a possibly 500ms-stale cached system reading against a live
process reading of the test binary itself. Where the panel is most of its own
accounting domain — a container, or a sandbox running the test alone — those two
numbers are nearly identical, so the comparison has no margin of its own and any
allocation in between flips it.
The process is now sampled first, so the reference reading is the newer one, and
a tolerance absorbs the remaining skew. What these tests guard is a mismatch of
sources, which was out by multiples (host /proc/meminfo against the cgroup), so
a percent-wide tolerance keeps the assertion meaningful.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
fix(metrics): resolve the real cgroup on LXC for memory and CPU (#85)
* fix(metrics): resolve the real cgroup on LXC, not just the hierarchy root
On LXC the memory cards still disagreed. Path resolution assumed
/sys/fs/cgroup is the whole hierarchy and simply appended the path from
/proc/self/cgroup to it. LXC's default (cgroup:mixed) mounts a tmpfs there and
bind-mounts only the container's own cgroup into a directory below it, so that
path names directories relative to the host hierarchy and does not exist from
inside the container. Resolution then failed, readServiceCgroupMemory() had no
fallback, and the per-process card silently degraded to raw RSS while the
system card read a container-scoped cgroup — two different sources, which is
exactly the inconsistency being reported.
The path is now translated through the mount's root from /proc/self/mountinfo:
if /sys/fs/cgroup exposes only a subtree, the part of the reported path that
the mount hides is dropped. That resolves both the container's own cgroup and
any unit nested inside it, so the two cards land on one accounting domain
instead of "working by accident" on hosts where the mount happens to be the
root.
readServiceCgroupMemory() also gained the root fallback it was missing, so a
layout that cannot be resolved at all still reads the domain the process is
accounted in rather than falling back to RSS. Adopting the root is gated on a
finite cap being enforced there: on a bare-metal host the root is unbounded and
attributing the machine's whole memory to two processes would be a
fabrication, so those hosts keep the previous RSS behaviour.
Both readers now share cgroupMemoryFiles(), so the v2/v1 field probing exists
once instead of being copied per caller — the duplication is how the missing
fallback went unnoticed. Memory path constants are dropped with it, and the
v1 PAGE_COUNTER_MAX sentinel is named rather than open-coded.
Tests cover the layouts directly, with /proc and /sys pointed at fixtures
through named path variables: plain hierarchy root, bind-mounted container
cgroup, a unit nested below it, cgroup v1, the bounded and unbounded fallback,
sentinel rejection, and the invariant that both cards resolve to the same
usage. A malformed mountinfo degrades to "unresolved", never to a wrong path.
* fix(metrics): read CPU accounting from the resolved cgroup
cpu.stat, cpu.max and cpuset.cpus.effective were read from hard-coded
hierarchy-root paths, so they had the same flaw the memory files had: in a
container the accounting for this process is not at the root. On LXC the root
is a tmpfs holding nothing readable, so the CPU card silently fell back to
host-wide /proc/stat and reported the machine instead of the container — the
container could be fully throttled while the bar sat near idle.
They now go through cpuAccountingDir(), which picks the process' own cgroup
when the kernel enforces a cap there (a memory limit or a CPU quota) and
otherwise keeps the hierarchy root. That preserves bare-metal semantics, where
the card is meant to describe the machine rather than the panel's own unit,
while a container — the case where the root is simply the wrong place to look —
is read from the cgroup that actually confines it.
Usage is taken from the first directory that has it, never mixed across two:
a delta between two different cgroups would be meaningless. Caps are probed
across the directories instead, since a quota on a broader domain still applies
to us, and cpuCapacity() already narrows to the tightest limit found.
The three fixed CPU path constants go away with the hard-coded reads; every
accounting file is now read relative to a resolved directory.
* test(system): stop comparing memory samples taken at different times
Two tests guarded the invariant that the system and process cards read the same
memory pool, and both failed intermittently — reproducibly under -race, and
occasionally in a plain run, which the backend job executes:
system memory used (2874142720) is smaller than this process' RSS (2875170816)
panel memory 2874384384 exceeds system used 2874142720
They compared a possibly 500ms-stale cached system reading against a live
process reading of the test binary itself. Where the panel is most of its own
accounting domain — a container, or a sandbox running the test alone — those two
numbers are nearly identical, so the comparison has no margin of its own and any
allocation in between flips it.
The process is now sampled first, so the reference reading is the newer one, and
a tolerance absorbs the remaining skew. What these tests guard is a mismatch of
sources, which was out by multiples (host /proc/meminfo against the cgroup), so
a percent-wide tolerance keeps the assertion meaningful.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
fix(metrics): resolve the real cgroup on LXC for memory and CPU (#85)
* fix(metrics): resolve the real cgroup on LXC, not just the hierarchy root
On LXC the memory cards still disagreed. Path resolution assumed
/sys/fs/cgroup is the whole hierarchy and simply appended the path from
/proc/self/cgroup to it. LXC's default (cgroup:mixed) mounts a tmpfs there and
bind-mounts only the container's own cgroup into a directory below it, so that
path names directories relative to the host hierarchy and does not exist from
inside the container. Resolution then failed, readServiceCgroupMemory() had no
fallback, and the per-process card silently degraded to raw RSS while the
system card read a container-scoped cgroup — two different sources, which is
exactly the inconsistency being reported.
The path is now translated through the mount's root from /proc/self/mountinfo:
if /sys/fs/cgroup exposes only a subtree, the part of the reported path that
the mount hides is dropped. That resolves both the container's own cgroup and
any unit nested inside it, so the two cards land on one accounting domain
instead of "working by accident" on hosts where the mount happens to be the
root.
readServiceCgroupMemory() also gained the root fallback it was missing, so a
layout that cannot be resolved at all still reads the domain the process is
accounted in rather than falling back to RSS. Adopting the root is gated on a
finite cap being enforced there: on a bare-metal host the root is unbounded and
attributing the machine's whole memory to two processes would be a
fabrication, so those hosts keep the previous RSS behaviour.
Both readers now share cgroupMemoryFiles(), so the v2/v1 field probing exists
once instead of being copied per caller — the duplication is how the missing
fallback went unnoticed. Memory path constants are dropped with it, and the
v1 PAGE_COUNTER_MAX sentinel is named rather than open-coded.
Tests cover the layouts directly, with /proc and /sys pointed at fixtures
through named path variables: plain hierarchy root, bind-mounted container
cgroup, a unit nested below it, cgroup v1, the bounded and unbounded fallback,
sentinel rejection, and the invariant that both cards resolve to the same
usage. A malformed mountinfo degrades to "unresolved", never to a wrong path.
* fix(metrics): read CPU accounting from the resolved cgroup
cpu.stat, cpu.max and cpuset.cpus.effective were read from hard-coded
hierarchy-root paths, so they had the same flaw the memory files had: in a
container the accounting for this process is not at the root. On LXC the root
is a tmpfs holding nothing readable, so the CPU card silently fell back to
host-wide /proc/stat and reported the machine instead of the container — the
container could be fully throttled while the bar sat near idle.
They now go through cpuAccountingDir(), which picks the process' own cgroup
when the kernel enforces a cap there (a memory limit or a CPU quota) and
otherwise keeps the hierarchy root. That preserves bare-metal semantics, where
the card is meant to describe the machine rather than the panel's own unit,
while a container — the case where the root is simply the wrong place to look —
is read from the cgroup that actually confines it.
Usage is taken from the first directory that has it, never mixed across two:
a delta between two different cgroups would be meaningless. Caps are probed
across the directories instead, since a quota on a broader domain still applies
to us, and cpuCapacity() already narrows to the tightest limit found.
The three fixed CPU path constants go away with the hard-coded reads; every
accounting file is now read relative to a resolved directory.
* test(system): stop comparing memory samples taken at different times
Two tests guarded the invariant that the system and process cards read the same
memory pool, and both failed intermittently — reproducibly under -race, and
occasionally in a plain run, which the backend job executes:
system memory used (2874142720) is smaller than this process' RSS (2875170816)
panel memory 2874384384 exceeds system used 2874142720
They compared a possibly 500ms-stale cached system reading against a live
process reading of the test binary itself. Where the panel is most of its own
accounting domain — a container, or a sandbox running the test alone — those two
numbers are nearly identical, so the comparison has no margin of its own and any
allocation in between flips it.
The process is now sampled first, so the reference reading is the newer one, and
a tolerance absorbs the remaining skew. What these tests guard is a mismatch of
sources, which was out by multiples (host /proc/meminfo against the cgroup), so
a percent-wide tolerance keeps the assertion meaningful.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
fix(metrics): resolve the real cgroup on LXC for memory and CPU (#85)
* fix(metrics): resolve the real cgroup on LXC, not just the hierarchy root
On LXC the memory cards still disagreed. Path resolution assumed
/sys/fs/cgroup is the whole hierarchy and simply appended the path from
/proc/self/cgroup to it. LXC's default (cgroup:mixed) mounts a tmpfs there and
bind-mounts only the container's own cgroup into a directory below it, so that
path names directories relative to the host hierarchy and does not exist from
inside the container. Resolution then failed, readServiceCgroupMemory() had no
fallback, and the per-process card silently degraded to raw RSS while the
system card read a container-scoped cgroup — two different sources, which is
exactly the inconsistency being reported.
The path is now translated through the mount's root from /proc/self/mountinfo:
if /sys/fs/cgroup exposes only a subtree, the part of the reported path that
the mount hides is dropped. That resolves both the container's own cgroup and
any unit nested inside it, so the two cards land on one accounting domain
instead of "working by accident" on hosts where the mount happens to be the
root.
readServiceCgroupMemory() also gained the root fallback it was missing, so a
layout that cannot be resolved at all still reads the domain the process is
accounted in rather than falling back to RSS. Adopting the root is gated on a
finite cap being enforced there: on a bare-metal host the root is unbounded and
attributing the machine's whole memory to two processes would be a
fabrication, so those hosts keep the previous RSS behaviour.
Both readers now share cgroupMemoryFiles(), so the v2/v1 field probing exists
once instead of being copied per caller — the duplication is how the missing
fallback went unnoticed. Memory path constants are dropped with it, and the
v1 PAGE_COUNTER_MAX sentinel is named rather than open-coded.
Tests cover the layouts directly, with /proc and /sys pointed at fixtures
through named path variables: plain hierarchy root, bind-mounted container
cgroup, a unit nested below it, cgroup v1, the bounded and unbounded fallback,
sentinel rejection, and the invariant that both cards resolve to the same
usage. A malformed mountinfo degrades to "unresolved", never to a wrong path.
* fix(metrics): read CPU accounting from the resolved cgroup
cpu.stat, cpu.max and cpuset.cpus.effective were read from hard-coded
hierarchy-root paths, so they had the same flaw the memory files had: in a
container the accounting for this process is not at the root. On LXC the root
is a tmpfs holding nothing readable, so the CPU card silently fell back to
host-wide /proc/stat and reported the machine instead of the container — the
container could be fully throttled while the bar sat near idle.
They now go through cpuAccountingDir(), which picks the process' own cgroup
when the kernel enforces a cap there (a memory limit or a CPU quota) and
otherwise keeps the hierarchy root. That preserves bare-metal semantics, where
the card is meant to describe the machine rather than the panel's own unit,
while a container — the case where the root is simply the wrong place to look —
is read from the cgroup that actually confines it.
Usage is taken from the first directory that has it, never mixed across two:
a delta between two different cgroups would be meaningless. Caps are probed
across the directories instead, since a quota on a broader domain still applies
to us, and cpuCapacity() already narrows to the tightest limit found.
The three fixed CPU path constants go away with the hard-coded reads; every
accounting file is now read relative to a resolved directory.
* test(system): stop comparing memory samples taken at different times
Two tests guarded the invariant that the system and process cards read the same
memory pool, and both failed intermittently — reproducibly under -race, and
occasionally in a plain run, which the backend job executes:
system memory used (2874142720) is smaller than this process' RSS (2875170816)
panel memory 2874384384 exceeds system used 2874142720
They compared a possibly 500ms-stale cached system reading against a live
process reading of the test binary itself. Where the panel is most of its own
accounting domain — a container, or a sandbox running the test alone — those two
numbers are nearly identical, so the comparison has no margin of its own and any
allocation in between flips it.
The process is now sampled first, so the reference reading is the newer one, and
a tolerance absorbs the remaining skew. What these tests guard is a mismatch of
sources, which was out by multiples (host /proc/meminfo against the cgroup), so
a percent-wide tolerance keeps the assertion meaningful.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
2026-09-27 15:58:47 +08:00
風雪and風雪 7129cc346d perf(memory): keep install, update and runtime inside a small-RAM budget (#83)
* perf(memory): keep install, update and runtime inside a small-RAM budget
Small NAT/VPS targets have 128-512MB of RAM and no swap, so anything that
cannot be reclaimed is fatal. This tuning touches no swap and no mmap; it
works by not allocating in the first place.
Installer (covers install and update; update.sh only execs install.sh):
- Put the scratch directory under DATA_DIR instead of mktemp -d. /tmp is
tmpfs on many of these hosts, and without swap every byte there is pinned
in RAM: bundle, extracted release and a copy of the data directory were
held at once, exceeding total RAM of the machines being targeted.
- Hash assets on the way to disk instead of re-reading them. Download is a
single pass now; verification still completes before tar sees the bytes.
- Release the intermediate data.tar right after extraction rather than
holding the data directory twice for the rest of the run.
- Check scratch capacity before downloading, so we fail before the old
service is stopped rather than halfway through.
- Refresh Environment=GOMEMLIMIT/GOGC/THREE_M_UI_CORE_LOW_MEMORY and
MemoryMax on an existing unit. write_service() keeps an operator's unit,
which meant the values baked in at install time never tracked updates.
Panel:
- Bound the Go heap from the process itself via debug.SetMemoryLimit, so the
tuning reaches manual runs, containers and the installer's own init and
healthcheck instead of only a systemd unit. It acts in-process, so the
Mihomo core never inherits the panel's tighter GC target. An explicit
GOMEMLIMIT or GOGC always wins.
- Emit geodata-loader: memconservative for the core when low-memory mode is
on. The key is omitempty and pinned after user fragments merge, so nothing
changes for anyone who does not opt in, and no fragment can undo it.
No new calls to Mmap/Munmap/Madvise, no swap or mmap anywhere in the tree.
* fix(ci): keep the new scratch directory out of the installer's own paths
Two problems CI caught on the small-memory change, both from moving the
scratch directory from mktemp into DATA_DIR.
restore_files() rebuilds the data directory by deleting everything in it
except backups. The scratch directory now lives inside it, so a rollback
deleted the very directory it was reading its own rollback out of, and the
restore died looking for restore/ENABLED. Rollback therefore had to be
skipped, exempting the scratch directory from that purge; the cleanup trap
already removes it.
The test curl also modelled only one of curl's output modes. Writing to
stdout and answering -I are both normal curl behaviour, and both are now
covered, so the mock no longer pins the installer to a single invocation
shape. The distribution fake likewise emits its tampered asset on stdout
instead of through --output, keeping that test about checksum rejection
rather than about which flag was used.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
perf(memory): keep install, update and runtime inside a small-RAM budget (#83)
* perf(memory): keep install, update and runtime inside a small-RAM budget
Small NAT/VPS targets have 128-512MB of RAM and no swap, so anything that
cannot be reclaimed is fatal. This tuning touches no swap and no mmap; it
works by not allocating in the first place.
Installer (covers install and update; update.sh only execs install.sh):
- Put the scratch directory under DATA_DIR instead of mktemp -d. /tmp is
tmpfs on many of these hosts, and without swap every byte there is pinned
in RAM: bundle, extracted release and a copy of the data directory were
held at once, exceeding total RAM of the machines being targeted.
- Hash assets on the way to disk instead of re-reading them. Download is a
single pass now; verification still completes before tar sees the bytes.
- Release the intermediate data.tar right after extraction rather than
holding the data directory twice for the rest of the run.
- Check scratch capacity before downloading, so we fail before the old
service is stopped rather than halfway through.
- Refresh Environment=GOMEMLIMIT/GOGC/THREE_M_UI_CORE_LOW_MEMORY and
MemoryMax on an existing unit. write_service() keeps an operator's unit,
which meant the values baked in at install time never tracked updates.
Panel:
- Bound the Go heap from the process itself via debug.SetMemoryLimit, so the
tuning reaches manual runs, containers and the installer's own init and
healthcheck instead of only a systemd unit. It acts in-process, so the
Mihomo core never inherits the panel's tighter GC target. An explicit
GOMEMLIMIT or GOGC always wins.
- Emit geodata-loader: memconservative for the core when low-memory mode is
on. The key is omitempty and pinned after user fragments merge, so nothing
changes for anyone who does not opt in, and no fragment can undo it.
No new calls to Mmap/Munmap/Madvise, no swap or mmap anywhere in the tree.
* fix(ci): keep the new scratch directory out of the installer's own paths
Two problems CI caught on the small-memory change, both from moving the
scratch directory from mktemp into DATA_DIR.
restore_files() rebuilds the data directory by deleting everything in it
except backups. The scratch directory now lives inside it, so a rollback
deleted the very directory it was reading its own rollback out of, and the
restore died looking for restore/ENABLED. Rollback therefore had to be
skipped, exempting the scratch directory from that purge; the cleanup trap
already removes it.
The test curl also modelled only one of curl's output modes. Writing to
stdout and answering -I are both normal curl behaviour, and both are now
covered, so the mock no longer pins the installer to a single invocation
shape. The distribution fake likewise emits its tampered asset on stdout
instead of through --output, keeping that test about checksum rejection
rather than about which flag was used.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
perf(memory): keep install, update and runtime inside a small-RAM budget (#83)
* perf(memory): keep install, update and runtime inside a small-RAM budget
Small NAT/VPS targets have 128-512MB of RAM and no swap, so anything that
cannot be reclaimed is fatal. This tuning touches no swap and no mmap; it
works by not allocating in the first place.
Installer (covers install and update; update.sh only execs install.sh):
- Put the scratch directory under DATA_DIR instead of mktemp -d. /tmp is
tmpfs on many of these hosts, and without swap every byte there is pinned
in RAM: bundle, extracted release and a copy of the data directory were
held at once, exceeding total RAM of the machines being targeted.
- Hash assets on the way to disk instead of re-reading them. Download is a
single pass now; verification still completes before tar sees the bytes.
- Release the intermediate data.tar right after extraction rather than
holding the data directory twice for the rest of the run.
- Check scratch capacity before downloading, so we fail before the old
service is stopped rather than halfway through.
- Refresh Environment=GOMEMLIMIT/GOGC/THREE_M_UI_CORE_LOW_MEMORY and
MemoryMax on an existing unit. write_service() keeps an operator's unit,
which meant the values baked in at install time never tracked updates.
Panel:
- Bound the Go heap from the process itself via debug.SetMemoryLimit, so the
tuning reaches manual runs, containers and the installer's own init and
healthcheck instead of only a systemd unit. It acts in-process, so the
Mihomo core never inherits the panel's tighter GC target. An explicit
GOMEMLIMIT or GOGC always wins.
- Emit geodata-loader: memconservative for the core when low-memory mode is
on. The key is omitempty and pinned after user fragments merge, so nothing
changes for anyone who does not opt in, and no fragment can undo it.
No new calls to Mmap/Munmap/Madvise, no swap or mmap anywhere in the tree.
* fix(ci): keep the new scratch directory out of the installer's own paths
Two problems CI caught on the small-memory change, both from moving the
scratch directory from mktemp into DATA_DIR.
restore_files() rebuilds the data directory by deleting everything in it
except backups. The scratch directory now lives inside it, so a rollback
deleted the very directory it was reading its own rollback out of, and the
restore died looking for restore/ENABLED. Rollback therefore had to be
skipped, exempting the scratch directory from that purge; the cleanup trap
already removes it.
The test curl also modelled only one of curl's output modes. Writing to
stdout and answering -I are both normal curl behaviour, and both are now
covered, so the mock no longer pins the installer to a single invocation
shape. The distribution fake likewise emits its tampered asset on stdout
instead of through --output, keeping that test about checksum rejection
rather than about which flag was used.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
perf(memory): keep install, update and runtime inside a small-RAM budget (#83)
* perf(memory): keep install, update and runtime inside a small-RAM budget
Small NAT/VPS targets have 128-512MB of RAM and no swap, so anything that
cannot be reclaimed is fatal. This tuning touches no swap and no mmap; it
works by not allocating in the first place.
Installer (covers install and update; update.sh only execs install.sh):
- Put the scratch directory under DATA_DIR instead of mktemp -d. /tmp is
tmpfs on many of these hosts, and without swap every byte there is pinned
in RAM: bundle, extracted release and a copy of the data directory were
held at once, exceeding total RAM of the machines being targeted.
- Hash assets on the way to disk instead of re-reading them. Download is a
single pass now; verification still completes before tar sees the bytes.
- Release the intermediate data.tar right after extraction rather than
holding the data directory twice for the rest of the run.
- Check scratch capacity before downloading, so we fail before the old
service is stopped rather than halfway through.
- Refresh Environment=GOMEMLIMIT/GOGC/THREE_M_UI_CORE_LOW_MEMORY and
MemoryMax on an existing unit. write_service() keeps an operator's unit,
which meant the values baked in at install time never tracked updates.
Panel:
- Bound the Go heap from the process itself via debug.SetMemoryLimit, so the
tuning reaches manual runs, containers and the installer's own init and
healthcheck instead of only a systemd unit. It acts in-process, so the
Mihomo core never inherits the panel's tighter GC target. An explicit
GOMEMLIMIT or GOGC always wins.
- Emit geodata-loader: memconservative for the core when low-memory mode is
on. The key is omitempty and pinned after user fragments merge, so nothing
changes for anyone who does not opt in, and no fragment can undo it.
No new calls to Mmap/Munmap/Madvise, no swap or mmap anywhere in the tree.
* fix(ci): keep the new scratch directory out of the installer's own paths
Two problems CI caught on the small-memory change, both from moving the
scratch directory from mktemp into DATA_DIR.
restore_files() rebuilds the data directory by deleting everything in it
except backups. The scratch directory now lives inside it, so a rollback
deleted the very directory it was reading its own rollback out of, and the
restore died looking for restore/ENABLED. Rollback therefore had to be
skipped, exempting the scratch directory from that purge; the cleanup trap
already removes it.
The test curl also modelled only one of curl's output modes. Writing to
stdout and answering -I are both normal curl behaviour, and both are now
covered, so the mock no longer pins the installer to a single invocation
shape. The distribution fake likewise emits its tampered asset on stdout
instead of through --output, keeping that test about checksum rejection
rather than about which flag was used.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
perf(memory): keep install, update and runtime inside a small-RAM budget (#83)
* perf(memory): keep install, update and runtime inside a small-RAM budget
Small NAT/VPS targets have 128-512MB of RAM and no swap, so anything that
cannot be reclaimed is fatal. This tuning touches no swap and no mmap; it
works by not allocating in the first place.
Installer (covers install and update; update.sh only execs install.sh):
- Put the scratch directory under DATA_DIR instead of mktemp -d. /tmp is
tmpfs on many of these hosts, and without swap every byte there is pinned
in RAM: bundle, extracted release and a copy of the data directory were
held at once, exceeding total RAM of the machines being targeted.
- Hash assets on the way to disk instead of re-reading them. Download is a
single pass now; verification still completes before tar sees the bytes.
- Release the intermediate data.tar right after extraction rather than
holding the data directory twice for the rest of the run.
- Check scratch capacity before downloading, so we fail before the old
service is stopped rather than halfway through.
- Refresh Environment=GOMEMLIMIT/GOGC/THREE_M_UI_CORE_LOW_MEMORY and
MemoryMax on an existing unit. write_service() keeps an operator's unit,
which meant the values baked in at install time never tracked updates.
Panel:
- Bound the Go heap from the process itself via debug.SetMemoryLimit, so the
tuning reaches manual runs, containers and the installer's own init and
healthcheck instead of only a systemd unit. It acts in-process, so the
Mihomo core never inherits the panel's tighter GC target. An explicit
GOMEMLIMIT or GOGC always wins.
- Emit geodata-loader: memconservative for the core when low-memory mode is
on. The key is omitempty and pinned after user fragments merge, so nothing
changes for anyone who does not opt in, and no fragment can undo it.
No new calls to Mmap/Munmap/Madvise, no swap or mmap anywhere in the tree.
* fix(ci): keep the new scratch directory out of the installer's own paths
Two problems CI caught on the small-memory change, both from moving the
scratch directory from mktemp into DATA_DIR.
restore_files() rebuilds the data directory by deleting everything in it
except backups. The scratch directory now lives inside it, so a rollback
deleted the very directory it was reading its own rollback out of, and the
restore died looking for restore/ENABLED. Rollback therefore had to be
skipped, exempting the scratch directory from that purge; the cleanup trap
already removes it.
The test curl also modelled only one of curl's output modes. Writing to
stdout and answering -I are both normal curl behaviour, and both are now
covered, so the mock no longer pins the installer to a single invocation
shape. The distribution fake likewise emits its tampered asset on stdout
instead of through --output, keeping that test about checksum rejection
rather than about which flag was used.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
perf(memory): keep install, update and runtime inside a small-RAM budget (#83)
* perf(memory): keep install, update and runtime inside a small-RAM budget
Small NAT/VPS targets have 128-512MB of RAM and no swap, so anything that
cannot be reclaimed is fatal. This tuning touches no swap and no mmap; it
works by not allocating in the first place.
Installer (covers install and update; update.sh only execs install.sh):
- Put the scratch directory under DATA_DIR instead of mktemp -d. /tmp is
tmpfs on many of these hosts, and without swap every byte there is pinned
in RAM: bundle, extracted release and a copy of the data directory were
held at once, exceeding total RAM of the machines being targeted.
- Hash assets on the way to disk instead of re-reading them. Download is a
single pass now; verification still completes before tar sees the bytes.
- Release the intermediate data.tar right after extraction rather than
holding the data directory twice for the rest of the run.
- Check scratch capacity before downloading, so we fail before the old
service is stopped rather than halfway through.
- Refresh Environment=GOMEMLIMIT/GOGC/THREE_M_UI_CORE_LOW_MEMORY and
MemoryMax on an existing unit. write_service() keeps an operator's unit,
which meant the values baked in at install time never tracked updates.
Panel:
- Bound the Go heap from the process itself via debug.SetMemoryLimit, so the
tuning reaches manual runs, containers and the installer's own init and
healthcheck instead of only a systemd unit. It acts in-process, so the
Mihomo core never inherits the panel's tighter GC target. An explicit
GOMEMLIMIT or GOGC always wins.
- Emit geodata-loader: memconservative for the core when low-memory mode is
on. The key is omitempty and pinned after user fragments merge, so nothing
changes for anyone who does not opt in, and no fragment can undo it.
No new calls to Mmap/Munmap/Madvise, no swap or mmap anywhere in the tree.
* fix(ci): keep the new scratch directory out of the installer's own paths
Two problems CI caught on the small-memory change, both from moving the
scratch directory from mktemp into DATA_DIR.
restore_files() rebuilds the data directory by deleting everything in it
except backups. The scratch directory now lives inside it, so a rollback
deleted the very directory it was reading its own rollback out of, and the
restore died looking for restore/ENABLED. Rollback therefore had to be
skipped, exempting the scratch directory from that purge; the cleanup trap
already removes it.
The test curl also modelled only one of curl's output modes. Writing to
stdout and answering -I are both normal curl behaviour, and both are now
covered, so the mock no longer pins the installer to a single invocation
shape. The distribution fake likewise emits its tampered asset on stdout
instead of through --output, keeping that test about checksum rejection
rather than about which flag was used.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
perf(memory): keep install, update and runtime inside a small-RAM budget (#83)
* perf(memory): keep install, update and runtime inside a small-RAM budget
Small NAT/VPS targets have 128-512MB of RAM and no swap, so anything that
cannot be reclaimed is fatal. This tuning touches no swap and no mmap; it
works by not allocating in the first place.
Installer (covers install and update; update.sh only execs install.sh):
- Put the scratch directory under DATA_DIR instead of mktemp -d. /tmp is
tmpfs on many of these hosts, and without swap every byte there is pinned
in RAM: bundle, extracted release and a copy of the data directory were
held at once, exceeding total RAM of the machines being targeted.
- Hash assets on the way to disk instead of re-reading them. Download is a
single pass now; verification still completes before tar sees the bytes.
- Release the intermediate data.tar right after extraction rather than
holding the data directory twice for the rest of the run.
- Check scratch capacity before downloading, so we fail before the old
service is stopped rather than halfway through.
- Refresh Environment=GOMEMLIMIT/GOGC/THREE_M_UI_CORE_LOW_MEMORY and
MemoryMax on an existing unit. write_service() keeps an operator's unit,
which meant the values baked in at install time never tracked updates.
Panel:
- Bound the Go heap from the process itself via debug.SetMemoryLimit, so the
tuning reaches manual runs, containers and the installer's own init and
healthcheck instead of only a systemd unit. It acts in-process, so the
Mihomo core never inherits the panel's tighter GC target. An explicit
GOMEMLIMIT or GOGC always wins.
- Emit geodata-loader: memconservative for the core when low-memory mode is
on. The key is omitempty and pinned after user fragments merge, so nothing
changes for anyone who does not opt in, and no fragment can undo it.
No new calls to Mmap/Munmap/Madvise, no swap or mmap anywhere in the tree.
* fix(ci): keep the new scratch directory out of the installer's own paths
Two problems CI caught on the small-memory change, both from moving the
scratch directory from mktemp into DATA_DIR.
restore_files() rebuilds the data directory by deleting everything in it
except backups. The scratch directory now lives inside it, so a rollback
deleted the very directory it was reading its own rollback out of, and the
restore died looking for restore/ENABLED. Rollback therefore had to be
skipped, exempting the scratch directory from that purge; the cleanup trap
already removes it.
The test curl also modelled only one of curl's output modes. Writing to
stdout and answering -I are both normal curl behaviour, and both are now
covered, so the mock no longer pins the installer to a single invocation
shape. The distribution fake likewise emits its tampered asset on stdout
instead of through --output, keeping that test about checksum rejection
rather than about which flag was used.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
perf(memory): keep install, update and runtime inside a small-RAM budget (#83)
* perf(memory): keep install, update and runtime inside a small-RAM budget
Small NAT/VPS targets have 128-512MB of RAM and no swap, so anything that
cannot be reclaimed is fatal. This tuning touches no swap and no mmap; it
works by not allocating in the first place.
Installer (covers install and update; update.sh only execs install.sh):
- Put the scratch directory under DATA_DIR instead of mktemp -d. /tmp is
tmpfs on many of these hosts, and without swap every byte there is pinned
in RAM: bundle, extracted release and a copy of the data directory were
held at once, exceeding total RAM of the machines being targeted.
- Hash assets on the way to disk instead of re-reading them. Download is a
single pass now; verification still completes before tar sees the bytes.
- Release the intermediate data.tar right after extraction rather than
holding the data directory twice for the rest of the run.
- Check scratch capacity before downloading, so we fail before the old
service is stopped rather than halfway through.
- Refresh Environment=GOMEMLIMIT/GOGC/THREE_M_UI_CORE_LOW_MEMORY and
MemoryMax on an existing unit. write_service() keeps an operator's unit,
which meant the values baked in at install time never tracked updates.
Panel:
- Bound the Go heap from the process itself via debug.SetMemoryLimit, so the
tuning reaches manual runs, containers and the installer's own init and
healthcheck instead of only a systemd unit. It acts in-process, so the
Mihomo core never inherits the panel's tighter GC target. An explicit
GOMEMLIMIT or GOGC always wins.
- Emit geodata-loader: memconservative for the core when low-memory mode is
on. The key is omitempty and pinned after user fragments merge, so nothing
changes for anyone who does not opt in, and no fragment can undo it.
No new calls to Mmap/Munmap/Madvise, no swap or mmap anywhere in the tree.
* fix(ci): keep the new scratch directory out of the installer's own paths
Two problems CI caught on the small-memory change, both from moving the
scratch directory from mktemp into DATA_DIR.
restore_files() rebuilds the data directory by deleting everything in it
except backups. The scratch directory now lives inside it, so a rollback
deleted the very directory it was reading its own rollback out of, and the
restore died looking for restore/ENABLED. Rollback therefore had to be
skipped, exempting the scratch directory from that purge; the cleanup trap
already removes it.
The test curl also modelled only one of curl's output modes. Writing to
stdout and answering -I are both normal curl behaviour, and both are now
covered, so the mock no longer pins the installer to a single invocation
shape. The distribution fake likewise emits its tampered asset on stdout
instead of through --output, keeping that test about checksum rejection
rather than about which flag was used.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
perf(memory): keep install, update and runtime inside a small-RAM budget (#83)
* perf(memory): keep install, update and runtime inside a small-RAM budget
Small NAT/VPS targets have 128-512MB of RAM and no swap, so anything that
cannot be reclaimed is fatal. This tuning touches no swap and no mmap; it
works by not allocating in the first place.
Installer (covers install and update; update.sh only execs install.sh):
- Put the scratch directory under DATA_DIR instead of mktemp -d. /tmp is
tmpfs on many of these hosts, and without swap every byte there is pinned
in RAM: bundle, extracted release and a copy of the data directory were
held at once, exceeding total RAM of the machines being targeted.
- Hash assets on the way to disk instead of re-reading them. Download is a
single pass now; verification still completes before tar sees the bytes.
- Release the intermediate data.tar right after extraction rather than
holding the data directory twice for the rest of the run.
- Check scratch capacity before downloading, so we fail before the old
service is stopped rather than halfway through.
- Refresh Environment=GOMEMLIMIT/GOGC/THREE_M_UI_CORE_LOW_MEMORY and
MemoryMax on an existing unit. write_service() keeps an operator's unit,
which meant the values baked in at install time never tracked updates.
Panel:
- Bound the Go heap from the process itself via debug.SetMemoryLimit, so the
tuning reaches manual runs, containers and the installer's own init and
healthcheck instead of only a systemd unit. It acts in-process, so the
Mihomo core never inherits the panel's tighter GC target. An explicit
GOMEMLIMIT or GOGC always wins.
- Emit geodata-loader: memconservative for the core when low-memory mode is
on. The key is omitempty and pinned after user fragments merge, so nothing
changes for anyone who does not opt in, and no fragment can undo it.
No new calls to Mmap/Munmap/Madvise, no swap or mmap anywhere in the tree.
* fix(ci): keep the new scratch directory out of the installer's own paths
Two problems CI caught on the small-memory change, both from moving the
scratch directory from mktemp into DATA_DIR.
restore_files() rebuilds the data directory by deleting everything in it
except backups. The scratch directory now lives inside it, so a rollback
deleted the very directory it was reading its own rollback out of, and the
restore died looking for restore/ENABLED. Rollback therefore had to be
skipped, exempting the scratch directory from that purge; the cleanup trap
already removes it.
The test curl also modelled only one of curl's output modes. Writing to
stdout and answering -I are both normal curl behaviour, and both are now
covered, so the mock no longer pins the installer to a single invocation
shape. The distribution fake likewise emits its tampered asset on stdout
instead of through --output, keeping that test about checksum rejection
rather than about which flag was used.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
perf(memory): keep install, update and runtime inside a small-RAM budget (#83)
* perf(memory): keep install, update and runtime inside a small-RAM budget
Small NAT/VPS targets have 128-512MB of RAM and no swap, so anything that
cannot be reclaimed is fatal. This tuning touches no swap and no mmap; it
works by not allocating in the first place.
Installer (covers install and update; update.sh only execs install.sh):
- Put the scratch directory under DATA_DIR instead of mktemp -d. /tmp is
tmpfs on many of these hosts, and without swap every byte there is pinned
in RAM: bundle, extracted release and a copy of the data directory were
held at once, exceeding total RAM of the machines being targeted.
- Hash assets on the way to disk instead of re-reading them. Download is a
single pass now; verification still completes before tar sees the bytes.
- Release the intermediate data.tar right after extraction rather than
holding the data directory twice for the rest of the run.
- Check scratch capacity before downloading, so we fail before the old
service is stopped rather than halfway through.
- Refresh Environment=GOMEMLIMIT/GOGC/THREE_M_UI_CORE_LOW_MEMORY and
MemoryMax on an existing unit. write_service() keeps an operator's unit,
which meant the values baked in at install time never tracked updates.
Panel:
- Bound the Go heap from the process itself via debug.SetMemoryLimit, so the
tuning reaches manual runs, containers and the installer's own init and
healthcheck instead of only a systemd unit. It acts in-process, so the
Mihomo core never inherits the panel's tighter GC target. An explicit
GOMEMLIMIT or GOGC always wins.
- Emit geodata-loader: memconservative for the core when low-memory mode is
on. The key is omitempty and pinned after user fragments merge, so nothing
changes for anyone who does not opt in, and no fragment can undo it.
No new calls to Mmap/Munmap/Madvise, no swap or mmap anywhere in the tree.
* fix(ci): keep the new scratch directory out of the installer's own paths
Two problems CI caught on the small-memory change, both from moving the
scratch directory from mktemp into DATA_DIR.
restore_files() rebuilds the data directory by deleting everything in it
except backups. The scratch directory now lives inside it, so a rollback
deleted the very directory it was reading its own rollback out of, and the
restore died looking for restore/ENABLED. Rollback therefore had to be
skipped, exempting the scratch directory from that purge; the cleanup trap
already removes it.
The test curl also modelled only one of curl's output modes. Writing to
stdout and answering -I are both normal curl behaviour, and both are now
covered, so the mock no longer pins the installer to a single invocation
shape. The distribution fake likewise emits its tampered asset on stdout
instead of through --output, keeping that test about checksum rejection
rather than about which flag was used.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
perf(memory): keep install, update and runtime inside a small-RAM budget (#83)
* perf(memory): keep install, update and runtime inside a small-RAM budget
Small NAT/VPS targets have 128-512MB of RAM and no swap, so anything that
cannot be reclaimed is fatal. This tuning touches no swap and no mmap; it
works by not allocating in the first place.
Installer (covers install and update; update.sh only execs install.sh):
- Put the scratch directory under DATA_DIR instead of mktemp -d. /tmp is
tmpfs on many of these hosts, and without swap every byte there is pinned
in RAM: bundle, extracted release and a copy of the data directory were
held at once, exceeding total RAM of the machines being targeted.
- Hash assets on the way to disk instead of re-reading them. Download is a
single pass now; verification still completes before tar sees the bytes.
- Release the intermediate data.tar right after extraction rather than
holding the data directory twice for the rest of the run.
- Check scratch capacity before downloading, so we fail before the old
service is stopped rather than halfway through.
- Refresh Environment=GOMEMLIMIT/GOGC/THREE_M_UI_CORE_LOW_MEMORY and
MemoryMax on an existing unit. write_service() keeps an operator's unit,
which meant the values baked in at install time never tracked updates.
Panel:
- Bound the Go heap from the process itself via debug.SetMemoryLimit, so the
tuning reaches manual runs, containers and the installer's own init and
healthcheck instead of only a systemd unit. It acts in-process, so the
Mihomo core never inherits the panel's tighter GC target. An explicit
GOMEMLIMIT or GOGC always wins.
- Emit geodata-loader: memconservative for the core when low-memory mode is
on. The key is omitempty and pinned after user fragments merge, so nothing
changes for anyone who does not opt in, and no fragment can undo it.
No new calls to Mmap/Munmap/Madvise, no swap or mmap anywhere in the tree.
* fix(ci): keep the new scratch directory out of the installer's own paths
Two problems CI caught on the small-memory change, both from moving the
scratch directory from mktemp into DATA_DIR.
restore_files() rebuilds the data directory by deleting everything in it
except backups. The scratch directory now lives inside it, so a rollback
deleted the very directory it was reading its own rollback out of, and the
restore died looking for restore/ENABLED. Rollback therefore had to be
skipped, exempting the scratch directory from that purge; the cleanup trap
already removes it.
The test curl also modelled only one of curl's output modes. Writing to
stdout and answering -I are both normal curl behaviour, and both are now
covered, so the mock no longer pins the installer to a single invocation
shape. The distribution fake likewise emits its tampered asset on stdout
instead of through --output, keeping that test about checksum rejection
rather than about which flag was used.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
perf(memory): keep install, update and runtime inside a small-RAM budget (#83)
* perf(memory): keep install, update and runtime inside a small-RAM budget
Small NAT/VPS targets have 128-512MB of RAM and no swap, so anything that
cannot be reclaimed is fatal. This tuning touches no swap and no mmap; it
works by not allocating in the first place.
Installer (covers install and update; update.sh only execs install.sh):
- Put the scratch directory under DATA_DIR instead of mktemp -d. /tmp is
tmpfs on many of these hosts, and without swap every byte there is pinned
in RAM: bundle, extracted release and a copy of the data directory were
held at once, exceeding total RAM of the machines being targeted.
- Hash assets on the way to disk instead of re-reading them. Download is a
single pass now; verification still completes before tar sees the bytes.
- Release the intermediate data.tar right after extraction rather than
holding the data directory twice for the rest of the run.
- Check scratch capacity before downloading, so we fail before the old
service is stopped rather than halfway through.
- Refresh Environment=GOMEMLIMIT/GOGC/THREE_M_UI_CORE_LOW_MEMORY and
MemoryMax on an existing unit. write_service() keeps an operator's unit,
which meant the values baked in at install time never tracked updates.
Panel:
- Bound the Go heap from the process itself via debug.SetMemoryLimit, so the
tuning reaches manual runs, containers and the installer's own init and
healthcheck instead of only a systemd unit. It acts in-process, so the
Mihomo core never inherits the panel's tighter GC target. An explicit
GOMEMLIMIT or GOGC always wins.
- Emit geodata-loader: memconservative for the core when low-memory mode is
on. The key is omitempty and pinned after user fragments merge, so nothing
changes for anyone who does not opt in, and no fragment can undo it.
No new calls to Mmap/Munmap/Madvise, no swap or mmap anywhere in the tree.
* fix(ci): keep the new scratch directory out of the installer's own paths
Two problems CI caught on the small-memory change, both from moving the
scratch directory from mktemp into DATA_DIR.
restore_files() rebuilds the data directory by deleting everything in it
except backups. The scratch directory now lives inside it, so a rollback
deleted the very directory it was reading its own rollback out of, and the
restore died looking for restore/ENABLED. Rollback therefore had to be
skipped, exempting the scratch directory from that purge; the cleanup trap
already removes it.
The test curl also modelled only one of curl's output modes. Writing to
stdout and answering -I are both normal curl behaviour, and both are now
covered, so the mock no longer pins the installer to a single invocation
shape. The distribution fake likewise emits its tampered asset on stdout
instead of through --output, keeping that test about checksum rejection
rather than about which flag was used.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
2026-09-27 09:27:35 +08:00
風雪and風雪 a089b14908 fix(metrics): measure process and system CPU by delta, honour cgroup limits (#82)
* fix(metrics): measure process and system CPU by delta, honour cgroup limits
The process card called gopsutil's Process.CPUPercent(), which returns
100 * lifetimeCPUTime / uptime -- an average over the process' entire
history rather than what it is doing now. Two failure modes followed:
- a long-lived panel settles on a near-constant figure that lags real
spikes by hours;
- when the start time cannot be resolved (BootTime from sysinfo
disagrees with /proc/stat inside some containers) totalTime goes
negative and the call silently yields 0. Measured here: a permanent
0.0% regardless of load.
The system card had the opposite bias inside a container: cpu.Percent()
reads /proc/stat, so a container capped at 4 of 32 host cores topped out
around 21% while actually being throttled. It also only described 200ms
of each second and blocked the request thread doing it.
Both cards now difference cumulative counters across two samples:
- CPU:     cgroup v2 cpu.stat / v1 cpuacct.usage, host /proc/stat fallback
- process: /proc/<pid>/stat user+system (Iowait excluded: it is block
I/O delay, waiting rather than consuming)
- shared denominator cpuCapacity() = min(CFS quota, cpuset, online CPUs)
so the process and system cards finally speak one unit.
Measured on a 4-of-32-core container:
system CPU busy    21%  -> 100%
process CPU busy    0%  ->  89%   (returns to ~0% once idle)
Also fixed along the way:
- GetSystemStats sampled outside its lock, so two concurrent requests
differenced the network counters over ~0s and reported huge spikes.
Sampling now serialises behind a barrier with a minimum interval.
- The disk card hard-coded "/" even when the SQLite database lives on a
mounted volume; it now anchors on the database directory, which
disk.Usage resolves to its containing filesystem via statfs.
- The Telegram resource alerts used the same broken samplers, so a CPU
threshold inside a limited container could never be reached and the
alert would never fire.
New tests cover load tracking, cgroup and quota arithmetic, cpuset
parsing and the already-guarded process memory denominator invariant.
Full suite plus -race passes; darwin/linux-arm64/linux-386 cross-compile.
* fix(memory): report cgroup working set instead of gross usage
The previous commit moved the memory card onto cgroup accounting, which
fixed the two cards disagreeing about their source but swapped in a new
error on the other side: it used memory.current verbatim, and that is a
gross figure billing the cgroup for reclaimable page cache.
Measured on this host against an 8GB cap:
memory.current        4.44 GB
anon                0.90 GB   <- real process memory
inactive_file       3.05 GB   <- reclaimable by the kernel on demand
current / limit       51.7%     <- what the card showed
working set / limit   16.2%     <- actual pressure
So the bar sat in the orange band (>=50%) while 84% of the budget was
free. Reading a large file once -- mihomo's geoip database, a rule
provider download, SQLite growth -- is enough to trigger it, because the
kernel keeps those pages cached and simply hands them back under demand.
"Used" is now the kubelet working-set figure, usage minus inactive_file,
which is what container runtimes report as a percentage. Only *inactive*
cache is discounted, so pages actively mapped and in use stay charged and
the bar can still hold every process' RSS. Three related points:
- The label finally means the same thing on both branches. The
/proc/meminfo fallback already excluded buffers and cache, so gross
cgroup usage and host "used" were two different quantities behind one
label depending on how the panel was deployed.
- The denominator no longer requires a cap to use cgroup accounting. An
unlimited container previously fell back to host-wide figures and so
tracked other tenants' activity, while the CPU card (which always
uses the container's own counter) did not. It falls back to the host
total now only because that genuinely is the cap.
- PAGE_COUNTER_MAX from cgroup v1 is still treated as "no limit".
New tests pin the arithmetic against the real numbers above, the three
ways the limit resolves, and the clamp when cache exceeds usage.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
fix(metrics): measure process and system CPU by delta, honour cgroup limits (#82)
* fix(metrics): measure process and system CPU by delta, honour cgroup limits
The process card called gopsutil's Process.CPUPercent(), which returns
100 * lifetimeCPUTime / uptime -- an average over the process' entire
history rather than what it is doing now. Two failure modes followed:
- a long-lived panel settles on a near-constant figure that lags real
spikes by hours;
- when the start time cannot be resolved (BootTime from sysinfo
disagrees with /proc/stat inside some containers) totalTime goes
negative and the call silently yields 0. Measured here: a permanent
0.0% regardless of load.
The system card had the opposite bias inside a container: cpu.Percent()
reads /proc/stat, so a container capped at 4 of 32 host cores topped out
around 21% while actually being throttled. It also only described 200ms
of each second and blocked the request thread doing it.
Both cards now difference cumulative counters across two samples:
- CPU:     cgroup v2 cpu.stat / v1 cpuacct.usage, host /proc/stat fallback
- process: /proc/<pid>/stat user+system (Iowait excluded: it is block
I/O delay, waiting rather than consuming)
- shared denominator cpuCapacity() = min(CFS quota, cpuset, online CPUs)
so the process and system cards finally speak one unit.
Measured on a 4-of-32-core container:
system CPU busy    21%  -> 100%
process CPU busy    0%  ->  89%   (returns to ~0% once idle)
Also fixed along the way:
- GetSystemStats sampled outside its lock, so two concurrent requests
differenced the network counters over ~0s and reported huge spikes.
Sampling now serialises behind a barrier with a minimum interval.
- The disk card hard-coded "/" even when the SQLite database lives on a
mounted volume; it now anchors on the database directory, which
disk.Usage resolves to its containing filesystem via statfs.
- The Telegram resource alerts used the same broken samplers, so a CPU
threshold inside a limited container could never be reached and the
alert would never fire.
New tests cover load tracking, cgroup and quota arithmetic, cpuset
parsing and the already-guarded process memory denominator invariant.
Full suite plus -race passes; darwin/linux-arm64/linux-386 cross-compile.
* fix(memory): report cgroup working set instead of gross usage
The previous commit moved the memory card onto cgroup accounting, which
fixed the two cards disagreeing about their source but swapped in a new
error on the other side: it used memory.current verbatim, and that is a
gross figure billing the cgroup for reclaimable page cache.
Measured on this host against an 8GB cap:
memory.current        4.44 GB
anon                0.90 GB   <- real process memory
inactive_file       3.05 GB   <- reclaimable by the kernel on demand
current / limit       51.7%     <- what the card showed
working set / limit   16.2%     <- actual pressure
So the bar sat in the orange band (>=50%) while 84% of the budget was
free. Reading a large file once -- mihomo's geoip database, a rule
provider download, SQLite growth -- is enough to trigger it, because the
kernel keeps those pages cached and simply hands them back under demand.
"Used" is now the kubelet working-set figure, usage minus inactive_file,
which is what container runtimes report as a percentage. Only *inactive*
cache is discounted, so pages actively mapped and in use stay charged and
the bar can still hold every process' RSS. Three related points:
- The label finally means the same thing on both branches. The
/proc/meminfo fallback already excluded buffers and cache, so gross
cgroup usage and host "used" were two different quantities behind one
label depending on how the panel was deployed.
- The denominator no longer requires a cap to use cgroup accounting. An
unlimited container previously fell back to host-wide figures and so
tracked other tenants' activity, while the CPU card (which always
uses the container's own counter) did not. It falls back to the host
total now only because that genuinely is the cap.
- PAGE_COUNTER_MAX from cgroup v1 is still treated as "no limit".
New tests pin the arithmetic against the real numbers above, the three
ways the limit resolves, and the clamp when cache exceeds usage.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
fix(metrics): measure process and system CPU by delta, honour cgroup limits (#82)
* fix(metrics): measure process and system CPU by delta, honour cgroup limits
The process card called gopsutil's Process.CPUPercent(), which returns
100 * lifetimeCPUTime / uptime -- an average over the process' entire
history rather than what it is doing now. Two failure modes followed:
- a long-lived panel settles on a near-constant figure that lags real
spikes by hours;
- when the start time cannot be resolved (BootTime from sysinfo
disagrees with /proc/stat inside some containers) totalTime goes
negative and the call silently yields 0. Measured here: a permanent
0.0% regardless of load.
The system card had the opposite bias inside a container: cpu.Percent()
reads /proc/stat, so a container capped at 4 of 32 host cores topped out
around 21% while actually being throttled. It also only described 200ms
of each second and blocked the request thread doing it.
Both cards now difference cumulative counters across two samples:
- CPU:     cgroup v2 cpu.stat / v1 cpuacct.usage, host /proc/stat fallback
- process: /proc/<pid>/stat user+system (Iowait excluded: it is block
I/O delay, waiting rather than consuming)
- shared denominator cpuCapacity() = min(CFS quota, cpuset, online CPUs)
so the process and system cards finally speak one unit.
Measured on a 4-of-32-core container:
system CPU busy    21%  -> 100%
process CPU busy    0%  ->  89%   (returns to ~0% once idle)
Also fixed along the way:
- GetSystemStats sampled outside its lock, so two concurrent requests
differenced the network counters over ~0s and reported huge spikes.
Sampling now serialises behind a barrier with a minimum interval.
- The disk card hard-coded "/" even when the SQLite database lives on a
mounted volume; it now anchors on the database directory, which
disk.Usage resolves to its containing filesystem via statfs.
- The Telegram resource alerts used the same broken samplers, so a CPU
threshold inside a limited container could never be reached and the
alert would never fire.
New tests cover load tracking, cgroup and quota arithmetic, cpuset
parsing and the already-guarded process memory denominator invariant.
Full suite plus -race passes; darwin/linux-arm64/linux-386 cross-compile.
* fix(memory): report cgroup working set instead of gross usage
The previous commit moved the memory card onto cgroup accounting, which
fixed the two cards disagreeing about their source but swapped in a new
error on the other side: it used memory.current verbatim, and that is a
gross figure billing the cgroup for reclaimable page cache.
Measured on this host against an 8GB cap:
memory.current        4.44 GB
anon                0.90 GB   <- real process memory
inactive_file       3.05 GB   <- reclaimable by the kernel on demand
current / limit       51.7%     <- what the card showed
working set / limit   16.2%     <- actual pressure
So the bar sat in the orange band (>=50%) while 84% of the budget was
free. Reading a large file once -- mihomo's geoip database, a rule
provider download, SQLite growth -- is enough to trigger it, because the
kernel keeps those pages cached and simply hands them back under demand.
"Used" is now the kubelet working-set figure, usage minus inactive_file,
which is what container runtimes report as a percentage. Only *inactive*
cache is discounted, so pages actively mapped and in use stay charged and
the bar can still hold every process' RSS. Three related points:
- The label finally means the same thing on both branches. The
/proc/meminfo fallback already excluded buffers and cache, so gross
cgroup usage and host "used" were two different quantities behind one
label depending on how the panel was deployed.
- The denominator no longer requires a cap to use cgroup accounting. An
unlimited container previously fell back to host-wide figures and so
tracked other tenants' activity, while the CPU card (which always
uses the container's own counter) did not. It falls back to the host
total now only because that genuinely is the cap.
- PAGE_COUNTER_MAX from cgroup v1 is still treated as "no limit".
New tests pin the arithmetic against the real numbers above, the three
ways the limit resolves, and the clamp when cache exceeds usage.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
fix(metrics): measure process and system CPU by delta, honour cgroup limits (#82)
* fix(metrics): measure process and system CPU by delta, honour cgroup limits
The process card called gopsutil's Process.CPUPercent(), which returns
100 * lifetimeCPUTime / uptime -- an average over the process' entire
history rather than what it is doing now. Two failure modes followed:
- a long-lived panel settles on a near-constant figure that lags real
spikes by hours;
- when the start time cannot be resolved (BootTime from sysinfo
disagrees with /proc/stat inside some containers) totalTime goes
negative and the call silently yields 0. Measured here: a permanent
0.0% regardless of load.
The system card had the opposite bias inside a container: cpu.Percent()
reads /proc/stat, so a container capped at 4 of 32 host cores topped out
around 21% while actually being throttled. It also only described 200ms
of each second and blocked the request thread doing it.
Both cards now difference cumulative counters across two samples:
- CPU:     cgroup v2 cpu.stat / v1 cpuacct.usage, host /proc/stat fallback
- process: /proc/<pid>/stat user+system (Iowait excluded: it is block
I/O delay, waiting rather than consuming)
- shared denominator cpuCapacity() = min(CFS quota, cpuset, online CPUs)
so the process and system cards finally speak one unit.
Measured on a 4-of-32-core container:
system CPU busy    21%  -> 100%
process CPU busy    0%  ->  89%   (returns to ~0% once idle)
Also fixed along the way:
- GetSystemStats sampled outside its lock, so two concurrent requests
differenced the network counters over ~0s and reported huge spikes.
Sampling now serialises behind a barrier with a minimum interval.
- The disk card hard-coded "/" even when the SQLite database lives on a
mounted volume; it now anchors on the database directory, which
disk.Usage resolves to its containing filesystem via statfs.
- The Telegram resource alerts used the same broken samplers, so a CPU
threshold inside a limited container could never be reached and the
alert would never fire.
New tests cover load tracking, cgroup and quota arithmetic, cpuset
parsing and the already-guarded process memory denominator invariant.
Full suite plus -race passes; darwin/linux-arm64/linux-386 cross-compile.
* fix(memory): report cgroup working set instead of gross usage
The previous commit moved the memory card onto cgroup accounting, which
fixed the two cards disagreeing about their source but swapped in a new
error on the other side: it used memory.current verbatim, and that is a
gross figure billing the cgroup for reclaimable page cache.
Measured on this host against an 8GB cap:
memory.current        4.44 GB
anon                0.90 GB   <- real process memory
inactive_file       3.05 GB   <- reclaimable by the kernel on demand
current / limit       51.7%     <- what the card showed
working set / limit   16.2%     <- actual pressure
So the bar sat in the orange band (>=50%) while 84% of the budget was
free. Reading a large file once -- mihomo's geoip database, a rule
provider download, SQLite growth -- is enough to trigger it, because the
kernel keeps those pages cached and simply hands them back under demand.
"Used" is now the kubelet working-set figure, usage minus inactive_file,
which is what container runtimes report as a percentage. Only *inactive*
cache is discounted, so pages actively mapped and in use stay charged and
the bar can still hold every process' RSS. Three related points:
- The label finally means the same thing on both branches. The
/proc/meminfo fallback already excluded buffers and cache, so gross
cgroup usage and host "used" were two different quantities behind one
label depending on how the panel was deployed.
- The denominator no longer requires a cap to use cgroup accounting. An
unlimited container previously fell back to host-wide figures and so
tracked other tenants' activity, while the CPU card (which always
uses the container's own counter) did not. It falls back to the host
total now only because that genuinely is the cap.
- PAGE_COUNTER_MAX from cgroup v1 is still treated as "no limit".
New tests pin the arithmetic against the real numbers above, the three
ways the limit resolves, and the clamp when cache exceeds usage.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
fix(metrics): measure process and system CPU by delta, honour cgroup limits (#82)
* fix(metrics): measure process and system CPU by delta, honour cgroup limits
The process card called gopsutil's Process.CPUPercent(), which returns
100 * lifetimeCPUTime / uptime -- an average over the process' entire
history rather than what it is doing now. Two failure modes followed:
- a long-lived panel settles on a near-constant figure that lags real
spikes by hours;
- when the start time cannot be resolved (BootTime from sysinfo
disagrees with /proc/stat inside some containers) totalTime goes
negative and the call silently yields 0. Measured here: a permanent
0.0% regardless of load.
The system card had the opposite bias inside a container: cpu.Percent()
reads /proc/stat, so a container capped at 4 of 32 host cores topped out
around 21% while actually being throttled. It also only described 200ms
of each second and blocked the request thread doing it.
Both cards now difference cumulative counters across two samples:
- CPU:     cgroup v2 cpu.stat / v1 cpuacct.usage, host /proc/stat fallback
- process: /proc/<pid>/stat user+system (Iowait excluded: it is block
I/O delay, waiting rather than consuming)
- shared denominator cpuCapacity() = min(CFS quota, cpuset, online CPUs)
so the process and system cards finally speak one unit.
Measured on a 4-of-32-core container:
system CPU busy    21%  -> 100%
process CPU busy    0%  ->  89%   (returns to ~0% once idle)
Also fixed along the way:
- GetSystemStats sampled outside its lock, so two concurrent requests
differenced the network counters over ~0s and reported huge spikes.
Sampling now serialises behind a barrier with a minimum interval.
- The disk card hard-coded "/" even when the SQLite database lives on a
mounted volume; it now anchors on the database directory, which
disk.Usage resolves to its containing filesystem via statfs.
- The Telegram resource alerts used the same broken samplers, so a CPU
threshold inside a limited container could never be reached and the
alert would never fire.
New tests cover load tracking, cgroup and quota arithmetic, cpuset
parsing and the already-guarded process memory denominator invariant.
Full suite plus -race passes; darwin/linux-arm64/linux-386 cross-compile.
* fix(memory): report cgroup working set instead of gross usage
The previous commit moved the memory card onto cgroup accounting, which
fixed the two cards disagreeing about their source but swapped in a new
error on the other side: it used memory.current verbatim, and that is a
gross figure billing the cgroup for reclaimable page cache.
Measured on this host against an 8GB cap:
memory.current        4.44 GB
anon                0.90 GB   <- real process memory
inactive_file       3.05 GB   <- reclaimable by the kernel on demand
current / limit       51.7%     <- what the card showed
working set / limit   16.2%     <- actual pressure
So the bar sat in the orange band (>=50%) while 84% of the budget was
free. Reading a large file once -- mihomo's geoip database, a rule
provider download, SQLite growth -- is enough to trigger it, because the
kernel keeps those pages cached and simply hands them back under demand.
"Used" is now the kubelet working-set figure, usage minus inactive_file,
which is what container runtimes report as a percentage. Only *inactive*
cache is discounted, so pages actively mapped and in use stay charged and
the bar can still hold every process' RSS. Three related points:
- The label finally means the same thing on both branches. The
/proc/meminfo fallback already excluded buffers and cache, so gross
cgroup usage and host "used" were two different quantities behind one
label depending on how the panel was deployed.
- The denominator no longer requires a cap to use cgroup accounting. An
unlimited container previously fell back to host-wide figures and so
tracked other tenants' activity, while the CPU card (which always
uses the container's own counter) did not. It falls back to the host
total now only because that genuinely is the cap.
- PAGE_COUNTER_MAX from cgroup v1 is still treated as "no limit".
New tests pin the arithmetic against the real numbers above, the three
ways the limit resolves, and the clamp when cache exceeds usage.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
fix(metrics): measure process and system CPU by delta, honour cgroup limits (#82)
* fix(metrics): measure process and system CPU by delta, honour cgroup limits
The process card called gopsutil's Process.CPUPercent(), which returns
100 * lifetimeCPUTime / uptime -- an average over the process' entire
history rather than what it is doing now. Two failure modes followed:
- a long-lived panel settles on a near-constant figure that lags real
spikes by hours;
- when the start time cannot be resolved (BootTime from sysinfo
disagrees with /proc/stat inside some containers) totalTime goes
negative and the call silently yields 0. Measured here: a permanent
0.0% regardless of load.
The system card had the opposite bias inside a container: cpu.Percent()
reads /proc/stat, so a container capped at 4 of 32 host cores topped out
around 21% while actually being throttled. It also only described 200ms
of each second and blocked the request thread doing it.
Both cards now difference cumulative counters across two samples:
- CPU:     cgroup v2 cpu.stat / v1 cpuacct.usage, host /proc/stat fallback
- process: /proc/<pid>/stat user+system (Iowait excluded: it is block
I/O delay, waiting rather than consuming)
- shared denominator cpuCapacity() = min(CFS quota, cpuset, online CPUs)
so the process and system cards finally speak one unit.
Measured on a 4-of-32-core container:
system CPU busy    21%  -> 100%
process CPU busy    0%  ->  89%   (returns to ~0% once idle)
Also fixed along the way:
- GetSystemStats sampled outside its lock, so two concurrent requests
differenced the network counters over ~0s and reported huge spikes.
Sampling now serialises behind a barrier with a minimum interval.
- The disk card hard-coded "/" even when the SQLite database lives on a
mounted volume; it now anchors on the database directory, which
disk.Usage resolves to its containing filesystem via statfs.
- The Telegram resource alerts used the same broken samplers, so a CPU
threshold inside a limited container could never be reached and the
alert would never fire.
New tests cover load tracking, cgroup and quota arithmetic, cpuset
parsing and the already-guarded process memory denominator invariant.
Full suite plus -race passes; darwin/linux-arm64/linux-386 cross-compile.
* fix(memory): report cgroup working set instead of gross usage
The previous commit moved the memory card onto cgroup accounting, which
fixed the two cards disagreeing about their source but swapped in a new
error on the other side: it used memory.current verbatim, and that is a
gross figure billing the cgroup for reclaimable page cache.
Measured on this host against an 8GB cap:
memory.current        4.44 GB
anon                0.90 GB   <- real process memory
inactive_file       3.05 GB   <- reclaimable by the kernel on demand
current / limit       51.7%     <- what the card showed
working set / limit   16.2%     <- actual pressure
So the bar sat in the orange band (>=50%) while 84% of the budget was
free. Reading a large file once -- mihomo's geoip database, a rule
provider download, SQLite growth -- is enough to trigger it, because the
kernel keeps those pages cached and simply hands them back under demand.
"Used" is now the kubelet working-set figure, usage minus inactive_file,
which is what container runtimes report as a percentage. Only *inactive*
cache is discounted, so pages actively mapped and in use stay charged and
the bar can still hold every process' RSS. Three related points:
- The label finally means the same thing on both branches. The
/proc/meminfo fallback already excluded buffers and cache, so gross
cgroup usage and host "used" were two different quantities behind one
label depending on how the panel was deployed.
- The denominator no longer requires a cap to use cgroup accounting. An
unlimited container previously fell back to host-wide figures and so
tracked other tenants' activity, while the CPU card (which always
uses the container's own counter) did not. It falls back to the host
total now only because that genuinely is the cap.
- PAGE_COUNTER_MAX from cgroup v1 is still treated as "no limit".
New tests pin the arithmetic against the real numbers above, the three
ways the limit resolves, and the clamp when cache exceeds usage.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
fix(metrics): measure process and system CPU by delta, honour cgroup limits (#82)
* fix(metrics): measure process and system CPU by delta, honour cgroup limits
The process card called gopsutil's Process.CPUPercent(), which returns
100 * lifetimeCPUTime / uptime -- an average over the process' entire
history rather than what it is doing now. Two failure modes followed:
- a long-lived panel settles on a near-constant figure that lags real
spikes by hours;
- when the start time cannot be resolved (BootTime from sysinfo
disagrees with /proc/stat inside some containers) totalTime goes
negative and the call silently yields 0. Measured here: a permanent
0.0% regardless of load.
The system card had the opposite bias inside a container: cpu.Percent()
reads /proc/stat, so a container capped at 4 of 32 host cores topped out
around 21% while actually being throttled. It also only described 200ms
of each second and blocked the request thread doing it.
Both cards now difference cumulative counters across two samples:
- CPU:     cgroup v2 cpu.stat / v1 cpuacct.usage, host /proc/stat fallback
- process: /proc/<pid>/stat user+system (Iowait excluded: it is block
I/O delay, waiting rather than consuming)
- shared denominator cpuCapacity() = min(CFS quota, cpuset, online CPUs)
so the process and system cards finally speak one unit.
Measured on a 4-of-32-core container:
system CPU busy    21%  -> 100%
process CPU busy    0%  ->  89%   (returns to ~0% once idle)
Also fixed along the way:
- GetSystemStats sampled outside its lock, so two concurrent requests
differenced the network counters over ~0s and reported huge spikes.
Sampling now serialises behind a barrier with a minimum interval.
- The disk card hard-coded "/" even when the SQLite database lives on a
mounted volume; it now anchors on the database directory, which
disk.Usage resolves to its containing filesystem via statfs.
- The Telegram resource alerts used the same broken samplers, so a CPU
threshold inside a limited container could never be reached and the
alert would never fire.
New tests cover load tracking, cgroup and quota arithmetic, cpuset
parsing and the already-guarded process memory denominator invariant.
Full suite plus -race passes; darwin/linux-arm64/linux-386 cross-compile.
* fix(memory): report cgroup working set instead of gross usage
The previous commit moved the memory card onto cgroup accounting, which
fixed the two cards disagreeing about their source but swapped in a new
error on the other side: it used memory.current verbatim, and that is a
gross figure billing the cgroup for reclaimable page cache.
Measured on this host against an 8GB cap:
memory.current        4.44 GB
anon                0.90 GB   <- real process memory
inactive_file       3.05 GB   <- reclaimable by the kernel on demand
current / limit       51.7%     <- what the card showed
working set / limit   16.2%     <- actual pressure
So the bar sat in the orange band (>=50%) while 84% of the budget was
free. Reading a large file once -- mihomo's geoip database, a rule
provider download, SQLite growth -- is enough to trigger it, because the
kernel keeps those pages cached and simply hands them back under demand.
"Used" is now the kubelet working-set figure, usage minus inactive_file,
which is what container runtimes report as a percentage. Only *inactive*
cache is discounted, so pages actively mapped and in use stay charged and
the bar can still hold every process' RSS. Three related points:
- The label finally means the same thing on both branches. The
/proc/meminfo fallback already excluded buffers and cache, so gross
cgroup usage and host "used" were two different quantities behind one
label depending on how the panel was deployed.
- The denominator no longer requires a cap to use cgroup accounting. An
unlimited container previously fell back to host-wide figures and so
tracked other tenants' activity, while the CPU card (which always
uses the container's own counter) did not. It falls back to the host
total now only because that genuinely is the cap.
- PAGE_COUNTER_MAX from cgroup v1 is still treated as "no limit".
New tests pin the arithmetic against the real numbers above, the three
ways the limit resolves, and the clamp when cache exceeds usage.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
fix(metrics): measure process and system CPU by delta, honour cgroup limits (#82)
* fix(metrics): measure process and system CPU by delta, honour cgroup limits
The process card called gopsutil's Process.CPUPercent(), which returns
100 * lifetimeCPUTime / uptime -- an average over the process' entire
history rather than what it is doing now. Two failure modes followed:
- a long-lived panel settles on a near-constant figure that lags real
spikes by hours;
- when the start time cannot be resolved (BootTime from sysinfo
disagrees with /proc/stat inside some containers) totalTime goes
negative and the call silently yields 0. Measured here: a permanent
0.0% regardless of load.
The system card had the opposite bias inside a container: cpu.Percent()
reads /proc/stat, so a container capped at 4 of 32 host cores topped out
around 21% while actually being throttled. It also only described 200ms
of each second and blocked the request thread doing it.
Both cards now difference cumulative counters across two samples:
- CPU:     cgroup v2 cpu.stat / v1 cpuacct.usage, host /proc/stat fallback
- process: /proc/<pid>/stat user+system (Iowait excluded: it is block
I/O delay, waiting rather than consuming)
- shared denominator cpuCapacity() = min(CFS quota, cpuset, online CPUs)
so the process and system cards finally speak one unit.
Measured on a 4-of-32-core container:
system CPU busy    21%  -> 100%
process CPU busy    0%  ->  89%   (returns to ~0% once idle)
Also fixed along the way:
- GetSystemStats sampled outside its lock, so two concurrent requests
differenced the network counters over ~0s and reported huge spikes.
Sampling now serialises behind a barrier with a minimum interval.
- The disk card hard-coded "/" even when the SQLite database lives on a
mounted volume; it now anchors on the database directory, which
disk.Usage resolves to its containing filesystem via statfs.
- The Telegram resource alerts used the same broken samplers, so a CPU
threshold inside a limited container could never be reached and the
alert would never fire.
New tests cover load tracking, cgroup and quota arithmetic, cpuset
parsing and the already-guarded process memory denominator invariant.
Full suite plus -race passes; darwin/linux-arm64/linux-386 cross-compile.
* fix(memory): report cgroup working set instead of gross usage
The previous commit moved the memory card onto cgroup accounting, which
fixed the two cards disagreeing about their source but swapped in a new
error on the other side: it used memory.current verbatim, and that is a
gross figure billing the cgroup for reclaimable page cache.
Measured on this host against an 8GB cap:
memory.current        4.44 GB
anon                0.90 GB   <- real process memory
inactive_file       3.05 GB   <- reclaimable by the kernel on demand
current / limit       51.7%     <- what the card showed
working set / limit   16.2%     <- actual pressure
So the bar sat in the orange band (>=50%) while 84% of the budget was
free. Reading a large file once -- mihomo's geoip database, a rule
provider download, SQLite growth -- is enough to trigger it, because the
kernel keeps those pages cached and simply hands them back under demand.
"Used" is now the kubelet working-set figure, usage minus inactive_file,
which is what container runtimes report as a percentage. Only *inactive*
cache is discounted, so pages actively mapped and in use stay charged and
the bar can still hold every process' RSS. Three related points:
- The label finally means the same thing on both branches. The
/proc/meminfo fallback already excluded buffers and cache, so gross
cgroup usage and host "used" were two different quantities behind one
label depending on how the panel was deployed.
- The denominator no longer requires a cap to use cgroup accounting. An
unlimited container previously fell back to host-wide figures and so
tracked other tenants' activity, while the CPU card (which always
uses the container's own counter) did not. It falls back to the host
total now only because that genuinely is the cap.
- PAGE_COUNTER_MAX from cgroup v1 is still treated as "no limit".
New tests pin the arithmetic against the real numbers above, the three
ways the limit resolves, and the clamp when cache exceeds usage.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
fix(metrics): measure process and system CPU by delta, honour cgroup limits (#82)
* fix(metrics): measure process and system CPU by delta, honour cgroup limits
The process card called gopsutil's Process.CPUPercent(), which returns
100 * lifetimeCPUTime / uptime -- an average over the process' entire
history rather than what it is doing now. Two failure modes followed:
- a long-lived panel settles on a near-constant figure that lags real
spikes by hours;
- when the start time cannot be resolved (BootTime from sysinfo
disagrees with /proc/stat inside some containers) totalTime goes
negative and the call silently yields 0. Measured here: a permanent
0.0% regardless of load.
The system card had the opposite bias inside a container: cpu.Percent()
reads /proc/stat, so a container capped at 4 of 32 host cores topped out
around 21% while actually being throttled. It also only described 200ms
of each second and blocked the request thread doing it.
Both cards now difference cumulative counters across two samples:
- CPU:     cgroup v2 cpu.stat / v1 cpuacct.usage, host /proc/stat fallback
- process: /proc/<pid>/stat user+system (Iowait excluded: it is block
I/O delay, waiting rather than consuming)
- shared denominator cpuCapacity() = min(CFS quota, cpuset, online CPUs)
so the process and system cards finally speak one unit.
Measured on a 4-of-32-core container:
system CPU busy    21%  -> 100%
process CPU busy    0%  ->  89%   (returns to ~0% once idle)
Also fixed along the way:
- GetSystemStats sampled outside its lock, so two concurrent requests
differenced the network counters over ~0s and reported huge spikes.
Sampling now serialises behind a barrier with a minimum interval.
- The disk card hard-coded "/" even when the SQLite database lives on a
mounted volume; it now anchors on the database directory, which
disk.Usage resolves to its containing filesystem via statfs.
- The Telegram resource alerts used the same broken samplers, so a CPU
threshold inside a limited container could never be reached and the
alert would never fire.
New tests cover load tracking, cgroup and quota arithmetic, cpuset
parsing and the already-guarded process memory denominator invariant.
Full suite plus -race passes; darwin/linux-arm64/linux-386 cross-compile.
* fix(memory): report cgroup working set instead of gross usage
The previous commit moved the memory card onto cgroup accounting, which
fixed the two cards disagreeing about their source but swapped in a new
error on the other side: it used memory.current verbatim, and that is a
gross figure billing the cgroup for reclaimable page cache.
Measured on this host against an 8GB cap:
memory.current        4.44 GB
anon                0.90 GB   <- real process memory
inactive_file       3.05 GB   <- reclaimable by the kernel on demand
current / limit       51.7%     <- what the card showed
working set / limit   16.2%     <- actual pressure
So the bar sat in the orange band (>=50%) while 84% of the budget was
free. Reading a large file once -- mihomo's geoip database, a rule
provider download, SQLite growth -- is enough to trigger it, because the
kernel keeps those pages cached and simply hands them back under demand.
"Used" is now the kubelet working-set figure, usage minus inactive_file,
which is what container runtimes report as a percentage. Only *inactive*
cache is discounted, so pages actively mapped and in use stay charged and
the bar can still hold every process' RSS. Three related points:
- The label finally means the same thing on both branches. The
/proc/meminfo fallback already excluded buffers and cache, so gross
cgroup usage and host "used" were two different quantities behind one
label depending on how the panel was deployed.
- The denominator no longer requires a cap to use cgroup accounting. An
unlimited container previously fell back to host-wide figures and so
tracked other tenants' activity, while the CPU card (which always
uses the container's own counter) did not. It falls back to the host
total now only because that genuinely is the cap.
- PAGE_COUNTER_MAX from cgroup v1 is still treated as "no limit".
New tests pin the arithmetic against the real numbers above, the three
ways the limit resolves, and the clamp when cache exceeds usage.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
fix(metrics): measure process and system CPU by delta, honour cgroup limits (#82)
* fix(metrics): measure process and system CPU by delta, honour cgroup limits
The process card called gopsutil's Process.CPUPercent(), which returns
100 * lifetimeCPUTime / uptime -- an average over the process' entire
history rather than what it is doing now. Two failure modes followed:
- a long-lived panel settles on a near-constant figure that lags real
spikes by hours;
- when the start time cannot be resolved (BootTime from sysinfo
disagrees with /proc/stat inside some containers) totalTime goes
negative and the call silently yields 0. Measured here: a permanent
0.0% regardless of load.
The system card had the opposite bias inside a container: cpu.Percent()
reads /proc/stat, so a container capped at 4 of 32 host cores topped out
around 21% while actually being throttled. It also only described 200ms
of each second and blocked the request thread doing it.
Both cards now difference cumulative counters across two samples:
- CPU:     cgroup v2 cpu.stat / v1 cpuacct.usage, host /proc/stat fallback
- process: /proc/<pid>/stat user+system (Iowait excluded: it is block
I/O delay, waiting rather than consuming)
- shared denominator cpuCapacity() = min(CFS quota, cpuset, online CPUs)
so the process and system cards finally speak one unit.
Measured on a 4-of-32-core container:
system CPU busy    21%  -> 100%
process CPU busy    0%  ->  89%   (returns to ~0% once idle)
Also fixed along the way:
- GetSystemStats sampled outside its lock, so two concurrent requests
differenced the network counters over ~0s and reported huge spikes.
Sampling now serialises behind a barrier with a minimum interval.
- The disk card hard-coded "/" even when the SQLite database lives on a
mounted volume; it now anchors on the database directory, which
disk.Usage resolves to its containing filesystem via statfs.
- The Telegram resource alerts used the same broken samplers, so a CPU
threshold inside a limited container could never be reached and the
alert would never fire.
New tests cover load tracking, cgroup and quota arithmetic, cpuset
parsing and the already-guarded process memory denominator invariant.
Full suite plus -race passes; darwin/linux-arm64/linux-386 cross-compile.
* fix(memory): report cgroup working set instead of gross usage
The previous commit moved the memory card onto cgroup accounting, which
fixed the two cards disagreeing about their source but swapped in a new
error on the other side: it used memory.current verbatim, and that is a
gross figure billing the cgroup for reclaimable page cache.
Measured on this host against an 8GB cap:
memory.current        4.44 GB
anon                0.90 GB   <- real process memory
inactive_file       3.05 GB   <- reclaimable by the kernel on demand
current / limit       51.7%     <- what the card showed
working set / limit   16.2%     <- actual pressure
So the bar sat in the orange band (>=50%) while 84% of the budget was
free. Reading a large file once -- mihomo's geoip database, a rule
provider download, SQLite growth -- is enough to trigger it, because the
kernel keeps those pages cached and simply hands them back under demand.
"Used" is now the kubelet working-set figure, usage minus inactive_file,
which is what container runtimes report as a percentage. Only *inactive*
cache is discounted, so pages actively mapped and in use stay charged and
the bar can still hold every process' RSS. Three related points:
- The label finally means the same thing on both branches. The
/proc/meminfo fallback already excluded buffers and cache, so gross
cgroup usage and host "used" were two different quantities behind one
label depending on how the panel was deployed.
- The denominator no longer requires a cap to use cgroup accounting. An
unlimited container previously fell back to host-wide figures and so
tracked other tenants' activity, while the CPU card (which always
uses the container's own counter) did not. It falls back to the host
total now only because that genuinely is the cap.
- PAGE_COUNTER_MAX from cgroup v1 is still treated as "no limit".
New tests pin the arithmetic against the real numbers above, the three
ways the limit resolves, and the clamp when cache exceeds usage.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
fix(metrics): measure process and system CPU by delta, honour cgroup limits (#82)
* fix(metrics): measure process and system CPU by delta, honour cgroup limits
The process card called gopsutil's Process.CPUPercent(), which returns
100 * lifetimeCPUTime / uptime -- an average over the process' entire
history rather than what it is doing now. Two failure modes followed:
- a long-lived panel settles on a near-constant figure that lags real
spikes by hours;
- when the start time cannot be resolved (BootTime from sysinfo
disagrees with /proc/stat inside some containers) totalTime goes
negative and the call silently yields 0. Measured here: a permanent
0.0% regardless of load.
The system card had the opposite bias inside a container: cpu.Percent()
reads /proc/stat, so a container capped at 4 of 32 host cores topped out
around 21% while actually being throttled. It also only described 200ms
of each second and blocked the request thread doing it.
Both cards now difference cumulative counters across two samples:
- CPU:     cgroup v2 cpu.stat / v1 cpuacct.usage, host /proc/stat fallback
- process: /proc/<pid>/stat user+system (Iowait excluded: it is block
I/O delay, waiting rather than consuming)
- shared denominator cpuCapacity() = min(CFS quota, cpuset, online CPUs)
so the process and system cards finally speak one unit.
Measured on a 4-of-32-core container:
system CPU busy    21%  -> 100%
process CPU busy    0%  ->  89%   (returns to ~0% once idle)
Also fixed along the way:
- GetSystemStats sampled outside its lock, so two concurrent requests
differenced the network counters over ~0s and reported huge spikes.
Sampling now serialises behind a barrier with a minimum interval.
- The disk card hard-coded "/" even when the SQLite database lives on a
mounted volume; it now anchors on the database directory, which
disk.Usage resolves to its containing filesystem via statfs.
- The Telegram resource alerts used the same broken samplers, so a CPU
threshold inside a limited container could never be reached and the
alert would never fire.
New tests cover load tracking, cgroup and quota arithmetic, cpuset
parsing and the already-guarded process memory denominator invariant.
Full suite plus -race passes; darwin/linux-arm64/linux-386 cross-compile.
* fix(memory): report cgroup working set instead of gross usage
The previous commit moved the memory card onto cgroup accounting, which
fixed the two cards disagreeing about their source but swapped in a new
error on the other side: it used memory.current verbatim, and that is a
gross figure billing the cgroup for reclaimable page cache.
Measured on this host against an 8GB cap:
memory.current        4.44 GB
anon                0.90 GB   <- real process memory
inactive_file       3.05 GB   <- reclaimable by the kernel on demand
current / limit       51.7%     <- what the card showed
working set / limit   16.2%     <- actual pressure
So the bar sat in the orange band (>=50%) while 84% of the budget was
free. Reading a large file once -- mihomo's geoip database, a rule
provider download, SQLite growth -- is enough to trigger it, because the
kernel keeps those pages cached and simply hands them back under demand.
"Used" is now the kubelet working-set figure, usage minus inactive_file,
which is what container runtimes report as a percentage. Only *inactive*
cache is discounted, so pages actively mapped and in use stay charged and
the bar can still hold every process' RSS. Three related points:
- The label finally means the same thing on both branches. The
/proc/meminfo fallback already excluded buffers and cache, so gross
cgroup usage and host "used" were two different quantities behind one
label depending on how the panel was deployed.
- The denominator no longer requires a cap to use cgroup accounting. An
unlimited container previously fell back to host-wide figures and so
tracked other tenants' activity, while the CPU card (which always
uses the container's own counter) did not. It falls back to the host
total now only because that genuinely is the cap.
- PAGE_COUNTER_MAX from cgroup v1 is still treated as "no limit".
New tests pin the arithmetic against the real numbers above, the three
ways the limit resolves, and the clamp when cache exceeds usage.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
fix(metrics): measure process and system CPU by delta, honour cgroup limits (#82)
* fix(metrics): measure process and system CPU by delta, honour cgroup limits
The process card called gopsutil's Process.CPUPercent(), which returns
100 * lifetimeCPUTime / uptime -- an average over the process' entire
history rather than what it is doing now. Two failure modes followed:
- a long-lived panel settles on a near-constant figure that lags real
spikes by hours;
- when the start time cannot be resolved (BootTime from sysinfo
disagrees with /proc/stat inside some containers) totalTime goes
negative and the call silently yields 0. Measured here: a permanent
0.0% regardless of load.
The system card had the opposite bias inside a container: cpu.Percent()
reads /proc/stat, so a container capped at 4 of 32 host cores topped out
around 21% while actually being throttled. It also only described 200ms
of each second and blocked the request thread doing it.
Both cards now difference cumulative counters across two samples:
- CPU:     cgroup v2 cpu.stat / v1 cpuacct.usage, host /proc/stat fallback
- process: /proc/<pid>/stat user+system (Iowait excluded: it is block
I/O delay, waiting rather than consuming)
- shared denominator cpuCapacity() = min(CFS quota, cpuset, online CPUs)
so the process and system cards finally speak one unit.
Measured on a 4-of-32-core container:
system CPU busy    21%  -> 100%
process CPU busy    0%  ->  89%   (returns to ~0% once idle)
Also fixed along the way:
- GetSystemStats sampled outside its lock, so two concurrent requests
differenced the network counters over ~0s and reported huge spikes.
Sampling now serialises behind a barrier with a minimum interval.
- The disk card hard-coded "/" even when the SQLite database lives on a
mounted volume; it now anchors on the database directory, which
disk.Usage resolves to its containing filesystem via statfs.
- The Telegram resource alerts used the same broken samplers, so a CPU
threshold inside a limited container could never be reached and the
alert would never fire.
New tests cover load tracking, cgroup and quota arithmetic, cpuset
parsing and the already-guarded process memory denominator invariant.
Full suite plus -race passes; darwin/linux-arm64/linux-386 cross-compile.
* fix(memory): report cgroup working set instead of gross usage
The previous commit moved the memory card onto cgroup accounting, which
fixed the two cards disagreeing about their source but swapped in a new
error on the other side: it used memory.current verbatim, and that is a
gross figure billing the cgroup for reclaimable page cache.
Measured on this host against an 8GB cap:
memory.current        4.44 GB
anon                0.90 GB   <- real process memory
inactive_file       3.05 GB   <- reclaimable by the kernel on demand
current / limit       51.7%     <- what the card showed
working set / limit   16.2%     <- actual pressure
So the bar sat in the orange band (>=50%) while 84% of the budget was
free. Reading a large file once -- mihomo's geoip database, a rule
provider download, SQLite growth -- is enough to trigger it, because the
kernel keeps those pages cached and simply hands them back under demand.
"Used" is now the kubelet working-set figure, usage minus inactive_file,
which is what container runtimes report as a percentage. Only *inactive*
cache is discounted, so pages actively mapped and in use stay charged and
the bar can still hold every process' RSS. Three related points:
- The label finally means the same thing on both branches. The
/proc/meminfo fallback already excluded buffers and cache, so gross
cgroup usage and host "used" were two different quantities behind one
label depending on how the panel was deployed.
- The denominator no longer requires a cap to use cgroup accounting. An
unlimited container previously fell back to host-wide figures and so
tracked other tenants' activity, while the CPU card (which always
uses the container's own counter) did not. It falls back to the host
total now only because that genuinely is the cap.
- PAGE_COUNTER_MAX from cgroup v1 is still treated as "no limit".
New tests pin the arithmetic against the real numbers above, the three
ways the limit resolves, and the clamp when cache exceeds usage.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
2026-09-27 00:10:01 +08:00
風雪 270f2669bd Delete .github/workflows/embed-frontend.yml
Delete .github/workflows/embed-frontend.yml
Delete .github/workflows/embed-frontend.yml
Delete .github/workflows/embed-frontend.yml
Delete .github/workflows/embed-frontend.yml
Delete .github/workflows/embed-frontend.yml
Delete .github/workflows/embed-frontend.yml
Delete .github/workflows/embed-frontend.yml
Delete .github/workflows/embed-frontend.yml
Delete .github/workflows/embed-frontend.yml
Delete .github/workflows/embed-frontend.yml
Delete .github/workflows/embed-frontend.yml
2026-09-26 09:35:06 +08:00
風雪 e930d09044 ui(dashboard): put process usage beside host resources on desktop (#78)
The process card was xs={24} on every breakpoint, so on desktop it dropped
to a row of its own under the host resource card — a wide, mostly empty
card closing the page. Give it md={12} lg={8} so it lands next to the
system card:
lg+  [Users][Status][Listeners] / [Traffic][System][Process]
md   [Users][Status] / [Listeners][Traffic] / [System][Process]
Sharing a row means the card is at most a third of the viewport, and the
side-by-side group layout does not survive that: each metric cell drops to
~60px below 1200px, narrower than the 28px figures inside it. Groups now
always stack, which also reads better — rows are processes, columns are
metrics. Cell padding trimmed to keep the stacked card close in height to
its neighbours.
Grid import dropped: the wall no longer queries breakpoints.
Co-authored-by: codebuddy <codebuddy@users.noreply.github.com>
ui(dashboard): put process usage beside host resources on desktop (#78)
The process card was xs={24} on every breakpoint, so on desktop it dropped
to a row of its own under the host resource card — a wide, mostly empty
card closing the page. Give it md={12} lg={8} so it lands next to the
system card:
lg+  [Users][Status][Listeners] / [Traffic][System][Process]
md   [Users][Status] / [Listeners][Traffic] / [System][Process]
Sharing a row means the card is at most a third of the viewport, and the
side-by-side group layout does not survive that: each metric cell drops to
~60px below 1200px, narrower than the 28px figures inside it. Groups now
always stack, which also reads better — rows are processes, columns are
metrics. Cell padding trimmed to keep the stacked card close in height to
its neighbours.
Grid import dropped: the wall no longer queries breakpoints.
Co-authored-by: codebuddy <codebuddy@users.noreply.github.com>
ui(dashboard): put process usage beside host resources on desktop (#78)
The process card was xs={24} on every breakpoint, so on desktop it dropped
to a row of its own under the host resource card — a wide, mostly empty
card closing the page. Give it md={12} lg={8} so it lands next to the
system card:
lg+  [Users][Status][Listeners] / [Traffic][System][Process]
md   [Users][Status] / [Listeners][Traffic] / [System][Process]
Sharing a row means the card is at most a third of the viewport, and the
side-by-side group layout does not survive that: each metric cell drops to
~60px below 1200px, narrower than the 28px figures inside it. Groups now
always stack, which also reads better — rows are processes, columns are
metrics. Cell padding trimmed to keep the stacked card close in height to
its neighbours.
Grid import dropped: the wall no longer queries breakpoints.
Co-authored-by: codebuddy <codebuddy@users.noreply.github.com>
ui(dashboard): put process usage beside host resources on desktop (#78)
The process card was xs={24} on every breakpoint, so on desktop it dropped
to a row of its own under the host resource card — a wide, mostly empty
card closing the page. Give it md={12} lg={8} so it lands next to the
system card:
lg+  [Users][Status][Listeners] / [Traffic][System][Process]
md   [Users][Status] / [Listeners][Traffic] / [System][Process]
Sharing a row means the card is at most a third of the viewport, and the
side-by-side group layout does not survive that: each metric cell drops to
~60px below 1200px, narrower than the 28px figures inside it. Groups now
always stack, which also reads better — rows are processes, columns are
metrics. Cell padding trimmed to keep the stacked card close in height to
its neighbours.
Grid import dropped: the wall no longer queries breakpoints.
Co-authored-by: codebuddy <codebuddy@users.noreply.github.com>
ui(dashboard): put process usage beside host resources on desktop (#78)
The process card was xs={24} on every breakpoint, so on desktop it dropped
to a row of its own under the host resource card — a wide, mostly empty
card closing the page. Give it md={12} lg={8} so it lands next to the
system card:
lg+  [Users][Status][Listeners] / [Traffic][System][Process]
md   [Users][Status] / [Listeners][Traffic] / [System][Process]
Sharing a row means the card is at most a third of the viewport, and the
side-by-side group layout does not survive that: each metric cell drops to
~60px below 1200px, narrower than the 28px figures inside it. Groups now
always stack, which also reads better — rows are processes, columns are
metrics. Cell padding trimmed to keep the stacked card close in height to
its neighbours.
Grid import dropped: the wall no longer queries breakpoints.
Co-authored-by: codebuddy <codebuddy@users.noreply.github.com>
ui(dashboard): put process usage beside host resources on desktop (#78)
The process card was xs={24} on every breakpoint, so on desktop it dropped
to a row of its own under the host resource card — a wide, mostly empty
card closing the page. Give it md={12} lg={8} so it lands next to the
system card:
lg+  [Users][Status][Listeners] / [Traffic][System][Process]
md   [Users][Status] / [Listeners][Traffic] / [System][Process]
Sharing a row means the card is at most a third of the viewport, and the
side-by-side group layout does not survive that: each metric cell drops to
~60px below 1200px, narrower than the 28px figures inside it. Groups now
always stack, which also reads better — rows are processes, columns are
metrics. Cell padding trimmed to keep the stacked card close in height to
its neighbours.
Grid import dropped: the wall no longer queries breakpoints.
Co-authored-by: codebuddy <codebuddy@users.noreply.github.com>
ui(dashboard): put process usage beside host resources on desktop (#78)
The process card was xs={24} on every breakpoint, so on desktop it dropped
to a row of its own under the host resource card — a wide, mostly empty
card closing the page. Give it md={12} lg={8} so it lands next to the
system card:
lg+  [Users][Status][Listeners] / [Traffic][System][Process]
md   [Users][Status] / [Listeners][Traffic] / [System][Process]
Sharing a row means the card is at most a third of the viewport, and the
side-by-side group layout does not survive that: each metric cell drops to
~60px below 1200px, narrower than the 28px figures inside it. Groups now
always stack, which also reads better — rows are processes, columns are
metrics. Cell padding trimmed to keep the stacked card close in height to
its neighbours.
Grid import dropped: the wall no longer queries breakpoints.
Co-authored-by: codebuddy <codebuddy@users.noreply.github.com>
ui(dashboard): put process usage beside host resources on desktop (#78)
The process card was xs={24} on every breakpoint, so on desktop it dropped
to a row of its own under the host resource card — a wide, mostly empty
card closing the page. Give it md={12} lg={8} so it lands next to the
system card:
lg+  [Users][Status][Listeners] / [Traffic][System][Process]
md   [Users][Status] / [Listeners][Traffic] / [System][Process]
Sharing a row means the card is at most a third of the viewport, and the
side-by-side group layout does not survive that: each metric cell drops to
~60px below 1200px, narrower than the 28px figures inside it. Groups now
always stack, which also reads better — rows are processes, columns are
metrics. Cell padding trimmed to keep the stacked card close in height to
its neighbours.
Grid import dropped: the wall no longer queries breakpoints.
Co-authored-by: codebuddy <codebuddy@users.noreply.github.com>
ui(dashboard): put process usage beside host resources on desktop (#78)
The process card was xs={24} on every breakpoint, so on desktop it dropped
to a row of its own under the host resource card — a wide, mostly empty
card closing the page. Give it md={12} lg={8} so it lands next to the
system card:
lg+  [Users][Status][Listeners] / [Traffic][System][Process]
md   [Users][Status] / [Listeners][Traffic] / [System][Process]
Sharing a row means the card is at most a third of the viewport, and the
side-by-side group layout does not survive that: each metric cell drops to
~60px below 1200px, narrower than the 28px figures inside it. Groups now
always stack, which also reads better — rows are processes, columns are
metrics. Cell padding trimmed to keep the stacked card close in height to
its neighbours.
Grid import dropped: the wall no longer queries breakpoints.
Co-authored-by: codebuddy <codebuddy@users.noreply.github.com>
ui(dashboard): put process usage beside host resources on desktop (#78)
The process card was xs={24} on every breakpoint, so on desktop it dropped
to a row of its own under the host resource card — a wide, mostly empty
card closing the page. Give it md={12} lg={8} so it lands next to the
system card:
lg+  [Users][Status][Listeners] / [Traffic][System][Process]
md   [Users][Status] / [Listeners][Traffic] / [System][Process]
Sharing a row means the card is at most a third of the viewport, and the
side-by-side group layout does not survive that: each metric cell drops to
~60px below 1200px, narrower than the 28px figures inside it. Groups now
always stack, which also reads better — rows are processes, columns are
metrics. Cell padding trimmed to keep the stacked card close in height to
its neighbours.
Grid import dropped: the wall no longer queries breakpoints.
Co-authored-by: codebuddy <codebuddy@users.noreply.github.com>
ui(dashboard): put process usage beside host resources on desktop (#78)
The process card was xs={24} on every breakpoint, so on desktop it dropped
to a row of its own under the host resource card — a wide, mostly empty
card closing the page. Give it md={12} lg={8} so it lands next to the
system card:
lg+  [Users][Status][Listeners] / [Traffic][System][Process]
md   [Users][Status] / [Listeners][Traffic] / [System][Process]
Sharing a row means the card is at most a third of the viewport, and the
side-by-side group layout does not survive that: each metric cell drops to
~60px below 1200px, narrower than the 28px figures inside it. Groups now
always stack, which also reads better — rows are processes, columns are
metrics. Cell padding trimmed to keep the stacked card close in height to
its neighbours.
Grid import dropped: the wall no longer queries breakpoints.
Co-authored-by: codebuddy <codebuddy@users.noreply.github.com>
ui(dashboard): put process usage beside host resources on desktop (#78)
The process card was xs={24} on every breakpoint, so on desktop it dropped
to a row of its own under the host resource card — a wide, mostly empty
card closing the page. Give it md={12} lg={8} so it lands next to the
system card:
lg+  [Users][Status][Listeners] / [Traffic][System][Process]
md   [Users][Status] / [Listeners][Traffic] / [System][Process]
Sharing a row means the card is at most a third of the viewport, and the
side-by-side group layout does not survive that: each metric cell drops to
~60px below 1200px, narrower than the 28px figures inside it. Groups now
always stack, which also reads better — rows are processes, columns are
metrics. Cell padding trimmed to keep the stacked card close in height to
its neighbours.
Grid import dropped: the wall no longer queries breakpoints.
Co-authored-by: codebuddy <codebuddy@users.noreply.github.com>
2026-09-16 09:35:31 +08:00
風雪 9678c9d5ee Delete .mailmap
Delete .mailmap
Delete .mailmap
Delete .mailmap
Delete .mailmap
Delete .mailmap
Delete .mailmap
Delete .mailmap
Delete .mailmap
Delete .mailmap
Delete .mailmap
Delete .mailmap
2026-09-16 00:03:08 +08:00
風雪 02c85cd3c9 perf(dashboard): refresh every second, bound CPU sampling cost
Poll /dashboard at 1s instead of 10s and make the sampling cheap enough to sustain it.
Frontend:
- fetchDashboard() now takes an AbortSignal.
- Replace setInterval(load, 10000) with startVisiblePolling(load, 1000) so requests are single-flight, cancelled on unmount, and paused while the tab is hidden.
- Swallow aborted requests instead of surfacing them as error toasts.
Backend:
- GetSystemStats() now serves from a 500ms TTL cache. sampleCPU() blocks for 200ms (cpu.Percent), so at 1s cadence an uncached path would burn ~20% of a core on sampling alone; the cache caps it and collapses concurrent requests onto one sample.
- Sampling logic moved to sampleSystemStats(); returned struct is copied so callers cannot mutate the cache.
Verified locally: frontend build + oxlint (0 errors) + 16/16 unit tests, backend gofmt/vet/build + go test ./internal/system/... ok.
perf(dashboard): refresh every second, bound CPU sampling cost
Poll /dashboard at 1s instead of 10s and make the sampling cheap enough to sustain it.
Frontend:
- fetchDashboard() now takes an AbortSignal.
- Replace setInterval(load, 10000) with startVisiblePolling(load, 1000) so requests are single-flight, cancelled on unmount, and paused while the tab is hidden.
- Swallow aborted requests instead of surfacing them as error toasts.
Backend:
- GetSystemStats() now serves from a 500ms TTL cache. sampleCPU() blocks for 200ms (cpu.Percent), so at 1s cadence an uncached path would burn ~20% of a core on sampling alone; the cache caps it and collapses concurrent requests onto one sample.
- Sampling logic moved to sampleSystemStats(); returned struct is copied so callers cannot mutate the cache.
Verified locally: frontend build + oxlint (0 errors) + 16/16 unit tests, backend gofmt/vet/build + go test ./internal/system/... ok.
perf(dashboard): refresh every second, bound CPU sampling cost
Poll /dashboard at 1s instead of 10s and make the sampling cheap enough to sustain it.
Frontend:
- fetchDashboard() now takes an AbortSignal.
- Replace setInterval(load, 10000) with startVisiblePolling(load, 1000) so requests are single-flight, cancelled on unmount, and paused while the tab is hidden.
- Swallow aborted requests instead of surfacing them as error toasts.
Backend:
- GetSystemStats() now serves from a 500ms TTL cache. sampleCPU() blocks for 200ms (cpu.Percent), so at 1s cadence an uncached path would burn ~20% of a core on sampling alone; the cache caps it and collapses concurrent requests onto one sample.
- Sampling logic moved to sampleSystemStats(); returned struct is copied so callers cannot mutate the cache.
Verified locally: frontend build + oxlint (0 errors) + 16/16 unit tests, backend gofmt/vet/build + go test ./internal/system/... ok.
perf(dashboard): refresh every second, bound CPU sampling cost
Poll /dashboard at 1s instead of 10s and make the sampling cheap enough to sustain it.
Frontend:
- fetchDashboard() now takes an AbortSignal.
- Replace setInterval(load, 10000) with startVisiblePolling(load, 1000) so requests are single-flight, cancelled on unmount, and paused while the tab is hidden.
- Swallow aborted requests instead of surfacing them as error toasts.
Backend:
- GetSystemStats() now serves from a 500ms TTL cache. sampleCPU() blocks for 200ms (cpu.Percent), so at 1s cadence an uncached path would burn ~20% of a core on sampling alone; the cache caps it and collapses concurrent requests onto one sample.
- Sampling logic moved to sampleSystemStats(); returned struct is copied so callers cannot mutate the cache.
Verified locally: frontend build + oxlint (0 errors) + 16/16 unit tests, backend gofmt/vet/build + go test ./internal/system/... ok.
perf(dashboard): refresh every second, bound CPU sampling cost
Poll /dashboard at 1s instead of 10s and make the sampling cheap enough to sustain it.
Frontend:
- fetchDashboard() now takes an AbortSignal.
- Replace setInterval(load, 10000) with startVisiblePolling(load, 1000) so requests are single-flight, cancelled on unmount, and paused while the tab is hidden.
- Swallow aborted requests instead of surfacing them as error toasts.
Backend:
- GetSystemStats() now serves from a 500ms TTL cache. sampleCPU() blocks for 200ms (cpu.Percent), so at 1s cadence an uncached path would burn ~20% of a core on sampling alone; the cache caps it and collapses concurrent requests onto one sample.
- Sampling logic moved to sampleSystemStats(); returned struct is copied so callers cannot mutate the cache.
Verified locally: frontend build + oxlint (0 errors) + 16/16 unit tests, backend gofmt/vet/build + go test ./internal/system/... ok.
perf(dashboard): refresh every second, bound CPU sampling cost
Poll /dashboard at 1s instead of 10s and make the sampling cheap enough to sustain it.
Frontend:
- fetchDashboard() now takes an AbortSignal.
- Replace setInterval(load, 10000) with startVisiblePolling(load, 1000) so requests are single-flight, cancelled on unmount, and paused while the tab is hidden.
- Swallow aborted requests instead of surfacing them as error toasts.
Backend:
- GetSystemStats() now serves from a 500ms TTL cache. sampleCPU() blocks for 200ms (cpu.Percent), so at 1s cadence an uncached path would burn ~20% of a core on sampling alone; the cache caps it and collapses concurrent requests onto one sample.
- Sampling logic moved to sampleSystemStats(); returned struct is copied so callers cannot mutate the cache.
Verified locally: frontend build + oxlint (0 errors) + 16/16 unit tests, backend gofmt/vet/build + go test ./internal/system/... ok.
perf(dashboard): refresh every second, bound CPU sampling cost
Poll /dashboard at 1s instead of 10s and make the sampling cheap enough to sustain it.
Frontend:
- fetchDashboard() now takes an AbortSignal.
- Replace setInterval(load, 10000) with startVisiblePolling(load, 1000) so requests are single-flight, cancelled on unmount, and paused while the tab is hidden.
- Swallow aborted requests instead of surfacing them as error toasts.
Backend:
- GetSystemStats() now serves from a 500ms TTL cache. sampleCPU() blocks for 200ms (cpu.Percent), so at 1s cadence an uncached path would burn ~20% of a core on sampling alone; the cache caps it and collapses concurrent requests onto one sample.
- Sampling logic moved to sampleSystemStats(); returned struct is copied so callers cannot mutate the cache.
Verified locally: frontend build + oxlint (0 errors) + 16/16 unit tests, backend gofmt/vet/build + go test ./internal/system/... ok.
perf(dashboard): refresh every second, bound CPU sampling cost
Poll /dashboard at 1s instead of 10s and make the sampling cheap enough to sustain it.
Frontend:
- fetchDashboard() now takes an AbortSignal.
- Replace setInterval(load, 10000) with startVisiblePolling(load, 1000) so requests are single-flight, cancelled on unmount, and paused while the tab is hidden.
- Swallow aborted requests instead of surfacing them as error toasts.
Backend:
- GetSystemStats() now serves from a 500ms TTL cache. sampleCPU() blocks for 200ms (cpu.Percent), so at 1s cadence an uncached path would burn ~20% of a core on sampling alone; the cache caps it and collapses concurrent requests onto one sample.
- Sampling logic moved to sampleSystemStats(); returned struct is copied so callers cannot mutate the cache.
Verified locally: frontend build + oxlint (0 errors) + 16/16 unit tests, backend gofmt/vet/build + go test ./internal/system/... ok.
perf(dashboard): refresh every second, bound CPU sampling cost
Poll /dashboard at 1s instead of 10s and make the sampling cheap enough to sustain it.
Frontend:
- fetchDashboard() now takes an AbortSignal.
- Replace setInterval(load, 10000) with startVisiblePolling(load, 1000) so requests are single-flight, cancelled on unmount, and paused while the tab is hidden.
- Swallow aborted requests instead of surfacing them as error toasts.
Backend:
- GetSystemStats() now serves from a 500ms TTL cache. sampleCPU() blocks for 200ms (cpu.Percent), so at 1s cadence an uncached path would burn ~20% of a core on sampling alone; the cache caps it and collapses concurrent requests onto one sample.
- Sampling logic moved to sampleSystemStats(); returned struct is copied so callers cannot mutate the cache.
Verified locally: frontend build + oxlint (0 errors) + 16/16 unit tests, backend gofmt/vet/build + go test ./internal/system/... ok.
perf(dashboard): refresh every second, bound CPU sampling cost
Poll /dashboard at 1s instead of 10s and make the sampling cheap enough to sustain it.
Frontend:
- fetchDashboard() now takes an AbortSignal.
- Replace setInterval(load, 10000) with startVisiblePolling(load, 1000) so requests are single-flight, cancelled on unmount, and paused while the tab is hidden.
- Swallow aborted requests instead of surfacing them as error toasts.
Backend:
- GetSystemStats() now serves from a 500ms TTL cache. sampleCPU() blocks for 200ms (cpu.Percent), so at 1s cadence an uncached path would burn ~20% of a core on sampling alone; the cache caps it and collapses concurrent requests onto one sample.
- Sampling logic moved to sampleSystemStats(); returned struct is copied so callers cannot mutate the cache.
Verified locally: frontend build + oxlint (0 errors) + 16/16 unit tests, backend gofmt/vet/build + go test ./internal/system/... ok.
perf(dashboard): refresh every second, bound CPU sampling cost
Poll /dashboard at 1s instead of 10s and make the sampling cheap enough to sustain it.
Frontend:
- fetchDashboard() now takes an AbortSignal.
- Replace setInterval(load, 10000) with startVisiblePolling(load, 1000) so requests are single-flight, cancelled on unmount, and paused while the tab is hidden.
- Swallow aborted requests instead of surfacing them as error toasts.
Backend:
- GetSystemStats() now serves from a 500ms TTL cache. sampleCPU() blocks for 200ms (cpu.Percent), so at 1s cadence an uncached path would burn ~20% of a core on sampling alone; the cache caps it and collapses concurrent requests onto one sample.
- Sampling logic moved to sampleSystemStats(); returned struct is copied so callers cannot mutate the cache.
Verified locally: frontend build + oxlint (0 errors) + 16/16 unit tests, backend gofmt/vet/build + go test ./internal/system/... ok.
perf(dashboard): refresh every second, bound CPU sampling cost
Poll /dashboard at 1s instead of 10s and make the sampling cheap enough to sustain it.
Frontend:
- fetchDashboard() now takes an AbortSignal.
- Replace setInterval(load, 10000) with startVisiblePolling(load, 1000) so requests are single-flight, cancelled on unmount, and paused while the tab is hidden.
- Swallow aborted requests instead of surfacing them as error toasts.
Backend:
- GetSystemStats() now serves from a 500ms TTL cache. sampleCPU() blocks for 200ms (cpu.Percent), so at 1s cadence an uncached path would burn ~20% of a core on sampling alone; the cache caps it and collapses concurrent requests onto one sample.
- Sampling logic moved to sampleSystemStats(); returned struct is copied so callers cannot mutate the cache.
Verified locally: frontend build + oxlint (0 errors) + 16/16 unit tests, backend gofmt/vet/build + go test ./internal/system/... ok.
2026-09-15 14:54:24 +08:00
風雪 322f3420ac ui(dashboard): group process usage by process, PID in group header (#76)
The two PIDs sat in the card corner as bare "PID 123  PID 456" with
nothing tying either to the panel or the core - and the core PID was
already shown by the Core Status card.
A PID belongs to a process, not to a metric, so the wall is now two
groups (Panel / Core), each with its own header carrying the name and
PID. Metric labels drop to CPU / Mem since the group supplies the
subject, and the corner overflow is gone.
Adds dashboard.panel / dashboard.core to all 18 locales, each derived
from the wording that locale already uses in panelMem / coreMem.
Co-authored-by: codebuddy <codebuddy@users.noreply.github.com>
ui(dashboard): group process usage by process, PID in group header (#76)
The two PIDs sat in the card corner as bare "PID 123  PID 456" with
nothing tying either to the panel or the core - and the core PID was
already shown by the Core Status card.
A PID belongs to a process, not to a metric, so the wall is now two
groups (Panel / Core), each with its own header carrying the name and
PID. Metric labels drop to CPU / Mem since the group supplies the
subject, and the corner overflow is gone.
Adds dashboard.panel / dashboard.core to all 18 locales, each derived
from the wording that locale already uses in panelMem / coreMem.
Co-authored-by: codebuddy <codebuddy@users.noreply.github.com>
ui(dashboard): group process usage by process, PID in group header (#76)
The two PIDs sat in the card corner as bare "PID 123  PID 456" with
nothing tying either to the panel or the core - and the core PID was
already shown by the Core Status card.
A PID belongs to a process, not to a metric, so the wall is now two
groups (Panel / Core), each with its own header carrying the name and
PID. Metric labels drop to CPU / Mem since the group supplies the
subject, and the corner overflow is gone.
Adds dashboard.panel / dashboard.core to all 18 locales, each derived
from the wording that locale already uses in panelMem / coreMem.
Co-authored-by: codebuddy <codebuddy@users.noreply.github.com>
ui(dashboard): group process usage by process, PID in group header (#76)
The two PIDs sat in the card corner as bare "PID 123  PID 456" with
nothing tying either to the panel or the core - and the core PID was
already shown by the Core Status card.
A PID belongs to a process, not to a metric, so the wall is now two
groups (Panel / Core), each with its own header carrying the name and
PID. Metric labels drop to CPU / Mem since the group supplies the
subject, and the corner overflow is gone.
Adds dashboard.panel / dashboard.core to all 18 locales, each derived
from the wording that locale already uses in panelMem / coreMem.
Co-authored-by: codebuddy <codebuddy@users.noreply.github.com>
ui(dashboard): group process usage by process, PID in group header (#76)
The two PIDs sat in the card corner as bare "PID 123  PID 456" with
nothing tying either to the panel or the core - and the core PID was
already shown by the Core Status card.
A PID belongs to a process, not to a metric, so the wall is now two
groups (Panel / Core), each with its own header carrying the name and
PID. Metric labels drop to CPU / Mem since the group supplies the
subject, and the corner overflow is gone.
Adds dashboard.panel / dashboard.core to all 18 locales, each derived
from the wording that locale already uses in panelMem / coreMem.
Co-authored-by: codebuddy <codebuddy@users.noreply.github.com>
ui(dashboard): group process usage by process, PID in group header (#76)
The two PIDs sat in the card corner as bare "PID 123  PID 456" with
nothing tying either to the panel or the core - and the core PID was
already shown by the Core Status card.
A PID belongs to a process, not to a metric, so the wall is now two
groups (Panel / Core), each with its own header carrying the name and
PID. Metric labels drop to CPU / Mem since the group supplies the
subject, and the corner overflow is gone.
Adds dashboard.panel / dashboard.core to all 18 locales, each derived
from the wording that locale already uses in panelMem / coreMem.
Co-authored-by: codebuddy <codebuddy@users.noreply.github.com>
ui(dashboard): group process usage by process, PID in group header (#76)
The two PIDs sat in the card corner as bare "PID 123  PID 456" with
nothing tying either to the panel or the core - and the core PID was
already shown by the Core Status card.
A PID belongs to a process, not to a metric, so the wall is now two
groups (Panel / Core), each with its own header carrying the name and
PID. Metric labels drop to CPU / Mem since the group supplies the
subject, and the corner overflow is gone.
Adds dashboard.panel / dashboard.core to all 18 locales, each derived
from the wording that locale already uses in panelMem / coreMem.
Co-authored-by: codebuddy <codebuddy@users.noreply.github.com>
ui(dashboard): group process usage by process, PID in group header (#76)
The two PIDs sat in the card corner as bare "PID 123  PID 456" with
nothing tying either to the panel or the core - and the core PID was
already shown by the Core Status card.
A PID belongs to a process, not to a metric, so the wall is now two
groups (Panel / Core), each with its own header carrying the name and
PID. Metric labels drop to CPU / Mem since the group supplies the
subject, and the corner overflow is gone.
Adds dashboard.panel / dashboard.core to all 18 locales, each derived
from the wording that locale already uses in panelMem / coreMem.
Co-authored-by: codebuddy <codebuddy@users.noreply.github.com>
ui(dashboard): group process usage by process, PID in group header (#76)
The two PIDs sat in the card corner as bare "PID 123  PID 456" with
nothing tying either to the panel or the core - and the core PID was
already shown by the Core Status card.
A PID belongs to a process, not to a metric, so the wall is now two
groups (Panel / Core), each with its own header carrying the name and
PID. Metric labels drop to CPU / Mem since the group supplies the
subject, and the corner overflow is gone.
Adds dashboard.panel / dashboard.core to all 18 locales, each derived
from the wording that locale already uses in panelMem / coreMem.
Co-authored-by: codebuddy <codebuddy@users.noreply.github.com>
ui(dashboard): group process usage by process, PID in group header (#76)
The two PIDs sat in the card corner as bare "PID 123  PID 456" with
nothing tying either to the panel or the core - and the core PID was
already shown by the Core Status card.
A PID belongs to a process, not to a metric, so the wall is now two
groups (Panel / Core), each with its own header carrying the name and
PID. Metric labels drop to CPU / Mem since the group supplies the
subject, and the corner overflow is gone.
Adds dashboard.panel / dashboard.core to all 18 locales, each derived
from the wording that locale already uses in panelMem / coreMem.
Co-authored-by: codebuddy <codebuddy@users.noreply.github.com>
ui(dashboard): group process usage by process, PID in group header (#76)
The two PIDs sat in the card corner as bare "PID 123  PID 456" with
nothing tying either to the panel or the core - and the core PID was
already shown by the Core Status card.
A PID belongs to a process, not to a metric, so the wall is now two
groups (Panel / Core), each with its own header carrying the name and
PID. Metric labels drop to CPU / Mem since the group supplies the
subject, and the corner overflow is gone.
Adds dashboard.panel / dashboard.core to all 18 locales, each derived
from the wording that locale already uses in panelMem / coreMem.
Co-authored-by: codebuddy <codebuddy@users.noreply.github.com>
ui(dashboard): group process usage by process, PID in group header (#76)
The two PIDs sat in the card corner as bare "PID 123  PID 456" with
nothing tying either to the panel or the core - and the core PID was
already shown by the Core Status card.
A PID belongs to a process, not to a metric, so the wall is now two
groups (Panel / Core), each with its own header carrying the name and
PID. Metric labels drop to CPU / Mem since the group supplies the
subject, and the corner overflow is gone.
Adds dashboard.panel / dashboard.core to all 18 locales, each derived
from the wording that locale already uses in panelMem / coreMem.
Co-authored-by: codebuddy <codebuddy@users.noreply.github.com>
2026-09-15 14:28:52 +08:00
風雪 2172985aa2 ui(dashboard): make process usage wall lead with percentage (#75)
The memory cells showed RSS ("256 MB") as the big number while the colour
came from memory-percent - a red figure with no visible reason - sitting
next to CPU cells that showed a bare percentage. Mixed units at identical
weight made the wall hard to read.
Every cell now leads with the percentage it is coloured by; absolute RSS
moves to a neutral sub-line. Cells without a sample render "-" instead of
a misleading 0%, and stopped cells keep their own label because the card
header already carries the running/stopped state.
Column separators now follow the Col breakpoints via Grid.useBreakpoint(),
so a 2x2 mobile row no longer draws a stray border on its right edge.
Co-authored-by: codebuddy <codebuddy@users.noreply.github.com>
ui(dashboard): make process usage wall lead with percentage (#75)
The memory cells showed RSS ("256 MB") as the big number while the colour
came from memory-percent - a red figure with no visible reason - sitting
next to CPU cells that showed a bare percentage. Mixed units at identical
weight made the wall hard to read.
Every cell now leads with the percentage it is coloured by; absolute RSS
moves to a neutral sub-line. Cells without a sample render "-" instead of
a misleading 0%, and stopped cells keep their own label because the card
header already carries the running/stopped state.
Column separators now follow the Col breakpoints via Grid.useBreakpoint(),
so a 2x2 mobile row no longer draws a stray border on its right edge.
Co-authored-by: codebuddy <codebuddy@users.noreply.github.com>
ui(dashboard): make process usage wall lead with percentage (#75)
The memory cells showed RSS ("256 MB") as the big number while the colour
came from memory-percent - a red figure with no visible reason - sitting
next to CPU cells that showed a bare percentage. Mixed units at identical
weight made the wall hard to read.
Every cell now leads with the percentage it is coloured by; absolute RSS
moves to a neutral sub-line. Cells without a sample render "-" instead of
a misleading 0%, and stopped cells keep their own label because the card
header already carries the running/stopped state.
Column separators now follow the Col breakpoints via Grid.useBreakpoint(),
so a 2x2 mobile row no longer draws a stray border on its right edge.
Co-authored-by: codebuddy <codebuddy@users.noreply.github.com>
ui(dashboard): make process usage wall lead with percentage (#75)
The memory cells showed RSS ("256 MB") as the big number while the colour
came from memory-percent - a red figure with no visible reason - sitting
next to CPU cells that showed a bare percentage. Mixed units at identical
weight made the wall hard to read.
Every cell now leads with the percentage it is coloured by; absolute RSS
moves to a neutral sub-line. Cells without a sample render "-" instead of
a misleading 0%, and stopped cells keep their own label because the card
header already carries the running/stopped state.
Column separators now follow the Col breakpoints via Grid.useBreakpoint(),
so a 2x2 mobile row no longer draws a stray border on its right edge.
Co-authored-by: codebuddy <codebuddy@users.noreply.github.com>
ui(dashboard): make process usage wall lead with percentage (#75)
The memory cells showed RSS ("256 MB") as the big number while the colour
came from memory-percent - a red figure with no visible reason - sitting
next to CPU cells that showed a bare percentage. Mixed units at identical
weight made the wall hard to read.
Every cell now leads with the percentage it is coloured by; absolute RSS
moves to a neutral sub-line. Cells without a sample render "-" instead of
a misleading 0%, and stopped cells keep their own label because the card
header already carries the running/stopped state.
Column separators now follow the Col breakpoints via Grid.useBreakpoint(),
so a 2x2 mobile row no longer draws a stray border on its right edge.
Co-authored-by: codebuddy <codebuddy@users.noreply.github.com>
ui(dashboard): make process usage wall lead with percentage (#75)
The memory cells showed RSS ("256 MB") as the big number while the colour
came from memory-percent - a red figure with no visible reason - sitting
next to CPU cells that showed a bare percentage. Mixed units at identical
weight made the wall hard to read.
Every cell now leads with the percentage it is coloured by; absolute RSS
moves to a neutral sub-line. Cells without a sample render "-" instead of
a misleading 0%, and stopped cells keep their own label because the card
header already carries the running/stopped state.
Column separators now follow the Col breakpoints via Grid.useBreakpoint(),
so a 2x2 mobile row no longer draws a stray border on its right edge.
Co-authored-by: codebuddy <codebuddy@users.noreply.github.com>
ui(dashboard): make process usage wall lead with percentage (#75)
The memory cells showed RSS ("256 MB") as the big number while the colour
came from memory-percent - a red figure with no visible reason - sitting
next to CPU cells that showed a bare percentage. Mixed units at identical
weight made the wall hard to read.
Every cell now leads with the percentage it is coloured by; absolute RSS
moves to a neutral sub-line. Cells without a sample render "-" instead of
a misleading 0%, and stopped cells keep their own label because the card
header already carries the running/stopped state.
Column separators now follow the Col breakpoints via Grid.useBreakpoint(),
so a 2x2 mobile row no longer draws a stray border on its right edge.
Co-authored-by: codebuddy <codebuddy@users.noreply.github.com>
ui(dashboard): make process usage wall lead with percentage (#75)
The memory cells showed RSS ("256 MB") as the big number while the colour
came from memory-percent - a red figure with no visible reason - sitting
next to CPU cells that showed a bare percentage. Mixed units at identical
weight made the wall hard to read.
Every cell now leads with the percentage it is coloured by; absolute RSS
moves to a neutral sub-line. Cells without a sample render "-" instead of
a misleading 0%, and stopped cells keep their own label because the card
header already carries the running/stopped state.
Column separators now follow the Col breakpoints via Grid.useBreakpoint(),
so a 2x2 mobile row no longer draws a stray border on its right edge.
Co-authored-by: codebuddy <codebuddy@users.noreply.github.com>
ui(dashboard): make process usage wall lead with percentage (#75)
The memory cells showed RSS ("256 MB") as the big number while the colour
came from memory-percent - a red figure with no visible reason - sitting
next to CPU cells that showed a bare percentage. Mixed units at identical
weight made the wall hard to read.
Every cell now leads with the percentage it is coloured by; absolute RSS
moves to a neutral sub-line. Cells without a sample render "-" instead of
a misleading 0%, and stopped cells keep their own label because the card
header already carries the running/stopped state.
Column separators now follow the Col breakpoints via Grid.useBreakpoint(),
so a 2x2 mobile row no longer draws a stray border on its right edge.
Co-authored-by: codebuddy <codebuddy@users.noreply.github.com>
ui(dashboard): make process usage wall lead with percentage (#75)
The memory cells showed RSS ("256 MB") as the big number while the colour
came from memory-percent - a red figure with no visible reason - sitting
next to CPU cells that showed a bare percentage. Mixed units at identical
weight made the wall hard to read.
Every cell now leads with the percentage it is coloured by; absolute RSS
moves to a neutral sub-line. Cells without a sample render "-" instead of
a misleading 0%, and stopped cells keep their own label because the card
header already carries the running/stopped state.
Column separators now follow the Col breakpoints via Grid.useBreakpoint(),
so a 2x2 mobile row no longer draws a stray border on its right edge.
Co-authored-by: codebuddy <codebuddy@users.noreply.github.com>
ui(dashboard): make process usage wall lead with percentage (#75)
The memory cells showed RSS ("256 MB") as the big number while the colour
came from memory-percent - a red figure with no visible reason - sitting
next to CPU cells that showed a bare percentage. Mixed units at identical
weight made the wall hard to read.
Every cell now leads with the percentage it is coloured by; absolute RSS
moves to a neutral sub-line. Cells without a sample render "-" instead of
a misleading 0%, and stopped cells keep their own label because the card
header already carries the running/stopped state.
Column separators now follow the Col breakpoints via Grid.useBreakpoint(),
so a 2x2 mobile row no longer draws a stray border on its right edge.
Co-authored-by: codebuddy <codebuddy@users.noreply.github.com>
ui(dashboard): make process usage wall lead with percentage (#75)
The memory cells showed RSS ("256 MB") as the big number while the colour
came from memory-percent - a red figure with no visible reason - sitting
next to CPU cells that showed a bare percentage. Mixed units at identical
weight made the wall hard to read.
Every cell now leads with the percentage it is coloured by; absolute RSS
moves to a neutral sub-line. Cells without a sample render "-" instead of
a misleading 0%, and stopped cells keep their own label because the card
header already carries the running/stopped state.
Column separators now follow the Col breakpoints via Grid.useBreakpoint(),
so a 2x2 mobile row no longer draws a stray border on its right edge.
Co-authored-by: codebuddy <codebuddy@users.noreply.github.com>
2026-09-15 14:08:56 +08:00
風雪 5c3af9c178 ui(dashboard): compact host resource card with colour-coded usage (#74)
CPU / memory / disk previously occupied three separate cards (three grid
slots, three title bars) and only turned red above 90%. Collapse them into
a single 'System Resources' card with slim bars, and colour by tier:
green below 50%, orange from 50%, red from 80% - matching the thresholds
already used by the process usage wall.
Co-authored-by: codebuddy <codebuddy@users.noreply.github.com>
ui(dashboard): compact host resource card with colour-coded usage (#74)
CPU / memory / disk previously occupied three separate cards (three grid
slots, three title bars) and only turned red above 90%. Collapse them into
a single 'System Resources' card with slim bars, and colour by tier:
green below 50%, orange from 50%, red from 80% - matching the thresholds
already used by the process usage wall.
Co-authored-by: codebuddy <codebuddy@users.noreply.github.com>
ui(dashboard): compact host resource card with colour-coded usage (#74)
CPU / memory / disk previously occupied three separate cards (three grid
slots, three title bars) and only turned red above 90%. Collapse them into
a single 'System Resources' card with slim bars, and colour by tier:
green below 50%, orange from 50%, red from 80% - matching the thresholds
already used by the process usage wall.
Co-authored-by: codebuddy <codebuddy@users.noreply.github.com>
ui(dashboard): compact host resource card with colour-coded usage (#74)
CPU / memory / disk previously occupied three separate cards (three grid
slots, three title bars) and only turned red above 90%. Collapse them into
a single 'System Resources' card with slim bars, and colour by tier:
green below 50%, orange from 50%, red from 80% - matching the thresholds
already used by the process usage wall.
Co-authored-by: codebuddy <codebuddy@users.noreply.github.com>
ui(dashboard): compact host resource card with colour-coded usage (#74)
CPU / memory / disk previously occupied three separate cards (three grid
slots, three title bars) and only turned red above 90%. Collapse them into
a single 'System Resources' card with slim bars, and colour by tier:
green below 50%, orange from 50%, red from 80% - matching the thresholds
already used by the process usage wall.
Co-authored-by: codebuddy <codebuddy@users.noreply.github.com>
ui(dashboard): compact host resource card with colour-coded usage (#74)
CPU / memory / disk previously occupied three separate cards (three grid
slots, three title bars) and only turned red above 90%. Collapse them into
a single 'System Resources' card with slim bars, and colour by tier:
green below 50%, orange from 50%, red from 80% - matching the thresholds
already used by the process usage wall.
Co-authored-by: codebuddy <codebuddy@users.noreply.github.com>
ui(dashboard): compact host resource card with colour-coded usage (#74)
CPU / memory / disk previously occupied three separate cards (three grid
slots, three title bars) and only turned red above 90%. Collapse them into
a single 'System Resources' card with slim bars, and colour by tier:
green below 50%, orange from 50%, red from 80% - matching the thresholds
already used by the process usage wall.
Co-authored-by: codebuddy <codebuddy@users.noreply.github.com>
ui(dashboard): compact host resource card with colour-coded usage (#74)
CPU / memory / disk previously occupied three separate cards (three grid
slots, three title bars) and only turned red above 90%. Collapse them into
a single 'System Resources' card with slim bars, and colour by tier:
green below 50%, orange from 50%, red from 80% - matching the thresholds
already used by the process usage wall.
Co-authored-by: codebuddy <codebuddy@users.noreply.github.com>
ui(dashboard): compact host resource card with colour-coded usage (#74)
CPU / memory / disk previously occupied three separate cards (three grid
slots, three title bars) and only turned red above 90%. Collapse them into
a single 'System Resources' card with slim bars, and colour by tier:
green below 50%, orange from 50%, red from 80% - matching the thresholds
already used by the process usage wall.
Co-authored-by: codebuddy <codebuddy@users.noreply.github.com>
ui(dashboard): compact host resource card with colour-coded usage (#74)
CPU / memory / disk previously occupied three separate cards (three grid
slots, three title bars) and only turned red above 90%. Collapse them into
a single 'System Resources' card with slim bars, and colour by tier:
green below 50%, orange from 50%, red from 80% - matching the thresholds
already used by the process usage wall.
Co-authored-by: codebuddy <codebuddy@users.noreply.github.com>
ui(dashboard): compact host resource card with colour-coded usage (#74)
CPU / memory / disk previously occupied three separate cards (three grid
slots, three title bars) and only turned red above 90%. Collapse them into
a single 'System Resources' card with slim bars, and colour by tier:
green below 50%, orange from 50%, red from 80% - matching the thresholds
already used by the process usage wall.
Co-authored-by: codebuddy <codebuddy@users.noreply.github.com>
ui(dashboard): compact host resource card with colour-coded usage (#74)
CPU / memory / disk previously occupied three separate cards (three grid
slots, three title bars) and only turned red above 90%. Collapse them into
a single 'System Resources' card with slim bars, and colour by tier:
green below 50%, orange from 50%, red from 80% - matching the thresholds
already used by the process usage wall.
Co-authored-by: codebuddy <codebuddy@users.noreply.github.com>
2026-09-15 13:38:22 +08:00
風雪 910d36fb3e i18n: complete dashboard panel/core process usage strings for all locales
Keys: panelUsage, coreUsage, processCPU, processMemory (16 languages; en/zh-CN already present; zh re-exports zh-CN).
i18n: complete dashboard panel/core process usage strings for all locales
Keys: panelUsage, coreUsage, processCPU, processMemory (16 languages; en/zh-CN already present; zh re-exports zh-CN).
i18n: complete dashboard panel/core process usage strings for all locales
Keys: panelUsage, coreUsage, processCPU, processMemory (16 languages; en/zh-CN already present; zh re-exports zh-CN).
i18n: complete dashboard panel/core process usage strings for all locales
Keys: panelUsage, coreUsage, processCPU, processMemory (16 languages; en/zh-CN already present; zh re-exports zh-CN).
i18n: complete dashboard panel/core process usage strings for all locales
Keys: panelUsage, coreUsage, processCPU, processMemory (16 languages; en/zh-CN already present; zh re-exports zh-CN).
i18n: complete dashboard panel/core process usage strings for all locales
Keys: panelUsage, coreUsage, processCPU, processMemory (16 languages; en/zh-CN already present; zh re-exports zh-CN).
i18n: complete dashboard panel/core process usage strings for all locales
Keys: panelUsage, coreUsage, processCPU, processMemory (16 languages; en/zh-CN already present; zh re-exports zh-CN).
i18n: complete dashboard panel/core process usage strings for all locales
Keys: panelUsage, coreUsage, processCPU, processMemory (16 languages; en/zh-CN already present; zh re-exports zh-CN).
i18n: complete dashboard panel/core process usage strings for all locales
Keys: panelUsage, coreUsage, processCPU, processMemory (16 languages; en/zh-CN already present; zh re-exports zh-CN).
i18n: complete dashboard panel/core process usage strings for all locales
Keys: panelUsage, coreUsage, processCPU, processMemory (16 languages; en/zh-CN already present; zh re-exports zh-CN).
i18n: complete dashboard panel/core process usage strings for all locales
Keys: panelUsage, coreUsage, processCPU, processMemory (16 languages; en/zh-CN already present; zh re-exports zh-CN).
i18n: complete dashboard panel/core process usage strings for all locales
Keys: panelUsage, coreUsage, processCPU, processMemory (16 languages; en/zh-CN already present; zh re-exports zh-CN).
2026-09-15 12:07:18 +08:00
風雪 9d33038afd docs+ui: dashboard process usage OpenAPI and responsive cards
Polish panel/core usage cards for mobile and desktop; document GET /dashboard panel and core ProcessUsage in OpenAPI.
docs+ui: dashboard process usage OpenAPI and responsive cards
Polish panel/core usage cards for mobile and desktop; document GET /dashboard panel and core ProcessUsage in OpenAPI.
docs+ui: dashboard process usage OpenAPI and responsive cards
Polish panel/core usage cards for mobile and desktop; document GET /dashboard panel and core ProcessUsage in OpenAPI.
docs+ui: dashboard process usage OpenAPI and responsive cards
Polish panel/core usage cards for mobile and desktop; document GET /dashboard panel and core ProcessUsage in OpenAPI.
docs+ui: dashboard process usage OpenAPI and responsive cards
Polish panel/core usage cards for mobile and desktop; document GET /dashboard panel and core ProcessUsage in OpenAPI.
docs+ui: dashboard process usage OpenAPI and responsive cards
Polish panel/core usage cards for mobile and desktop; document GET /dashboard panel and core ProcessUsage in OpenAPI.
docs+ui: dashboard process usage OpenAPI and responsive cards
Polish panel/core usage cards for mobile and desktop; document GET /dashboard panel and core ProcessUsage in OpenAPI.
docs+ui: dashboard process usage OpenAPI and responsive cards
Polish panel/core usage cards for mobile and desktop; document GET /dashboard panel and core ProcessUsage in OpenAPI.
docs+ui: dashboard process usage OpenAPI and responsive cards
Polish panel/core usage cards for mobile and desktop; document GET /dashboard panel and core ProcessUsage in OpenAPI.
docs+ui: dashboard process usage OpenAPI and responsive cards
Polish panel/core usage cards for mobile and desktop; document GET /dashboard panel and core ProcessUsage in OpenAPI.
docs+ui: dashboard process usage OpenAPI and responsive cards
Polish panel/core usage cards for mobile and desktop; document GET /dashboard panel and core ProcessUsage in OpenAPI.
docs+ui: dashboard process usage OpenAPI and responsive cards
Polish panel/core usage cards for mobile and desktop; document GET /dashboard panel and core ProcessUsage in OpenAPI.
2026-09-15 12:04:41 +08:00
風雪 d25f3d144d feat(dashboard): show panel and Mihomo core process CPU/memory usage
GET /dashboard includes panel and core process samples (pid, cpu_percent,
memory_used RSS, memory_percent). UI cards on the dashboard page.
feat(dashboard): show panel and Mihomo core process CPU/memory usage
GET /dashboard includes panel and core process samples (pid, cpu_percent,
memory_used RSS, memory_percent). UI cards on the dashboard page.
feat(dashboard): show panel and Mihomo core process CPU/memory usage
GET /dashboard includes panel and core process samples (pid, cpu_percent,
memory_used RSS, memory_percent). UI cards on the dashboard page.
feat(dashboard): show panel and Mihomo core process CPU/memory usage
GET /dashboard includes panel and core process samples (pid, cpu_percent,
memory_used RSS, memory_percent). UI cards on the dashboard page.
feat(dashboard): show panel and Mihomo core process CPU/memory usage
GET /dashboard includes panel and core process samples (pid, cpu_percent,
memory_used RSS, memory_percent). UI cards on the dashboard page.
feat(dashboard): show panel and Mihomo core process CPU/memory usage
GET /dashboard includes panel and core process samples (pid, cpu_percent,
memory_used RSS, memory_percent). UI cards on the dashboard page.
feat(dashboard): show panel and Mihomo core process CPU/memory usage
GET /dashboard includes panel and core process samples (pid, cpu_percent,
memory_used RSS, memory_percent). UI cards on the dashboard page.
feat(dashboard): show panel and Mihomo core process CPU/memory usage
GET /dashboard includes panel and core process samples (pid, cpu_percent,
memory_used RSS, memory_percent). UI cards on the dashboard page.
feat(dashboard): show panel and Mihomo core process CPU/memory usage
GET /dashboard includes panel and core process samples (pid, cpu_percent,
memory_used RSS, memory_percent). UI cards on the dashboard page.
feat(dashboard): show panel and Mihomo core process CPU/memory usage
GET /dashboard includes panel and core process samples (pid, cpu_percent,
memory_used RSS, memory_percent). UI cards on the dashboard page.
feat(dashboard): show panel and Mihomo core process CPU/memory usage
GET /dashboard includes panel and core process samples (pid, cpu_percent,
memory_used RSS, memory_percent). UI cards on the dashboard page.
feat(dashboard): show panel and Mihomo core process CPU/memory usage
GET /dashboard includes panel and core process samples (pid, cpu_percent,
memory_used RSS, memory_percent). UI cards on the dashboard page.
2026-09-15 12:00:32 +08:00
風雪 7f664a5d1a fix(ci): wait for async certstore after listener create in smoke-runtime
Listener TLS PEMs are written during debounced GenerateFinalConfig/ApplyConfig.
Smoke was asserting listener-certs immediately after POST /listeners and failed on amd64/arm64 pre builds.
fix(ci): wait for async certstore after listener create in smoke-runtime
Listener TLS PEMs are written during debounced GenerateFinalConfig/ApplyConfig.
Smoke was asserting listener-certs immediately after POST /listeners and failed on amd64/arm64 pre builds.
fix(ci): wait for async certstore after listener create in smoke-runtime
Listener TLS PEMs are written during debounced GenerateFinalConfig/ApplyConfig.
Smoke was asserting listener-certs immediately after POST /listeners and failed on amd64/arm64 pre builds.
fix(ci): wait for async certstore after listener create in smoke-runtime
Listener TLS PEMs are written during debounced GenerateFinalConfig/ApplyConfig.
Smoke was asserting listener-certs immediately after POST /listeners and failed on amd64/arm64 pre builds.
fix(ci): wait for async certstore after listener create in smoke-runtime
Listener TLS PEMs are written during debounced GenerateFinalConfig/ApplyConfig.
Smoke was asserting listener-certs immediately after POST /listeners and failed on amd64/arm64 pre builds.
fix(ci): wait for async certstore after listener create in smoke-runtime
Listener TLS PEMs are written during debounced GenerateFinalConfig/ApplyConfig.
Smoke was asserting listener-certs immediately after POST /listeners and failed on amd64/arm64 pre builds.
fix(ci): wait for async certstore after listener create in smoke-runtime
Listener TLS PEMs are written during debounced GenerateFinalConfig/ApplyConfig.
Smoke was asserting listener-certs immediately after POST /listeners and failed on amd64/arm64 pre builds.
fix(ci): wait for async certstore after listener create in smoke-runtime
Listener TLS PEMs are written during debounced GenerateFinalConfig/ApplyConfig.
Smoke was asserting listener-certs immediately after POST /listeners and failed on amd64/arm64 pre builds.
fix(ci): wait for async certstore after listener create in smoke-runtime
Listener TLS PEMs are written during debounced GenerateFinalConfig/ApplyConfig.
Smoke was asserting listener-certs immediately after POST /listeners and failed on amd64/arm64 pre builds.
fix(ci): wait for async certstore after listener create in smoke-runtime
Listener TLS PEMs are written during debounced GenerateFinalConfig/ApplyConfig.
Smoke was asserting listener-certs immediately after POST /listeners and failed on amd64/arm64 pre builds.
fix(ci): wait for async certstore after listener create in smoke-runtime
Listener TLS PEMs are written during debounced GenerateFinalConfig/ApplyConfig.
Smoke was asserting listener-certs immediately after POST /listeners and failed on amd64/arm64 pre builds.
fix(ci): wait for async certstore after listener create in smoke-runtime
Listener TLS PEMs are written during debounced GenerateFinalConfig/ApplyConfig.
Smoke was asserting listener-certs immediately after POST /listeners and failed on amd64/arm64 pre builds.
2026-09-15 11:29:07 +08:00
風雪 0ab07066ba fix(listener): do not hold mutation lock during Mihomo ApplyConfig
RegenerateConfig and async flush generated YAML under s.mu then released
the lock before ApplyConfig. Holding the lock across ApplyConfig deadlocked
Create when a concurrent regenerate was blocked in the applier (CI 10m timeout).
fix(listener): do not hold mutation lock during Mihomo ApplyConfig
RegenerateConfig and async flush generated YAML under s.mu then released
the lock before ApplyConfig. Holding the lock across ApplyConfig deadlocked
Create when a concurrent regenerate was blocked in the applier (CI 10m timeout).
fix(listener): do not hold mutation lock during Mihomo ApplyConfig
RegenerateConfig and async flush generated YAML under s.mu then released
the lock before ApplyConfig. Holding the lock across ApplyConfig deadlocked
Create when a concurrent regenerate was blocked in the applier (CI 10m timeout).
fix(listener): do not hold mutation lock during Mihomo ApplyConfig
RegenerateConfig and async flush generated YAML under s.mu then released
the lock before ApplyConfig. Holding the lock across ApplyConfig deadlocked
Create when a concurrent regenerate was blocked in the applier (CI 10m timeout).
fix(listener): do not hold mutation lock during Mihomo ApplyConfig
RegenerateConfig and async flush generated YAML under s.mu then released
the lock before ApplyConfig. Holding the lock across ApplyConfig deadlocked
Create when a concurrent regenerate was blocked in the applier (CI 10m timeout).
fix(listener): do not hold mutation lock during Mihomo ApplyConfig
RegenerateConfig and async flush generated YAML under s.mu then released
the lock before ApplyConfig. Holding the lock across ApplyConfig deadlocked
Create when a concurrent regenerate was blocked in the applier (CI 10m timeout).
fix(listener): do not hold mutation lock during Mihomo ApplyConfig
RegenerateConfig and async flush generated YAML under s.mu then released
the lock before ApplyConfig. Holding the lock across ApplyConfig deadlocked
Create when a concurrent regenerate was blocked in the applier (CI 10m timeout).
fix(listener): do not hold mutation lock during Mihomo ApplyConfig
RegenerateConfig and async flush generated YAML under s.mu then released
the lock before ApplyConfig. Holding the lock across ApplyConfig deadlocked
Create when a concurrent regenerate was blocked in the applier (CI 10m timeout).
fix(listener): do not hold mutation lock during Mihomo ApplyConfig
RegenerateConfig and async flush generated YAML under s.mu then released
the lock before ApplyConfig. Holding the lock across ApplyConfig deadlocked
Create when a concurrent regenerate was blocked in the applier (CI 10m timeout).
fix(listener): do not hold mutation lock during Mihomo ApplyConfig
RegenerateConfig and async flush generated YAML under s.mu then released
the lock before ApplyConfig. Holding the lock across ApplyConfig deadlocked
Create when a concurrent regenerate was blocked in the applier (CI 10m timeout).
fix(listener): do not hold mutation lock during Mihomo ApplyConfig
RegenerateConfig and async flush generated YAML under s.mu then released
the lock before ApplyConfig. Holding the lock across ApplyConfig deadlocked
Create when a concurrent regenerate was blocked in the applier (CI 10m timeout).
fix(listener): do not hold mutation lock during Mihomo ApplyConfig
RegenerateConfig and async flush generated YAML under s.mu then released
the lock before ApplyConfig. Holding the lock across ApplyConfig deadlocked
Create when a concurrent regenerate was blocked in the applier (CI 10m timeout).
2026-09-15 10:43:55 +08:00
風雪 534eacb0c3 docs(openapi): document async Mihomo apply and POST /nodes/quick
Align OpenAPI with listener/user create: DB write returns first, ApplyConfig is debounced. Document one-click quick create.
docs(openapi): document async Mihomo apply and POST /nodes/quick
Align OpenAPI with listener/user create: DB write returns first, ApplyConfig is debounced. Document one-click quick create.
docs(openapi): document async Mihomo apply and POST /nodes/quick
Align OpenAPI with listener/user create: DB write returns first, ApplyConfig is debounced. Document one-click quick create.
docs(openapi): document async Mihomo apply and POST /nodes/quick
Align OpenAPI with listener/user create: DB write returns first, ApplyConfig is debounced. Document one-click quick create.
docs(openapi): document async Mihomo apply and POST /nodes/quick
Align OpenAPI with listener/user create: DB write returns first, ApplyConfig is debounced. Document one-click quick create.
docs(openapi): document async Mihomo apply and POST /nodes/quick
Align OpenAPI with listener/user create: DB write returns first, ApplyConfig is debounced. Document one-click quick create.
docs(openapi): document async Mihomo apply and POST /nodes/quick
Align OpenAPI with listener/user create: DB write returns first, ApplyConfig is debounced. Document one-click quick create.
docs(openapi): document async Mihomo apply and POST /nodes/quick
Align OpenAPI with listener/user create: DB write returns first, ApplyConfig is debounced. Document one-click quick create.
docs(openapi): document async Mihomo apply and POST /nodes/quick
Align OpenAPI with listener/user create: DB write returns first, ApplyConfig is debounced. Document one-click quick create.
docs(openapi): document async Mihomo apply and POST /nodes/quick
Align OpenAPI with listener/user create: DB write returns first, ApplyConfig is debounced. Document one-click quick create.
docs(openapi): document async Mihomo apply and POST /nodes/quick
Align OpenAPI with listener/user create: DB write returns first, ApplyConfig is debounced. Document one-click quick create.
docs(openapi): document async Mihomo apply and POST /nodes/quick
Align OpenAPI with listener/user create: DB write returns first, ApplyConfig is debounced. Document one-click quick create.
2026-09-15 10:32:38 +08:00
風雪 36c018d4ff fix(listener): async debounced Mihomo apply after node mutations
Match user-create: return after DB write so the browser is not blocked on
ApplyConfig/restart. Schedule a 400ms debounced config reload; failures are
logged without rolling back the panel DB. Explicit RegenerateConfig stays sync.
fix(listener): async debounced Mihomo apply after node mutations
Match user-create: return after DB write so the browser is not blocked on
ApplyConfig/restart. Schedule a 400ms debounced config reload; failures are
logged without rolling back the panel DB. Explicit RegenerateConfig stays sync.
fix(listener): async debounced Mihomo apply after node mutations
Match user-create: return after DB write so the browser is not blocked on
ApplyConfig/restart. Schedule a 400ms debounced config reload; failures are
logged without rolling back the panel DB. Explicit RegenerateConfig stays sync.
fix(listener): async debounced Mihomo apply after node mutations
Match user-create: return after DB write so the browser is not blocked on
ApplyConfig/restart. Schedule a 400ms debounced config reload; failures are
logged without rolling back the panel DB. Explicit RegenerateConfig stays sync.
fix(listener): async debounced Mihomo apply after node mutations
Match user-create: return after DB write so the browser is not blocked on
ApplyConfig/restart. Schedule a 400ms debounced config reload; failures are
logged without rolling back the panel DB. Explicit RegenerateConfig stays sync.
fix(listener): async debounced Mihomo apply after node mutations
Match user-create: return after DB write so the browser is not blocked on
ApplyConfig/restart. Schedule a 400ms debounced config reload; failures are
logged without rolling back the panel DB. Explicit RegenerateConfig stays sync.
fix(listener): async debounced Mihomo apply after node mutations
Match user-create: return after DB write so the browser is not blocked on
ApplyConfig/restart. Schedule a 400ms debounced config reload; failures are
logged without rolling back the panel DB. Explicit RegenerateConfig stays sync.
fix(listener): async debounced Mihomo apply after node mutations
Match user-create: return after DB write so the browser is not blocked on
ApplyConfig/restart. Schedule a 400ms debounced config reload; failures are
logged without rolling back the panel DB. Explicit RegenerateConfig stays sync.
fix(listener): async debounced Mihomo apply after node mutations
Match user-create: return after DB write so the browser is not blocked on
ApplyConfig/restart. Schedule a 400ms debounced config reload; failures are
logged without rolling back the panel DB. Explicit RegenerateConfig stays sync.
fix(listener): async debounced Mihomo apply after node mutations
Match user-create: return after DB write so the browser is not blocked on
ApplyConfig/restart. Schedule a 400ms debounced config reload; failures are
logged without rolling back the panel DB. Explicit RegenerateConfig stays sync.
fix(listener): async debounced Mihomo apply after node mutations
Match user-create: return after DB write so the browser is not blocked on
ApplyConfig/restart. Schedule a 400ms debounced config reload; failures are
logged without rolling back the panel DB. Explicit RegenerateConfig stays sync.
fix(listener): async debounced Mihomo apply after node mutations
Match user-create: return after DB write so the browser is not blocked on
ApplyConfig/restart. Schedule a 400ms debounced config reload; failures are
logged without rolling back the panel DB. Explicit RegenerateConfig stays sync.
2026-09-15 10:29:32 +08:00
風雪 200d9790ec docs: add Star History chart to all README languages 2026-09-15 10:12:09 +08:00
風雪 d62d776484 fix(frontend): recover create/delete when panel response drops during core reload
Create applies Mihomo config under the same request; proxies/timeouts can
cut the HTTP response after the DB write succeeds. Re-list listeners and treat
as success when the node appears (or disappears for delete).
fix(frontend): recover create/delete when panel response drops during core reload
Create applies Mihomo config under the same request; proxies/timeouts can
cut the HTTP response after the DB write succeeds. Re-list listeners and treat
as success when the node appears (or disappears for delete).
fix(frontend): recover create/delete when panel response drops during core reload
Create applies Mihomo config under the same request; proxies/timeouts can
cut the HTTP response after the DB write succeeds. Re-list listeners and treat
as success when the node appears (or disappears for delete).
fix(frontend): recover create/delete when panel response drops during core reload
Create applies Mihomo config under the same request; proxies/timeouts can
cut the HTTP response after the DB write succeeds. Re-list listeners and treat
as success when the node appears (or disappears for delete).
fix(frontend): recover create/delete when panel response drops during core reload
Create applies Mihomo config under the same request; proxies/timeouts can
cut the HTTP response after the DB write succeeds. Re-list listeners and treat
as success when the node appears (or disappears for delete).
fix(frontend): recover create/delete when panel response drops during core reload
Create applies Mihomo config under the same request; proxies/timeouts can
cut the HTTP response after the DB write succeeds. Re-list listeners and treat
as success when the node appears (or disappears for delete).
fix(frontend): recover create/delete when panel response drops during core reload
Create applies Mihomo config under the same request; proxies/timeouts can
cut the HTTP response after the DB write succeeds. Re-list listeners and treat
as success when the node appears (or disappears for delete).
fix(frontend): recover create/delete when panel response drops during core reload
Create applies Mihomo config under the same request; proxies/timeouts can
cut the HTTP response after the DB write succeeds. Re-list listeners and treat
as success when the node appears (or disappears for delete).
fix(frontend): recover create/delete when panel response drops during core reload
Create applies Mihomo config under the same request; proxies/timeouts can
cut the HTTP response after the DB write succeeds. Re-list listeners and treat
as success when the node appears (or disappears for delete).
fix(frontend): recover create/delete when panel response drops during core reload
Create applies Mihomo config under the same request; proxies/timeouts can
cut the HTTP response after the DB write succeeds. Re-list listeners and treat
as success when the node appears (or disappears for delete).
fix(frontend): recover create/delete when panel response drops during core reload
Create applies Mihomo config under the same request; proxies/timeouts can
cut the HTTP response after the DB write succeeds. Re-list listeners and treat
as success when the node appears (or disappears for delete).
fix(frontend): recover create/delete when panel response drops during core reload
Create applies Mihomo config under the same request; proxies/timeouts can
cut the HTTP response after the DB write succeeds. Re-list listeners and treat
as success when the node appears (or disappears for delete).
2026-09-15 10:09:27 +08:00
風雪 5f1479ae24 fix: port conflict auto-retry + stop false logout on delete
- RequireAuth: SQLite busy / non-not-found DB errors → 503 (not 401)
- Create: on endpoint conflict, reallocate free ports (exclude tried)
- ensureEndpointAvailable checks all live listeners, not only enabled
- Frontend: only force /login on real session 401 messages; handle 503
fix: port conflict auto-retry + stop false logout on delete
- RequireAuth: SQLite busy / non-not-found DB errors → 503 (not 401)
- Create: on endpoint conflict, reallocate free ports (exclude tried)
- ensureEndpointAvailable checks all live listeners, not only enabled
- Frontend: only force /login on real session 401 messages; handle 503
fix: port conflict auto-retry + stop false logout on delete
- RequireAuth: SQLite busy / non-not-found DB errors → 503 (not 401)
- Create: on endpoint conflict, reallocate free ports (exclude tried)
- ensureEndpointAvailable checks all live listeners, not only enabled
- Frontend: only force /login on real session 401 messages; handle 503
fix: port conflict auto-retry + stop false logout on delete
- RequireAuth: SQLite busy / non-not-found DB errors → 503 (not 401)
- Create: on endpoint conflict, reallocate free ports (exclude tried)
- ensureEndpointAvailable checks all live listeners, not only enabled
- Frontend: only force /login on real session 401 messages; handle 503
fix: port conflict auto-retry + stop false logout on delete
- RequireAuth: SQLite busy / non-not-found DB errors → 503 (not 401)
- Create: on endpoint conflict, reallocate free ports (exclude tried)
- ensureEndpointAvailable checks all live listeners, not only enabled
- Frontend: only force /login on real session 401 messages; handle 503
fix: port conflict auto-retry + stop false logout on delete
- RequireAuth: SQLite busy / non-not-found DB errors → 503 (not 401)
- Create: on endpoint conflict, reallocate free ports (exclude tried)
- ensureEndpointAvailable checks all live listeners, not only enabled
- Frontend: only force /login on real session 401 messages; handle 503
fix: port conflict auto-retry + stop false logout on delete
- RequireAuth: SQLite busy / non-not-found DB errors → 503 (not 401)
- Create: on endpoint conflict, reallocate free ports (exclude tried)
- ensureEndpointAvailable checks all live listeners, not only enabled
- Frontend: only force /login on real session 401 messages; handle 503
fix: port conflict auto-retry + stop false logout on delete
- RequireAuth: SQLite busy / non-not-found DB errors → 503 (not 401)
- Create: on endpoint conflict, reallocate free ports (exclude tried)
- ensureEndpointAvailable checks all live listeners, not only enabled
- Frontend: only force /login on real session 401 messages; handle 503
fix: port conflict auto-retry + stop false logout on delete
- RequireAuth: SQLite busy / non-not-found DB errors → 503 (not 401)
- Create: on endpoint conflict, reallocate free ports (exclude tried)
- ensureEndpointAvailable checks all live listeners, not only enabled
- Frontend: only force /login on real session 401 messages; handle 503
fix: port conflict auto-retry + stop false logout on delete
- RequireAuth: SQLite busy / non-not-found DB errors → 503 (not 401)
- Create: on endpoint conflict, reallocate free ports (exclude tried)
- ensureEndpointAvailable checks all live listeners, not only enabled
- Frontend: only force /login on real session 401 messages; handle 503
fix: port conflict auto-retry + stop false logout on delete
- RequireAuth: SQLite busy / non-not-found DB errors → 503 (not 401)
- Create: on endpoint conflict, reallocate free ports (exclude tried)
- ensureEndpointAvailable checks all live listeners, not only enabled
- Frontend: only force /login on real session 401 messages; handle 503
fix: port conflict auto-retry + stop false logout on delete
- RequireAuth: SQLite busy / non-not-found DB errors → 503 (not 401)
- Create: on endpoint conflict, reallocate free ports (exclude tried)
- ensureEndpointAvailable checks all live listeners, not only enabled
- Frontend: only force /login on real session 401 messages; handle 503
2026-09-15 10:03:37 +08:00
風雪 2c3ea59d0a i18n(listeners): add quick-create keys for all 18 locales
Keys: quickCreate, quickCreated, quickCreateHint, nameRenamed
Also restores en.ts damaged by a previous partial push.
i18n(listeners): add quick-create keys for all 18 locales
Keys: quickCreate, quickCreated, quickCreateHint, nameRenamed
Also restores en.ts damaged by a previous partial push.
i18n(listeners): add quick-create keys for all 18 locales
Keys: quickCreate, quickCreated, quickCreateHint, nameRenamed
Also restores en.ts damaged by a previous partial push.
i18n(listeners): add quick-create keys for all 18 locales
Keys: quickCreate, quickCreated, quickCreateHint, nameRenamed
Also restores en.ts damaged by a previous partial push.
i18n(listeners): add quick-create keys for all 18 locales
Keys: quickCreate, quickCreated, quickCreateHint, nameRenamed
Also restores en.ts damaged by a previous partial push.
i18n(listeners): add quick-create keys for all 18 locales
Keys: quickCreate, quickCreated, quickCreateHint, nameRenamed
Also restores en.ts damaged by a previous partial push.
i18n(listeners): add quick-create keys for all 18 locales
Keys: quickCreate, quickCreated, quickCreateHint, nameRenamed
Also restores en.ts damaged by a previous partial push.
i18n(listeners): add quick-create keys for all 18 locales
Keys: quickCreate, quickCreated, quickCreateHint, nameRenamed
Also restores en.ts damaged by a previous partial push.
i18n(listeners): add quick-create keys for all 18 locales
Keys: quickCreate, quickCreated, quickCreateHint, nameRenamed
Also restores en.ts damaged by a previous partial push.
i18n(listeners): add quick-create keys for all 18 locales
Keys: quickCreate, quickCreated, quickCreateHint, nameRenamed
Also restores en.ts damaged by a previous partial push.
i18n(listeners): add quick-create keys for all 18 locales
Keys: quickCreate, quickCreated, quickCreateHint, nameRenamed
Also restores en.ts damaged by a previous partial push.
i18n(listeners): add quick-create keys for all 18 locales
Keys: quickCreate, quickCreated, quickCreateHint, nameRenamed
Also restores en.ts damaged by a previous partial push.
2026-09-15 09:28:06 +08:00
風雪 379b402bef i18n(listeners): restore en.ts with quick-create keys
i18n(listeners): restore en.ts with quick-create keys
i18n(listeners): restore en.ts with quick-create keys
i18n(listeners): restore en.ts with quick-create keys
i18n(listeners): restore en.ts with quick-create keys
i18n(listeners): restore en.ts with quick-create keys
i18n(listeners): restore en.ts with quick-create keys
i18n(listeners): restore en.ts with quick-create keys
i18n(listeners): restore en.ts with quick-create keys
i18n(listeners): restore en.ts with quick-create keys
i18n(listeners): restore en.ts with quick-create keys
i18n(listeners): restore en.ts with quick-create keys
2026-09-15 09:24:25 +08:00
風雪 99c8842be7 i18n(listeners): add quick-create keys (en, zh-CN, zh-TW)
i18n(listeners): add quick-create keys (en, zh-CN, zh-TW)
i18n(listeners): add quick-create keys (en, zh-CN, zh-TW)
i18n(listeners): add quick-create keys (en, zh-CN, zh-TW)
i18n(listeners): add quick-create keys (en, zh-CN, zh-TW)
i18n(listeners): add quick-create keys (en, zh-CN, zh-TW)
i18n(listeners): add quick-create keys (en, zh-CN, zh-TW)
i18n(listeners): add quick-create keys (en, zh-CN, zh-TW)
i18n(listeners): add quick-create keys (en, zh-CN, zh-TW)
i18n(listeners): add quick-create keys (en, zh-CN, zh-TW)
i18n(listeners): add quick-create keys (en, zh-CN, zh-TW)
i18n(listeners): add quick-create keys (en, zh-CN, zh-TW)
2026-09-15 09:19:54 +08:00
風雪 e54126b03b test(frontend): fix listener submission harness for name auto-rename
CI failed because onSubmit now calls nextListenerNameAfterConflict on UNIQUE conflicts, but the VM harness did not provide it.
- Inject nextListenerNameAfterConflict, data, setFieldsValue, message.info
- Cover auto-rename success and persistent conflict paths
test(frontend): fix listener submission harness for name auto-rename
CI failed because onSubmit now calls nextListenerNameAfterConflict on UNIQUE conflicts, but the VM harness did not provide it.
- Inject nextListenerNameAfterConflict, data, setFieldsValue, message.info
- Cover auto-rename success and persistent conflict paths
test(frontend): fix listener submission harness for name auto-rename
CI failed because onSubmit now calls nextListenerNameAfterConflict on UNIQUE conflicts, but the VM harness did not provide it.
- Inject nextListenerNameAfterConflict, data, setFieldsValue, message.info
- Cover auto-rename success and persistent conflict paths
test(frontend): fix listener submission harness for name auto-rename
CI failed because onSubmit now calls nextListenerNameAfterConflict on UNIQUE conflicts, but the VM harness did not provide it.
- Inject nextListenerNameAfterConflict, data, setFieldsValue, message.info
- Cover auto-rename success and persistent conflict paths
test(frontend): fix listener submission harness for name auto-rename
CI failed because onSubmit now calls nextListenerNameAfterConflict on UNIQUE conflicts, but the VM harness did not provide it.
- Inject nextListenerNameAfterConflict, data, setFieldsValue, message.info
- Cover auto-rename success and persistent conflict paths
test(frontend): fix listener submission harness for name auto-rename
CI failed because onSubmit now calls nextListenerNameAfterConflict on UNIQUE conflicts, but the VM harness did not provide it.
- Inject nextListenerNameAfterConflict, data, setFieldsValue, message.info
- Cover auto-rename success and persistent conflict paths
test(frontend): fix listener submission harness for name auto-rename
CI failed because onSubmit now calls nextListenerNameAfterConflict on UNIQUE conflicts, but the VM harness did not provide it.
- Inject nextListenerNameAfterConflict, data, setFieldsValue, message.info
- Cover auto-rename success and persistent conflict paths
test(frontend): fix listener submission harness for name auto-rename
CI failed because onSubmit now calls nextListenerNameAfterConflict on UNIQUE conflicts, but the VM harness did not provide it.
- Inject nextListenerNameAfterConflict, data, setFieldsValue, message.info
- Cover auto-rename success and persistent conflict paths
test(frontend): fix listener submission harness for name auto-rename
CI failed because onSubmit now calls nextListenerNameAfterConflict on UNIQUE conflicts, but the VM harness did not provide it.
- Inject nextListenerNameAfterConflict, data, setFieldsValue, message.info
- Cover auto-rename success and persistent conflict paths
test(frontend): fix listener submission harness for name auto-rename
CI failed because onSubmit now calls nextListenerNameAfterConflict on UNIQUE conflicts, but the VM harness did not provide it.
- Inject nextListenerNameAfterConflict, data, setFieldsValue, message.info
- Cover auto-rename success and persistent conflict paths
test(frontend): fix listener submission harness for name auto-rename
CI failed because onSubmit now calls nextListenerNameAfterConflict on UNIQUE conflicts, but the VM harness did not provide it.
- Inject nextListenerNameAfterConflict, data, setFieldsValue, message.info
- Cover auto-rename success and persistent conflict paths
test(frontend): fix listener submission harness for name auto-rename
CI failed because onSubmit now calls nextListenerNameAfterConflict on UNIQUE conflicts, but the VM harness did not provide it.
- Inject nextListenerNameAfterConflict, data, setFieldsValue, message.info
- Cover auto-rename success and persistent conflict paths
2026-09-15 08:59:52 +08:00
風雪 68aafdea86 feat(scripts/3m-ui): add channel stable|pre subcommand
3m-ui channel [stable|pre] [--update] — show/set update channel and optional upgrade.
feat(scripts/3m-ui): add channel stable|pre subcommand
3m-ui channel [stable|pre] [--update] — show/set update channel and optional upgrade.
feat(scripts/3m-ui): add channel stable|pre subcommand
3m-ui channel [stable|pre] [--update] — show/set update channel and optional upgrade.
feat(scripts/3m-ui): add channel stable|pre subcommand
3m-ui channel [stable|pre] [--update] — show/set update channel and optional upgrade.
feat(scripts/3m-ui): add channel stable|pre subcommand
3m-ui channel [stable|pre] [--update] — show/set update channel and optional upgrade.
feat(scripts/3m-ui): add channel stable|pre subcommand
3m-ui channel [stable|pre] [--update] — show/set update channel and optional upgrade.
feat(scripts/3m-ui): add channel stable|pre subcommand
3m-ui channel [stable|pre] [--update] — show/set update channel and optional upgrade.
feat(scripts/3m-ui): add channel stable|pre subcommand
3m-ui channel [stable|pre] [--update] — show/set update channel and optional upgrade.
feat(scripts/3m-ui): add channel stable|pre subcommand
3m-ui channel [stable|pre] [--update] — show/set update channel and optional upgrade.
feat(scripts/3m-ui): add channel stable|pre subcommand
3m-ui channel [stable|pre] [--update] — show/set update channel and optional upgrade.
feat(scripts/3m-ui): add channel stable|pre subcommand
3m-ui channel [stable|pre] [--update] — show/set update channel and optional upgrade.
feat(scripts/3m-ui): add channel stable|pre subcommand
3m-ui channel [stable|pre] [--update] — show/set update channel and optional upgrade.
2026-09-14 23:01:35 +08:00
風雪 f74f708919 fix(listeners): idempotent delete + create name conflict recovery
- Delete of missing id returns success (stale list / double-click)
- Create retries once after reclaiming unique-name race
- UI: optimistic remove on delete; auto-rename on already exists
fix(listeners): idempotent delete + create name conflict recovery
- Delete of missing id returns success (stale list / double-click)
- Create retries once after reclaiming unique-name race
- UI: optimistic remove on delete; auto-rename on already exists
fix(listeners): idempotent delete + create name conflict recovery
- Delete of missing id returns success (stale list / double-click)
- Create retries once after reclaiming unique-name race
- UI: optimistic remove on delete; auto-rename on already exists
fix(listeners): idempotent delete + create name conflict recovery
- Delete of missing id returns success (stale list / double-click)
- Create retries once after reclaiming unique-name race
- UI: optimistic remove on delete; auto-rename on already exists
fix(listeners): idempotent delete + create name conflict recovery
- Delete of missing id returns success (stale list / double-click)
- Create retries once after reclaiming unique-name race
- UI: optimistic remove on delete; auto-rename on already exists
fix(listeners): idempotent delete + create name conflict recovery
- Delete of missing id returns success (stale list / double-click)
- Create retries once after reclaiming unique-name race
- UI: optimistic remove on delete; auto-rename on already exists
fix(listeners): idempotent delete + create name conflict recovery
- Delete of missing id returns success (stale list / double-click)
- Create retries once after reclaiming unique-name race
- UI: optimistic remove on delete; auto-rename on already exists
fix(listeners): idempotent delete + create name conflict recovery
- Delete of missing id returns success (stale list / double-click)
- Create retries once after reclaiming unique-name race
- UI: optimistic remove on delete; auto-rename on already exists
fix(listeners): idempotent delete + create name conflict recovery
- Delete of missing id returns success (stale list / double-click)
- Create retries once after reclaiming unique-name race
- UI: optimistic remove on delete; auto-rename on already exists
fix(listeners): idempotent delete + create name conflict recovery
- Delete of missing id returns success (stale list / double-click)
- Create retries once after reclaiming unique-name race
- UI: optimistic remove on delete; auto-rename on already exists
fix(listeners): idempotent delete + create name conflict recovery
- Delete of missing id returns success (stale list / double-click)
- Create retries once after reclaiming unique-name race
- UI: optimistic remove on delete; auto-rename on already exists
fix(listeners): idempotent delete + create name conflict recovery
- Delete of missing id returns success (stale list / double-click)
- Create retries once after reclaiming unique-name race
- UI: optimistic remove on delete; auto-rename on already exists
2026-09-14 22:53:34 +08:00
風雪 ca3ae28dd2 fix(listeners/users): quick-create REALITY dest + submit locks
- Seed default REALITY dest/server-names for one-click vless/vmess/trojan
(autofill previously failed with "destination is required")
- Prefill dest on full create form for vless defaults
- Quick-create uses useRef lock against double submit
- Reset user form submit lock when opening create/edit
fix(listeners/users): quick-create REALITY dest + submit locks
- Seed default REALITY dest/server-names for one-click vless/vmess/trojan
(autofill previously failed with "destination is required")
- Prefill dest on full create form for vless defaults
- Quick-create uses useRef lock against double submit
- Reset user form submit lock when opening create/edit
fix(listeners/users): quick-create REALITY dest + submit locks
- Seed default REALITY dest/server-names for one-click vless/vmess/trojan
(autofill previously failed with "destination is required")
- Prefill dest on full create form for vless defaults
- Quick-create uses useRef lock against double submit
- Reset user form submit lock when opening create/edit
fix(listeners/users): quick-create REALITY dest + submit locks
- Seed default REALITY dest/server-names for one-click vless/vmess/trojan
(autofill previously failed with "destination is required")
- Prefill dest on full create form for vless defaults
- Quick-create uses useRef lock against double submit
- Reset user form submit lock when opening create/edit
fix(listeners/users): quick-create REALITY dest + submit locks
- Seed default REALITY dest/server-names for one-click vless/vmess/trojan
(autofill previously failed with "destination is required")
- Prefill dest on full create form for vless defaults
- Quick-create uses useRef lock against double submit
- Reset user form submit lock when opening create/edit
fix(listeners/users): quick-create REALITY dest + submit locks
- Seed default REALITY dest/server-names for one-click vless/vmess/trojan
(autofill previously failed with "destination is required")
- Prefill dest on full create form for vless defaults
- Quick-create uses useRef lock against double submit
- Reset user form submit lock when opening create/edit
fix(listeners/users): quick-create REALITY dest + submit locks
- Seed default REALITY dest/server-names for one-click vless/vmess/trojan
(autofill previously failed with "destination is required")
- Prefill dest on full create form for vless defaults
- Quick-create uses useRef lock against double submit
- Reset user form submit lock when opening create/edit
fix(listeners/users): quick-create REALITY dest + submit locks
- Seed default REALITY dest/server-names for one-click vless/vmess/trojan
(autofill previously failed with "destination is required")
- Prefill dest on full create form for vless defaults
- Quick-create uses useRef lock against double submit
- Reset user form submit lock when opening create/edit
fix(listeners/users): quick-create REALITY dest + submit locks
- Seed default REALITY dest/server-names for one-click vless/vmess/trojan
(autofill previously failed with "destination is required")
- Prefill dest on full create form for vless defaults
- Quick-create uses useRef lock against double submit
- Reset user form submit lock when opening create/edit
fix(listeners/users): quick-create REALITY dest + submit locks
- Seed default REALITY dest/server-names for one-click vless/vmess/trojan
(autofill previously failed with "destination is required")
- Prefill dest on full create form for vless defaults
- Quick-create uses useRef lock against double submit
- Reset user form submit lock when opening create/edit
fix(listeners/users): quick-create REALITY dest + submit locks
- Seed default REALITY dest/server-names for one-click vless/vmess/trojan
(autofill previously failed with "destination is required")
- Prefill dest on full create form for vless defaults
- Quick-create uses useRef lock against double submit
- Reset user form submit lock when opening create/edit
fix(listeners/users): quick-create REALITY dest + submit locks
- Seed default REALITY dest/server-names for one-click vless/vmess/trojan
(autofill previously failed with "destination is required")
- Prefill dest on full create form for vless defaults
- Quick-create uses useRef lock against double submit
- Reset user form submit lock when opening create/edit
2026-09-14 22:47:22 +08:00
風雪 e8b1708c8c chore: disable Dependabot (remove dependabot.yml) (#73)
Stops automated version-update and security-update PRs. Dependency bumps
are now handled manually.
Co-authored-by: codebuddy <codebuddy@users.noreply.github.com>
chore: disable Dependabot (remove dependabot.yml) (#73)
Stops automated version-update and security-update PRs. Dependency bumps
are now handled manually.
Co-authored-by: codebuddy <codebuddy@users.noreply.github.com>
chore: disable Dependabot (remove dependabot.yml) (#73)
Stops automated version-update and security-update PRs. Dependency bumps
are now handled manually.
Co-authored-by: codebuddy <codebuddy@users.noreply.github.com>
chore: disable Dependabot (remove dependabot.yml) (#73)
Stops automated version-update and security-update PRs. Dependency bumps
are now handled manually.
Co-authored-by: codebuddy <codebuddy@users.noreply.github.com>
chore: disable Dependabot (remove dependabot.yml) (#73)
Stops automated version-update and security-update PRs. Dependency bumps
are now handled manually.
Co-authored-by: codebuddy <codebuddy@users.noreply.github.com>
chore: disable Dependabot (remove dependabot.yml) (#73)
Stops automated version-update and security-update PRs. Dependency bumps
are now handled manually.
Co-authored-by: codebuddy <codebuddy@users.noreply.github.com>
chore: disable Dependabot (remove dependabot.yml) (#73)
Stops automated version-update and security-update PRs. Dependency bumps
are now handled manually.
Co-authored-by: codebuddy <codebuddy@users.noreply.github.com>
chore: disable Dependabot (remove dependabot.yml) (#73)
Stops automated version-update and security-update PRs. Dependency bumps
are now handled manually.
Co-authored-by: codebuddy <codebuddy@users.noreply.github.com>
chore: disable Dependabot (remove dependabot.yml) (#73)
Stops automated version-update and security-update PRs. Dependency bumps
are now handled manually.
Co-authored-by: codebuddy <codebuddy@users.noreply.github.com>
chore: disable Dependabot (remove dependabot.yml) (#73)
Stops automated version-update and security-update PRs. Dependency bumps
are now handled manually.
Co-authored-by: codebuddy <codebuddy@users.noreply.github.com>
chore: disable Dependabot (remove dependabot.yml) (#73)
Stops automated version-update and security-update PRs. Dependency bumps
are now handled manually.
Co-authored-by: codebuddy <codebuddy@users.noreply.github.com>
chore: disable Dependabot (remove dependabot.yml) (#73)
Stops automated version-update and security-update PRs. Dependency bumps
are now handled manually.
Co-authored-by: codebuddy <codebuddy@users.noreply.github.com>
2026-09-14 14:27:40 +08:00
風雪 7d00aa2bd5 Merge pull request #72 from kazeyukiro/security-fixes/ssrf-and-deps 2026-09-14 14:12:17 +08:00
風雪 b4be3dbc68 Merge pull request #62 from kazeyukiro/security-fixes/ssrf-and-deps 2026-09-14 13:41:14 +08:00
風雪 0c559a9420 Merge pull request #61 from kazeyukiro/codex/3m-ui
Preserve Shadowsocks UDP=false and restrict unverified updater/downloads in management script
Merge pull request #61 from kazeyukiro/codex/3m-ui
Preserve Shadowsocks UDP=false and restrict unverified updater/downloads in management script
Merge pull request #61 from kazeyukiro/codex/3m-ui
Preserve Shadowsocks UDP=false and restrict unverified updater/downloads in management script
Merge pull request #61 from kazeyukiro/codex/3m-ui
Preserve Shadowsocks UDP=false and restrict unverified updater/downloads in management script
Merge pull request #61 from kazeyukiro/codex/3m-ui
Preserve Shadowsocks UDP=false and restrict unverified updater/downloads in management script
Merge pull request #61 from kazeyukiro/codex/3m-ui
Preserve Shadowsocks UDP=false and restrict unverified updater/downloads in management script
Merge pull request #61 from kazeyukiro/codex/3m-ui
Preserve Shadowsocks UDP=false and restrict unverified updater/downloads in management script
Merge pull request #61 from kazeyukiro/codex/3m-ui
Preserve Shadowsocks UDP=false and restrict unverified updater/downloads in management script
Merge pull request #61 from kazeyukiro/codex/3m-ui
Preserve Shadowsocks UDP=false and restrict unverified updater/downloads in management script
Merge pull request #61 from kazeyukiro/codex/3m-ui
Preserve Shadowsocks UDP=false and restrict unverified updater/downloads in management script
Merge pull request #61 from kazeyukiro/codex/3m-ui
Preserve Shadowsocks UDP=false and restrict unverified updater/downloads in management script
Merge pull request #61 from kazeyukiro/codex/3m-ui
Preserve Shadowsocks UDP=false and restrict unverified updater/downloads in management script
2026-09-14 12:34:57 +08:00
風雪 d5629d0247 security: remove unverified script downloads
security: remove unverified script downloads
security: remove unverified script downloads
security: remove unverified script downloads
security: remove unverified script downloads
security: remove unverified script downloads
security: remove unverified script downloads
security: remove unverified script downloads
security: remove unverified script downloads
security: remove unverified script downloads
security: remove unverified script downloads
security: remove unverified script downloads
2026-09-14 12:31:46 +08:00
風雪 0288287aff feat(listeners): one-click create with protocol + name only
Backend POST /nodes/quick allocates a free port, seeds protocol defaults
(REALITY for vless/vmess/trojan, credentials via AutofillListenerDefaults),
enables the listener and regenerates Mihomo config.
Frontend: primary Quick create button + compact modal (mobile/desktop).
feat(listeners): one-click create with protocol + name only
Backend POST /nodes/quick allocates a free port, seeds protocol defaults
(REALITY for vless/vmess/trojan, credentials via AutofillListenerDefaults),
enables the listener and regenerates Mihomo config.
Frontend: primary Quick create button + compact modal (mobile/desktop).
feat(listeners): one-click create with protocol + name only
Backend POST /nodes/quick allocates a free port, seeds protocol defaults
(REALITY for vless/vmess/trojan, credentials via AutofillListenerDefaults),
enables the listener and regenerates Mihomo config.
Frontend: primary Quick create button + compact modal (mobile/desktop).
feat(listeners): one-click create with protocol + name only
Backend POST /nodes/quick allocates a free port, seeds protocol defaults
(REALITY for vless/vmess/trojan, credentials via AutofillListenerDefaults),
enables the listener and regenerates Mihomo config.
Frontend: primary Quick create button + compact modal (mobile/desktop).
feat(listeners): one-click create with protocol + name only
Backend POST /nodes/quick allocates a free port, seeds protocol defaults
(REALITY for vless/vmess/trojan, credentials via AutofillListenerDefaults),
enables the listener and regenerates Mihomo config.
Frontend: primary Quick create button + compact modal (mobile/desktop).
feat(listeners): one-click create with protocol + name only
Backend POST /nodes/quick allocates a free port, seeds protocol defaults
(REALITY for vless/vmess/trojan, credentials via AutofillListenerDefaults),
enables the listener and regenerates Mihomo config.
Frontend: primary Quick create button + compact modal (mobile/desktop).
feat(listeners): one-click create with protocol + name only
Backend POST /nodes/quick allocates a free port, seeds protocol defaults
(REALITY for vless/vmess/trojan, credentials via AutofillListenerDefaults),
enables the listener and regenerates Mihomo config.
Frontend: primary Quick create button + compact modal (mobile/desktop).
feat(listeners): one-click create with protocol + name only
Backend POST /nodes/quick allocates a free port, seeds protocol defaults
(REALITY for vless/vmess/trojan, credentials via AutofillListenerDefaults),
enables the listener and regenerates Mihomo config.
Frontend: primary Quick create button + compact modal (mobile/desktop).
feat(listeners): one-click create with protocol + name only
Backend POST /nodes/quick allocates a free port, seeds protocol defaults
(REALITY for vless/vmess/trojan, credentials via AutofillListenerDefaults),
enables the listener and regenerates Mihomo config.
Frontend: primary Quick create button + compact modal (mobile/desktop).
feat(listeners): one-click create with protocol + name only
Backend POST /nodes/quick allocates a free port, seeds protocol defaults
(REALITY for vless/vmess/trojan, credentials via AutofillListenerDefaults),
enables the listener and regenerates Mihomo config.
Frontend: primary Quick create button + compact modal (mobile/desktop).
feat(listeners): one-click create with protocol + name only
Backend POST /nodes/quick allocates a free port, seeds protocol defaults
(REALITY for vless/vmess/trojan, credentials via AutofillListenerDefaults),
enables the listener and regenerates Mihomo config.
Frontend: primary Quick create button + compact modal (mobile/desktop).
feat(listeners): one-click create with protocol + name only
Backend POST /nodes/quick allocates a free port, seeds protocol defaults
(REALITY for vless/vmess/trojan, credentials via AutofillListenerDefaults),
enables the listener and regenerates Mihomo config.
Frontend: primary Quick create button + compact modal (mobile/desktop).
2026-09-14 12:08:14 +08:00
風雪 0084c4f053 fix(install): always refresh all lifecycle scripts on update
- Force-install 3m-ui-bin, install/update/uninstall.sh, 3m-ui.sh, 3m-ui
- Re-install /usr/local/bin/3m-ui from the same release (no stale entry)
- Log each installed path; verify executables after write
- chmod scripts in prepare_release; document in update.sh
fix(install): always refresh all lifecycle scripts on update
- Force-install 3m-ui-bin, install/update/uninstall.sh, 3m-ui.sh, 3m-ui
- Re-install /usr/local/bin/3m-ui from the same release (no stale entry)
- Log each installed path; verify executables after write
- chmod scripts in prepare_release; document in update.sh
fix(install): always refresh all lifecycle scripts on update
- Force-install 3m-ui-bin, install/update/uninstall.sh, 3m-ui.sh, 3m-ui
- Re-install /usr/local/bin/3m-ui from the same release (no stale entry)
- Log each installed path; verify executables after write
- chmod scripts in prepare_release; document in update.sh
fix(install): always refresh all lifecycle scripts on update
- Force-install 3m-ui-bin, install/update/uninstall.sh, 3m-ui.sh, 3m-ui
- Re-install /usr/local/bin/3m-ui from the same release (no stale entry)
- Log each installed path; verify executables after write
- chmod scripts in prepare_release; document in update.sh
fix(install): always refresh all lifecycle scripts on update
- Force-install 3m-ui-bin, install/update/uninstall.sh, 3m-ui.sh, 3m-ui
- Re-install /usr/local/bin/3m-ui from the same release (no stale entry)
- Log each installed path; verify executables after write
- chmod scripts in prepare_release; document in update.sh
fix(install): always refresh all lifecycle scripts on update
- Force-install 3m-ui-bin, install/update/uninstall.sh, 3m-ui.sh, 3m-ui
- Re-install /usr/local/bin/3m-ui from the same release (no stale entry)
- Log each installed path; verify executables after write
- chmod scripts in prepare_release; document in update.sh
fix(install): always refresh all lifecycle scripts on update
- Force-install 3m-ui-bin, install/update/uninstall.sh, 3m-ui.sh, 3m-ui
- Re-install /usr/local/bin/3m-ui from the same release (no stale entry)
- Log each installed path; verify executables after write
- chmod scripts in prepare_release; document in update.sh
fix(install): always refresh all lifecycle scripts on update
- Force-install 3m-ui-bin, install/update/uninstall.sh, 3m-ui.sh, 3m-ui
- Re-install /usr/local/bin/3m-ui from the same release (no stale entry)
- Log each installed path; verify executables after write
- chmod scripts in prepare_release; document in update.sh
fix(install): always refresh all lifecycle scripts on update
- Force-install 3m-ui-bin, install/update/uninstall.sh, 3m-ui.sh, 3m-ui
- Re-install /usr/local/bin/3m-ui from the same release (no stale entry)
- Log each installed path; verify executables after write
- chmod scripts in prepare_release; document in update.sh
fix(install): always refresh all lifecycle scripts on update
- Force-install 3m-ui-bin, install/update/uninstall.sh, 3m-ui.sh, 3m-ui
- Re-install /usr/local/bin/3m-ui from the same release (no stale entry)
- Log each installed path; verify executables after write
- chmod scripts in prepare_release; document in update.sh
fix(install): always refresh all lifecycle scripts on update
- Force-install 3m-ui-bin, install/update/uninstall.sh, 3m-ui.sh, 3m-ui
- Re-install /usr/local/bin/3m-ui from the same release (no stale entry)
- Log each installed path; verify executables after write
- chmod scripts in prepare_release; document in update.sh
fix(install): always refresh all lifecycle scripts on update
- Force-install 3m-ui-bin, install/update/uninstall.sh, 3m-ui.sh, 3m-ui
- Re-install /usr/local/bin/3m-ui from the same release (no stale entry)
- Log each installed path; verify executables after write
- chmod scripts in prepare_release; document in update.sh
2026-09-14 11:53:36 +08:00
風雪 508b3e30c0 fix(scripts/3m-ui): harden yaml edit, DATA_DIR, logs follow, update_self prompt
- Expand $BASE in update_self confirm
- mkdir DATA_DIR before run_app
- Flexible yaml_get/yaml_set indentation; fail if no server:
- logs -f tolerates Ctrl+C under set -e
- Clearer enable/disable errors
fix(scripts/3m-ui): harden yaml edit, DATA_DIR, logs follow, update_self prompt
- Expand $BASE in update_self confirm
- mkdir DATA_DIR before run_app
- Flexible yaml_get/yaml_set indentation; fail if no server:
- logs -f tolerates Ctrl+C under set -e
- Clearer enable/disable errors
fix(scripts/3m-ui): harden yaml edit, DATA_DIR, logs follow, update_self prompt
- Expand $BASE in update_self confirm
- mkdir DATA_DIR before run_app
- Flexible yaml_get/yaml_set indentation; fail if no server:
- logs -f tolerates Ctrl+C under set -e
- Clearer enable/disable errors
fix(scripts/3m-ui): harden yaml edit, DATA_DIR, logs follow, update_self prompt
- Expand $BASE in update_self confirm
- mkdir DATA_DIR before run_app
- Flexible yaml_get/yaml_set indentation; fail if no server:
- logs -f tolerates Ctrl+C under set -e
- Clearer enable/disable errors
fix(scripts/3m-ui): harden yaml edit, DATA_DIR, logs follow, update_self prompt
- Expand $BASE in update_self confirm
- mkdir DATA_DIR before run_app
- Flexible yaml_get/yaml_set indentation; fail if no server:
- logs -f tolerates Ctrl+C under set -e
- Clearer enable/disable errors
fix(scripts/3m-ui): harden yaml edit, DATA_DIR, logs follow, update_self prompt
- Expand $BASE in update_self confirm
- mkdir DATA_DIR before run_app
- Flexible yaml_get/yaml_set indentation; fail if no server:
- logs -f tolerates Ctrl+C under set -e
- Clearer enable/disable errors
fix(scripts/3m-ui): harden yaml edit, DATA_DIR, logs follow, update_self prompt
- Expand $BASE in update_self confirm
- mkdir DATA_DIR before run_app
- Flexible yaml_get/yaml_set indentation; fail if no server:
- logs -f tolerates Ctrl+C under set -e
- Clearer enable/disable errors
fix(scripts/3m-ui): harden yaml edit, DATA_DIR, logs follow, update_self prompt
- Expand $BASE in update_self confirm
- mkdir DATA_DIR before run_app
- Flexible yaml_get/yaml_set indentation; fail if no server:
- logs -f tolerates Ctrl+C under set -e
- Clearer enable/disable errors
fix(scripts/3m-ui): harden yaml edit, DATA_DIR, logs follow, update_self prompt
- Expand $BASE in update_self confirm
- mkdir DATA_DIR before run_app
- Flexible yaml_get/yaml_set indentation; fail if no server:
- logs -f tolerates Ctrl+C under set -e
- Clearer enable/disable errors
fix(scripts/3m-ui): harden yaml edit, DATA_DIR, logs follow, update_self prompt
- Expand $BASE in update_self confirm
- mkdir DATA_DIR before run_app
- Flexible yaml_get/yaml_set indentation; fail if no server:
- logs -f tolerates Ctrl+C under set -e
- Clearer enable/disable errors
fix(scripts/3m-ui): harden yaml edit, DATA_DIR, logs follow, update_self prompt
- Expand $BASE in update_self confirm
- mkdir DATA_DIR before run_app
- Flexible yaml_get/yaml_set indentation; fail if no server:
- logs -f tolerates Ctrl+C under set -e
- Clearer enable/disable errors
fix(scripts/3m-ui): harden yaml edit, DATA_DIR, logs follow, update_self prompt
- Expand $BASE in update_self confirm
- mkdir DATA_DIR before run_app
- Flexible yaml_get/yaml_set indentation; fail if no server:
- logs -f tolerates Ctrl+C under set -e
- Clearer enable/disable errors
2026-09-14 11:33:08 +08:00
風雪 aa08f34be4 feat(scripts/3m-ui): expand management console with ops features
Reimplemented for 3m-ui (not a copy of third-party scripts):
- enable/disable on boot, follow logs, access URI view
- port / public_url / web_path / sub_path / sub_port editors
- update to version, update this script from repo
- BBR status/enable/disable, Geo, firewall hints
- SSL cert helper (certbot/acme.sh command templates)
- backup entry, healthcheck, confirm + pause menu flow
feat(scripts/3m-ui): expand management console with ops features
Reimplemented for 3m-ui (not a copy of third-party scripts):
- enable/disable on boot, follow logs, access URI view
- port / public_url / web_path / sub_path / sub_port editors
- update to version, update this script from repo
- BBR status/enable/disable, Geo, firewall hints
- SSL cert helper (certbot/acme.sh command templates)
- backup entry, healthcheck, confirm + pause menu flow
feat(scripts/3m-ui): expand management console with ops features
Reimplemented for 3m-ui (not a copy of third-party scripts):
- enable/disable on boot, follow logs, access URI view
- port / public_url / web_path / sub_path / sub_port editors
- update to version, update this script from repo
- BBR status/enable/disable, Geo, firewall hints
- SSL cert helper (certbot/acme.sh command templates)
- backup entry, healthcheck, confirm + pause menu flow
feat(scripts/3m-ui): expand management console with ops features
Reimplemented for 3m-ui (not a copy of third-party scripts):
- enable/disable on boot, follow logs, access URI view
- port / public_url / web_path / sub_path / sub_port editors
- update to version, update this script from repo
- BBR status/enable/disable, Geo, firewall hints
- SSL cert helper (certbot/acme.sh command templates)
- backup entry, healthcheck, confirm + pause menu flow
feat(scripts/3m-ui): expand management console with ops features
Reimplemented for 3m-ui (not a copy of third-party scripts):
- enable/disable on boot, follow logs, access URI view
- port / public_url / web_path / sub_path / sub_port editors
- update to version, update this script from repo
- BBR status/enable/disable, Geo, firewall hints
- SSL cert helper (certbot/acme.sh command templates)
- backup entry, healthcheck, confirm + pause menu flow
feat(scripts/3m-ui): expand management console with ops features
Reimplemented for 3m-ui (not a copy of third-party scripts):
- enable/disable on boot, follow logs, access URI view
- port / public_url / web_path / sub_path / sub_port editors
- update to version, update this script from repo
- BBR status/enable/disable, Geo, firewall hints
- SSL cert helper (certbot/acme.sh command templates)
- backup entry, healthcheck, confirm + pause menu flow
feat(scripts/3m-ui): expand management console with ops features
Reimplemented for 3m-ui (not a copy of third-party scripts):
- enable/disable on boot, follow logs, access URI view
- port / public_url / web_path / sub_path / sub_port editors
- update to version, update this script from repo
- BBR status/enable/disable, Geo, firewall hints
- SSL cert helper (certbot/acme.sh command templates)
- backup entry, healthcheck, confirm + pause menu flow
feat(scripts/3m-ui): expand management console with ops features
Reimplemented for 3m-ui (not a copy of third-party scripts):
- enable/disable on boot, follow logs, access URI view
- port / public_url / web_path / sub_path / sub_port editors
- update to version, update this script from repo
- BBR status/enable/disable, Geo, firewall hints
- SSL cert helper (certbot/acme.sh command templates)
- backup entry, healthcheck, confirm + pause menu flow
feat(scripts/3m-ui): expand management console with ops features
Reimplemented for 3m-ui (not a copy of third-party scripts):
- enable/disable on boot, follow logs, access URI view
- port / public_url / web_path / sub_path / sub_port editors
- update to version, update this script from repo
- BBR status/enable/disable, Geo, firewall hints
- SSL cert helper (certbot/acme.sh command templates)
- backup entry, healthcheck, confirm + pause menu flow
feat(scripts/3m-ui): expand management console with ops features
Reimplemented for 3m-ui (not a copy of third-party scripts):
- enable/disable on boot, follow logs, access URI view
- port / public_url / web_path / sub_path / sub_port editors
- update to version, update this script from repo
- BBR status/enable/disable, Geo, firewall hints
- SSL cert helper (certbot/acme.sh command templates)
- backup entry, healthcheck, confirm + pause menu flow
feat(scripts/3m-ui): expand management console with ops features
Reimplemented for 3m-ui (not a copy of third-party scripts):
- enable/disable on boot, follow logs, access URI view
- port / public_url / web_path / sub_path / sub_port editors
- update to version, update this script from repo
- BBR status/enable/disable, Geo, firewall hints
- SSL cert helper (certbot/acme.sh command templates)
- backup entry, healthcheck, confirm + pause menu flow
feat(scripts/3m-ui): expand management console with ops features
Reimplemented for 3m-ui (not a copy of third-party scripts):
- enable/disable on boot, follow logs, access URI view
- port / public_url / web_path / sub_path / sub_port editors
- update to version, update this script from repo
- BBR status/enable/disable, Geo, firewall hints
- SSL cert helper (certbot/acme.sh command templates)
- backup entry, healthcheck, confirm + pause menu flow
2026-09-14 11:27:17 +08:00
風雪 eadd5255b0 docs(readme): point contributors to GitHub Contributors graph
Replace hard-coded names with link to https://github.com/kazeyukiro/3m-ui/graphs/contributors across all README locales.
docs(readme): point contributors to GitHub Contributors graph
Replace hard-coded names with link to https://github.com/kazeyukiro/3m-ui/graphs/contributors across all README locales.
docs(readme): point contributors to GitHub Contributors graph
Replace hard-coded names with link to https://github.com/kazeyukiro/3m-ui/graphs/contributors across all README locales.
docs(readme): point contributors to GitHub Contributors graph
Replace hard-coded names with link to https://github.com/kazeyukiro/3m-ui/graphs/contributors across all README locales.
docs(readme): point contributors to GitHub Contributors graph
Replace hard-coded names with link to https://github.com/kazeyukiro/3m-ui/graphs/contributors across all README locales.
docs(readme): point contributors to GitHub Contributors graph
Replace hard-coded names with link to https://github.com/kazeyukiro/3m-ui/graphs/contributors across all README locales.
docs(readme): point contributors to GitHub Contributors graph
Replace hard-coded names with link to https://github.com/kazeyukiro/3m-ui/graphs/contributors across all README locales.
docs(readme): point contributors to GitHub Contributors graph
Replace hard-coded names with link to https://github.com/kazeyukiro/3m-ui/graphs/contributors across all README locales.
docs(readme): point contributors to GitHub Contributors graph
Replace hard-coded names with link to https://github.com/kazeyukiro/3m-ui/graphs/contributors across all README locales.
docs(readme): point contributors to GitHub Contributors graph
Replace hard-coded names with link to https://github.com/kazeyukiro/3m-ui/graphs/contributors across all README locales.
docs(readme): point contributors to GitHub Contributors graph
Replace hard-coded names with link to https://github.com/kazeyukiro/3m-ui/graphs/contributors across all README locales.
docs(readme): point contributors to GitHub Contributors graph
Replace hard-coded names with link to https://github.com/kazeyukiro/3m-ui/graphs/contributors across all README locales.
2026-09-14 11:16:49 +08:00
風雪 bcdec99d82 fix(scripts/3m-ui): menu set -e survival, service_is_active, safer config edits
- Menu actions no longer abort the whole console on failure (set -e)
- Use local status/start/stop/logs/version instead of 3m-ui.sh detour
- service_is_active for post-restart checks; help without root
- Safer port/public_url YAML edits (missing field, BusyBox insert, sed escape)
fix(scripts/3m-ui): menu set -e survival, service_is_active, safer config edits
- Menu actions no longer abort the whole console on failure (set -e)
- Use local status/start/stop/logs/version instead of 3m-ui.sh detour
- service_is_active for post-restart checks; help without root
- Safer port/public_url YAML edits (missing field, BusyBox insert, sed escape)
fix(scripts/3m-ui): menu set -e survival, service_is_active, safer config edits
- Menu actions no longer abort the whole console on failure (set -e)
- Use local status/start/stop/logs/version instead of 3m-ui.sh detour
- service_is_active for post-restart checks; help without root
- Safer port/public_url YAML edits (missing field, BusyBox insert, sed escape)
fix(scripts/3m-ui): menu set -e survival, service_is_active, safer config edits
- Menu actions no longer abort the whole console on failure (set -e)
- Use local status/start/stop/logs/version instead of 3m-ui.sh detour
- service_is_active for post-restart checks; help without root
- Safer port/public_url YAML edits (missing field, BusyBox insert, sed escape)
fix(scripts/3m-ui): menu set -e survival, service_is_active, safer config edits
- Menu actions no longer abort the whole console on failure (set -e)
- Use local status/start/stop/logs/version instead of 3m-ui.sh detour
- service_is_active for post-restart checks; help without root
- Safer port/public_url YAML edits (missing field, BusyBox insert, sed escape)
fix(scripts/3m-ui): menu set -e survival, service_is_active, safer config edits
- Menu actions no longer abort the whole console on failure (set -e)
- Use local status/start/stop/logs/version instead of 3m-ui.sh detour
- service_is_active for post-restart checks; help without root
- Safer port/public_url YAML edits (missing field, BusyBox insert, sed escape)
fix(scripts/3m-ui): menu set -e survival, service_is_active, safer config edits
- Menu actions no longer abort the whole console on failure (set -e)
- Use local status/start/stop/logs/version instead of 3m-ui.sh detour
- service_is_active for post-restart checks; help without root
- Safer port/public_url YAML edits (missing field, BusyBox insert, sed escape)
fix(scripts/3m-ui): menu set -e survival, service_is_active, safer config edits
- Menu actions no longer abort the whole console on failure (set -e)
- Use local status/start/stop/logs/version instead of 3m-ui.sh detour
- service_is_active for post-restart checks; help without root
- Safer port/public_url YAML edits (missing field, BusyBox insert, sed escape)
fix(scripts/3m-ui): menu set -e survival, service_is_active, safer config edits
- Menu actions no longer abort the whole console on failure (set -e)
- Use local status/start/stop/logs/version instead of 3m-ui.sh detour
- service_is_active for post-restart checks; help without root
- Safer port/public_url YAML edits (missing field, BusyBox insert, sed escape)
fix(scripts/3m-ui): menu set -e survival, service_is_active, safer config edits
- Menu actions no longer abort the whole console on failure (set -e)
- Use local status/start/stop/logs/version instead of 3m-ui.sh detour
- service_is_active for post-restart checks; help without root
- Safer port/public_url YAML edits (missing field, BusyBox insert, sed escape)
fix(scripts/3m-ui): menu set -e survival, service_is_active, safer config edits
- Menu actions no longer abort the whole console on failure (set -e)
- Use local status/start/stop/logs/version instead of 3m-ui.sh detour
- service_is_active for post-restart checks; help without root
- Safer port/public_url YAML edits (missing field, BusyBox insert, sed escape)
fix(scripts/3m-ui): menu set -e survival, service_is_active, safer config edits
- Menu actions no longer abort the whole console on failure (set -e)
- Use local status/start/stop/logs/version instead of 3m-ui.sh detour
- service_is_active for post-restart checks; help without root
- Safer port/public_url YAML edits (missing field, BusyBox insert, sed escape)
2026-09-14 00:34:16 +08:00
風雪 8cf969abd8 fix(ui): drop Settings minHeight:480 on mobile to avoid empty gap 2026-09-14 00:18:04 +08:00
風雪 8cc49fe7a5 fix(ui): remove mobile full-height white panel gap under page titles
Content area is transparent on mobile; cards sit directly under the title
instead of floating inside a stretched empty white card. Force ant-layout
content flex:none overrides.
fix(ui): remove mobile full-height white panel gap under page titles
Content area is transparent on mobile; cards sit directly under the title
instead of floating inside a stretched empty white card. Force ant-layout
content flex:none overrides.
fix(ui): remove mobile full-height white panel gap under page titles
Content area is transparent on mobile; cards sit directly under the title
instead of floating inside a stretched empty white card. Force ant-layout
content flex:none overrides.
fix(ui): remove mobile full-height white panel gap under page titles
Content area is transparent on mobile; cards sit directly under the title
instead of floating inside a stretched empty white card. Force ant-layout
content flex:none overrides.
fix(ui): remove mobile full-height white panel gap under page titles
Content area is transparent on mobile; cards sit directly under the title
instead of floating inside a stretched empty white card. Force ant-layout
content flex:none overrides.
fix(ui): remove mobile full-height white panel gap under page titles
Content area is transparent on mobile; cards sit directly under the title
instead of floating inside a stretched empty white card. Force ant-layout
content flex:none overrides.
fix(ui): remove mobile full-height white panel gap under page titles
Content area is transparent on mobile; cards sit directly under the title
instead of floating inside a stretched empty white card. Force ant-layout
content flex:none overrides.
fix(ui): remove mobile full-height white panel gap under page titles
Content area is transparent on mobile; cards sit directly under the title
instead of floating inside a stretched empty white card. Force ant-layout
content flex:none overrides.
fix(ui): remove mobile full-height white panel gap under page titles
Content area is transparent on mobile; cards sit directly under the title
instead of floating inside a stretched empty white card. Force ant-layout
content flex:none overrides.
fix(ui): remove mobile full-height white panel gap under page titles
Content area is transparent on mobile; cards sit directly under the title
instead of floating inside a stretched empty white card. Force ant-layout
content flex:none overrides.
fix(ui): remove mobile full-height white panel gap under page titles
Content area is transparent on mobile; cards sit directly under the title
instead of floating inside a stretched empty white card. Force ant-layout
content flex:none overrides.
fix(ui): remove mobile full-height white panel gap under page titles
Content area is transparent on mobile; cards sit directly under the title
instead of floating inside a stretched empty white card. Force ant-layout
content flex:none overrides.
2026-09-14 00:14:00 +08:00
風雪 6739850e97 fix(ui): stop mobile content card from stretching full viewport height
Ant Layout flex was expanding .app-page-content into a tall empty white
block under short pages. Content is now height:auto / flex:0 on mobile;
body safe-area padding removed (handled by header + bottom nav).
fix(ui): stop mobile content card from stretching full viewport height
Ant Layout flex was expanding .app-page-content into a tall empty white
block under short pages. Content is now height:auto / flex:0 on mobile;
body safe-area padding removed (handled by header + bottom nav).
fix(ui): stop mobile content card from stretching full viewport height
Ant Layout flex was expanding .app-page-content into a tall empty white
block under short pages. Content is now height:auto / flex:0 on mobile;
body safe-area padding removed (handled by header + bottom nav).
fix(ui): stop mobile content card from stretching full viewport height
Ant Layout flex was expanding .app-page-content into a tall empty white
block under short pages. Content is now height:auto / flex:0 on mobile;
body safe-area padding removed (handled by header + bottom nav).
fix(ui): stop mobile content card from stretching full viewport height
Ant Layout flex was expanding .app-page-content into a tall empty white
block under short pages. Content is now height:auto / flex:0 on mobile;
body safe-area padding removed (handled by header + bottom nav).
fix(ui): stop mobile content card from stretching full viewport height
Ant Layout flex was expanding .app-page-content into a tall empty white
block under short pages. Content is now height:auto / flex:0 on mobile;
body safe-area padding removed (handled by header + bottom nav).
fix(ui): stop mobile content card from stretching full viewport height
Ant Layout flex was expanding .app-page-content into a tall empty white
block under short pages. Content is now height:auto / flex:0 on mobile;
body safe-area padding removed (handled by header + bottom nav).
fix(ui): stop mobile content card from stretching full viewport height
Ant Layout flex was expanding .app-page-content into a tall empty white
block under short pages. Content is now height:auto / flex:0 on mobile;
body safe-area padding removed (handled by header + bottom nav).
fix(ui): stop mobile content card from stretching full viewport height
Ant Layout flex was expanding .app-page-content into a tall empty white
block under short pages. Content is now height:auto / flex:0 on mobile;
body safe-area padding removed (handled by header + bottom nav).
fix(ui): stop mobile content card from stretching full viewport height
Ant Layout flex was expanding .app-page-content into a tall empty white
block under short pages. Content is now height:auto / flex:0 on mobile;
body safe-area padding removed (handled by header + bottom nav).
fix(ui): stop mobile content card from stretching full viewport height
Ant Layout flex was expanding .app-page-content into a tall empty white
block under short pages. Content is now height:auto / flex:0 on mobile;
body safe-area padding removed (handled by header + bottom nav).
fix(ui): stop mobile content card from stretching full viewport height
Ant Layout flex was expanding .app-page-content into a tall empty white
block under short pages. Content is now height:auto / flex:0 on mobile;
body safe-area padding removed (handled by header + bottom nav).
2026-09-14 00:01:36 +08:00
風雪 efa39ee5ed fix(ui): define isMobile in SharePage for compact layout props 2026-09-13 23:52:26 +08:00
風雪 f1f35da11a fix(ui): close Settings TOTP div; compact mobile layout on all pages
- Repair unclosed JSX in Settings security/TOTP block (CI frontend build)
- Denser mobile CSS: cards, tables, stats, forms, entity lists, bottom nav
- Small card/table density on Dashboard/Traffic/Cluster/Config/etc.
fix(ui): close Settings TOTP div; compact mobile layout on all pages
- Repair unclosed JSX in Settings security/TOTP block (CI frontend build)
- Denser mobile CSS: cards, tables, stats, forms, entity lists, bottom nav
- Small card/table density on Dashboard/Traffic/Cluster/Config/etc.
fix(ui): close Settings TOTP div; compact mobile layout on all pages
- Repair unclosed JSX in Settings security/TOTP block (CI frontend build)
- Denser mobile CSS: cards, tables, stats, forms, entity lists, bottom nav
- Small card/table density on Dashboard/Traffic/Cluster/Config/etc.
fix(ui): close Settings TOTP div; compact mobile layout on all pages
- Repair unclosed JSX in Settings security/TOTP block (CI frontend build)
- Denser mobile CSS: cards, tables, stats, forms, entity lists, bottom nav
- Small card/table density on Dashboard/Traffic/Cluster/Config/etc.
fix(ui): close Settings TOTP div; compact mobile layout on all pages
- Repair unclosed JSX in Settings security/TOTP block (CI frontend build)
- Denser mobile CSS: cards, tables, stats, forms, entity lists, bottom nav
- Small card/table density on Dashboard/Traffic/Cluster/Config/etc.
fix(ui): close Settings TOTP div; compact mobile layout on all pages
- Repair unclosed JSX in Settings security/TOTP block (CI frontend build)
- Denser mobile CSS: cards, tables, stats, forms, entity lists, bottom nav
- Small card/table density on Dashboard/Traffic/Cluster/Config/etc.
fix(ui): close Settings TOTP div; compact mobile layout on all pages
- Repair unclosed JSX in Settings security/TOTP block (CI frontend build)
- Denser mobile CSS: cards, tables, stats, forms, entity lists, bottom nav
- Small card/table density on Dashboard/Traffic/Cluster/Config/etc.
fix(ui): close Settings TOTP div; compact mobile layout on all pages
- Repair unclosed JSX in Settings security/TOTP block (CI frontend build)
- Denser mobile CSS: cards, tables, stats, forms, entity lists, bottom nav
- Small card/table density on Dashboard/Traffic/Cluster/Config/etc.
fix(ui): close Settings TOTP div; compact mobile layout on all pages
- Repair unclosed JSX in Settings security/TOTP block (CI frontend build)
- Denser mobile CSS: cards, tables, stats, forms, entity lists, bottom nav
- Small card/table density on Dashboard/Traffic/Cluster/Config/etc.
fix(ui): close Settings TOTP div; compact mobile layout on all pages
- Repair unclosed JSX in Settings security/TOTP block (CI frontend build)
- Denser mobile CSS: cards, tables, stats, forms, entity lists, bottom nav
- Small card/table density on Dashboard/Traffic/Cluster/Config/etc.
fix(ui): close Settings TOTP div; compact mobile layout on all pages
- Repair unclosed JSX in Settings security/TOTP block (CI frontend build)
- Denser mobile CSS: cards, tables, stats, forms, entity lists, bottom nav
- Small card/table density on Dashboard/Traffic/Cluster/Config/etc.
fix(ui): close Settings TOTP div; compact mobile layout on all pages
- Repair unclosed JSX in Settings security/TOTP block (CI frontend build)
- Denser mobile CSS: cards, tables, stats, forms, entity lists, bottom nav
- Small card/table density on Dashboard/Traffic/Cluster/Config/etc.
2026-09-13 23:49:38 +08:00
風雪 e03861e4f7 i18n: sync nav.more + TOTP UI strings across 18 locales
All panel languages now include bottom-nav "More" and two-factor setup/login copy.
i18n: sync nav.more + TOTP UI strings across 18 locales
All panel languages now include bottom-nav "More" and two-factor setup/login copy.
i18n: sync nav.more + TOTP UI strings across 18 locales
All panel languages now include bottom-nav "More" and two-factor setup/login copy.
i18n: sync nav.more + TOTP UI strings across 18 locales
All panel languages now include bottom-nav "More" and two-factor setup/login copy.
i18n: sync nav.more + TOTP UI strings across 18 locales
All panel languages now include bottom-nav "More" and two-factor setup/login copy.
i18n: sync nav.more + TOTP UI strings across 18 locales
All panel languages now include bottom-nav "More" and two-factor setup/login copy.
i18n: sync nav.more + TOTP UI strings across 18 locales
All panel languages now include bottom-nav "More" and two-factor setup/login copy.
i18n: sync nav.more + TOTP UI strings across 18 locales
All panel languages now include bottom-nav "More" and two-factor setup/login copy.
i18n: sync nav.more + TOTP UI strings across 18 locales
All panel languages now include bottom-nav "More" and two-factor setup/login copy.
i18n: sync nav.more + TOTP UI strings across 18 locales
All panel languages now include bottom-nav "More" and two-factor setup/login copy.
i18n: sync nav.more + TOTP UI strings across 18 locales
All panel languages now include bottom-nav "More" and two-factor setup/login copy.
i18n: sync nav.more + TOTP UI strings across 18 locales
All panel languages now include bottom-nav "More" and two-factor setup/login copy.
2026-09-13 23:38:17 +08:00
風雪 4aefb11b26 ui(mobile): replace side drawer with bottom tab bar
Primary: Dashboard / Listeners / Users / Traffic; More sheet for the rest + logout.
Header shows logo on mobile; content padded for safe-area bottom nav.
ui(mobile): replace side drawer with bottom tab bar
Primary: Dashboard / Listeners / Users / Traffic; More sheet for the rest + logout.
Header shows logo on mobile; content padded for safe-area bottom nav.
ui(mobile): replace side drawer with bottom tab bar
Primary: Dashboard / Listeners / Users / Traffic; More sheet for the rest + logout.
Header shows logo on mobile; content padded for safe-area bottom nav.
ui(mobile): replace side drawer with bottom tab bar
Primary: Dashboard / Listeners / Users / Traffic; More sheet for the rest + logout.
Header shows logo on mobile; content padded for safe-area bottom nav.
ui(mobile): replace side drawer with bottom tab bar
Primary: Dashboard / Listeners / Users / Traffic; More sheet for the rest + logout.
Header shows logo on mobile; content padded for safe-area bottom nav.
ui(mobile): replace side drawer with bottom tab bar
Primary: Dashboard / Listeners / Users / Traffic; More sheet for the rest + logout.
Header shows logo on mobile; content padded for safe-area bottom nav.
ui(mobile): replace side drawer with bottom tab bar
Primary: Dashboard / Listeners / Users / Traffic; More sheet for the rest + logout.
Header shows logo on mobile; content padded for safe-area bottom nav.
ui(mobile): replace side drawer with bottom tab bar
Primary: Dashboard / Listeners / Users / Traffic; More sheet for the rest + logout.
Header shows logo on mobile; content padded for safe-area bottom nav.
ui(mobile): replace side drawer with bottom tab bar
Primary: Dashboard / Listeners / Users / Traffic; More sheet for the rest + logout.
Header shows logo on mobile; content padded for safe-area bottom nav.
ui(mobile): replace side drawer with bottom tab bar
Primary: Dashboard / Listeners / Users / Traffic; More sheet for the rest + logout.
Header shows logo on mobile; content padded for safe-area bottom nav.
ui(mobile): replace side drawer with bottom tab bar
Primary: Dashboard / Listeners / Users / Traffic; More sheet for the rest + logout.
Header shows logo on mobile; content padded for safe-area bottom nav.
ui(mobile): replace side drawer with bottom tab bar
Primary: Dashboard / Listeners / Users / Traffic; More sheet for the rest + logout.
Header shows logo on mobile; content padded for safe-area bottom nav.
2026-09-13 23:33:22 +08:00
風雪 7a43346588 fix(ui): repair Listeners versions table render corrupted by size prop injection 2026-09-13 23:29:53 +08:00
風雪 e332cfdac3 ui: improve desktop/mobile usability and long i18n label layout
- Global CSS: wrap long translations, safer forms/modals/tables on mobile
- PageHeader stacks actions on narrow screens
- Users mobile cards show traffic, expiry, remark/tags without losing info
- Layout content width + drawer width polish
ui: improve desktop/mobile usability and long i18n label layout
- Global CSS: wrap long translations, safer forms/modals/tables on mobile
- PageHeader stacks actions on narrow screens
- Users mobile cards show traffic, expiry, remark/tags without losing info
- Layout content width + drawer width polish
ui: improve desktop/mobile usability and long i18n label layout
- Global CSS: wrap long translations, safer forms/modals/tables on mobile
- PageHeader stacks actions on narrow screens
- Users mobile cards show traffic, expiry, remark/tags without losing info
- Layout content width + drawer width polish
ui: improve desktop/mobile usability and long i18n label layout
- Global CSS: wrap long translations, safer forms/modals/tables on mobile
- PageHeader stacks actions on narrow screens
- Users mobile cards show traffic, expiry, remark/tags without losing info
- Layout content width + drawer width polish
ui: improve desktop/mobile usability and long i18n label layout
- Global CSS: wrap long translations, safer forms/modals/tables on mobile
- PageHeader stacks actions on narrow screens
- Users mobile cards show traffic, expiry, remark/tags without losing info
- Layout content width + drawer width polish
ui: improve desktop/mobile usability and long i18n label layout
- Global CSS: wrap long translations, safer forms/modals/tables on mobile
- PageHeader stacks actions on narrow screens
- Users mobile cards show traffic, expiry, remark/tags without losing info
- Layout content width + drawer width polish
ui: improve desktop/mobile usability and long i18n label layout
- Global CSS: wrap long translations, safer forms/modals/tables on mobile
- PageHeader stacks actions on narrow screens
- Users mobile cards show traffic, expiry, remark/tags without losing info
- Layout content width + drawer width polish
ui: improve desktop/mobile usability and long i18n label layout
- Global CSS: wrap long translations, safer forms/modals/tables on mobile
- PageHeader stacks actions on narrow screens
- Users mobile cards show traffic, expiry, remark/tags without losing info
- Layout content width + drawer width polish
ui: improve desktop/mobile usability and long i18n label layout
- Global CSS: wrap long translations, safer forms/modals/tables on mobile
- PageHeader stacks actions on narrow screens
- Users mobile cards show traffic, expiry, remark/tags without losing info
- Layout content width + drawer width polish
ui: improve desktop/mobile usability and long i18n label layout
- Global CSS: wrap long translations, safer forms/modals/tables on mobile
- PageHeader stacks actions on narrow screens
- Users mobile cards show traffic, expiry, remark/tags without losing info
- Layout content width + drawer width polish
ui: improve desktop/mobile usability and long i18n label layout
- Global CSS: wrap long translations, safer forms/modals/tables on mobile
- PageHeader stacks actions on narrow screens
- Users mobile cards show traffic, expiry, remark/tags without losing info
- Layout content width + drawer width polish
ui: improve desktop/mobile usability and long i18n label layout
- Global CSS: wrap long translations, safer forms/modals/tables on mobile
- PageHeader stacks actions on narrow screens
- Users mobile cards show traffic, expiry, remark/tags without losing info
- Layout content width + drawer width polish
2026-09-13 23:29:23 +08:00
風雪 4d6cb94145 fix(ui): load TOTP enabled state from /auth/me on settings mount
Refresh was resetting local totpEnabled to false because Settings never
read totp_enabled from the server after reload.
fix(ui): load TOTP enabled state from /auth/me on settings mount
Refresh was resetting local totpEnabled to false because Settings never
read totp_enabled from the server after reload.
fix(ui): load TOTP enabled state from /auth/me on settings mount
Refresh was resetting local totpEnabled to false because Settings never
read totp_enabled from the server after reload.
fix(ui): load TOTP enabled state from /auth/me on settings mount
Refresh was resetting local totpEnabled to false because Settings never
read totp_enabled from the server after reload.
fix(ui): load TOTP enabled state from /auth/me on settings mount
Refresh was resetting local totpEnabled to false because Settings never
read totp_enabled from the server after reload.
fix(ui): load TOTP enabled state from /auth/me on settings mount
Refresh was resetting local totpEnabled to false because Settings never
read totp_enabled from the server after reload.
fix(ui): load TOTP enabled state from /auth/me on settings mount
Refresh was resetting local totpEnabled to false because Settings never
read totp_enabled from the server after reload.
fix(ui): load TOTP enabled state from /auth/me on settings mount
Refresh was resetting local totpEnabled to false because Settings never
read totp_enabled from the server after reload.
fix(ui): load TOTP enabled state from /auth/me on settings mount
Refresh was resetting local totpEnabled to false because Settings never
read totp_enabled from the server after reload.
fix(ui): load TOTP enabled state from /auth/me on settings mount
Refresh was resetting local totpEnabled to false because Settings never
read totp_enabled from the server after reload.
fix(ui): load TOTP enabled state from /auth/me on settings mount
Refresh was resetting local totpEnabled to false because Settings never
read totp_enabled from the server after reload.
fix(ui): load TOTP enabled state from /auth/me on settings mount
Refresh was resetting local totpEnabled to false because Settings never
read totp_enabled from the server after reload.
2026-09-13 23:14:18 +08:00
風雪 12727fb18a ui(totp): polish login step and settings setup modal
- Login: two-step flow with Steps, Alert, OTP-styled input, back action
- Settings: QR setup modal, clearer enable/disable forms, loading states
- i18n: TOTP strings for en / zh-CN / zh-TW
ui(totp): polish login step and settings setup modal
- Login: two-step flow with Steps, Alert, OTP-styled input, back action
- Settings: QR setup modal, clearer enable/disable forms, loading states
- i18n: TOTP strings for en / zh-CN / zh-TW
ui(totp): polish login step and settings setup modal
- Login: two-step flow with Steps, Alert, OTP-styled input, back action
- Settings: QR setup modal, clearer enable/disable forms, loading states
- i18n: TOTP strings for en / zh-CN / zh-TW
ui(totp): polish login step and settings setup modal
- Login: two-step flow with Steps, Alert, OTP-styled input, back action
- Settings: QR setup modal, clearer enable/disable forms, loading states
- i18n: TOTP strings for en / zh-CN / zh-TW
ui(totp): polish login step and settings setup modal
- Login: two-step flow with Steps, Alert, OTP-styled input, back action
- Settings: QR setup modal, clearer enable/disable forms, loading states
- i18n: TOTP strings for en / zh-CN / zh-TW
ui(totp): polish login step and settings setup modal
- Login: two-step flow with Steps, Alert, OTP-styled input, back action
- Settings: QR setup modal, clearer enable/disable forms, loading states
- i18n: TOTP strings for en / zh-CN / zh-TW
ui(totp): polish login step and settings setup modal
- Login: two-step flow with Steps, Alert, OTP-styled input, back action
- Settings: QR setup modal, clearer enable/disable forms, loading states
- i18n: TOTP strings for en / zh-CN / zh-TW
ui(totp): polish login step and settings setup modal
- Login: two-step flow with Steps, Alert, OTP-styled input, back action
- Settings: QR setup modal, clearer enable/disable forms, loading states
- i18n: TOTP strings for en / zh-CN / zh-TW
ui(totp): polish login step and settings setup modal
- Login: two-step flow with Steps, Alert, OTP-styled input, back action
- Settings: QR setup modal, clearer enable/disable forms, loading states
- i18n: TOTP strings for en / zh-CN / zh-TW
ui(totp): polish login step and settings setup modal
- Login: two-step flow with Steps, Alert, OTP-styled input, back action
- Settings: QR setup modal, clearer enable/disable forms, loading states
- i18n: TOTP strings for en / zh-CN / zh-TW
ui(totp): polish login step and settings setup modal
- Login: two-step flow with Steps, Alert, OTP-styled input, back action
- Settings: QR setup modal, clearer enable/disable forms, loading states
- i18n: TOTP strings for en / zh-CN / zh-TW
ui(totp): polish login step and settings setup modal
- Login: two-step flow with Steps, Alert, OTP-styled input, back action
- Settings: QR setup modal, clearer enable/disable forms, loading states
- i18n: TOTP strings for en / zh-CN / zh-TW
2026-09-13 23:07:34 +08:00
風雪 c82bc0eb20 docs: align README feature matrix with v1.1.0
TOTP, web_path, HWID/first-use, WARP, online connections.
docs: align README feature matrix with v1.1.0
TOTP, web_path, HWID/first-use, WARP, online connections.
docs: align README feature matrix with v1.1.0
TOTP, web_path, HWID/first-use, WARP, online connections.
docs: align README feature matrix with v1.1.0
TOTP, web_path, HWID/first-use, WARP, online connections.
docs: align README feature matrix with v1.1.0
TOTP, web_path, HWID/first-use, WARP, online connections.
docs: align README feature matrix with v1.1.0
TOTP, web_path, HWID/first-use, WARP, online connections.
docs: align README feature matrix with v1.1.0
TOTP, web_path, HWID/first-use, WARP, online connections.
docs: align README feature matrix with v1.1.0
TOTP, web_path, HWID/first-use, WARP, online connections.
docs: align README feature matrix with v1.1.0
TOTP, web_path, HWID/first-use, WARP, online connections.
docs: align README feature matrix with v1.1.0
TOTP, web_path, HWID/first-use, WARP, online connections.
docs: align README feature matrix with v1.1.0
TOTP, web_path, HWID/first-use, WARP, online connections.
docs: align README feature matrix with v1.1.0
TOTP, web_path, HWID/first-use, WARP, online connections.
2026-09-13 22:55:23 +08:00