main
100
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
914053a7ac | fix(i18n): declare listeners.httpHint and socksHint in zh-CN | ||
|
|
d0f115011f | fix(i18n): declare listeners.httpHint and socksHint in en | ||
|
|
89ca102a25 | fix(i18n): add zh-CN httpHint/socksHint for HTTP and SOCKS | ||
|
|
6102c33f57 | fix(i18n): add listeners.httpHint and socksHint for CI | ||
|
|
97a445d5a1 | fix(ui): restore ListenerConfigFields; optional TLS for HTTP/SOCKS | ||
|
|
5d4430de0e | fix(ui): restore ListenerConfigFields and enable HTTP/SOCKS optional TLS | ||
|
|
d59b46a215 | fix(ui): expose optional TLS form for HTTP and SOCKS listeners | ||
|
|
5bb47282a0 | Delete scripts/tests/test_ssh_menu.sh | ||
|
|
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.
|
||
|
|
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`.
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
96dc3b38a2 | style: align VMess test fields | ||
|
|
e75a5847d7 | style: format VMess changes | ||
|
|
0f2da42e02 | style: format VMess changes | ||
|
|
82a75716aa | style: format VMess changes | ||
|
|
8c0779afa6 | ci: diagnose VMess gofmt failure | ||
|
|
35886e6b02 | test: cover VMess mux client export | ||
|
|
fcd5fb8dee | test: preserve VMess per-user alterId | ||
|
|
3b966b3245 | feat: expose complete VMess TLS Mirror options | ||
|
|
72b03cd029 | fix: export VMess listener mux as client smux | ||
|
|
caff433bcc | fix: preserve VMess per-user alterId on compile | ||
|
|
8fe2f9ebb3 | test: provide required listener fields | ||
|
|
8ad38ca2d5 | ci: restore format check | ||
|
|
f75619f0d1 | style: gofmt URI extended file | ||
|
|
e830b02440 | ci: diagnose remaining gofmt difference | ||
|
|
1f6f3b57b0 | ci: restore format check | ||
|
|
b061b1436e | fix: import strings for Sudoku URI serialization | ||
|
|
faf53ae934 | fix: remove duplicate Mieru validation case | ||
|
|
31b9e110df | ci: print gofmt offenders for debugging | ||
|
|
266a3d20b0 | fix: merge protocol-specific listener validation cases | ||
|
|
8417c61645 | test: format listener schema regression tests | ||
|
|
e36d44bb4b | test: format Sudoku URI regression test | ||
|
|
af609b746b | fix(tests): restore validator tests and append listener coverage | ||
|
|
4f864d8859 | test(listener): validate documented protocol values | ||
|
|
5c98af6ca4 | fix(listener): validate Sudoku Mieru and TrustTunnel values | ||
|
|
984ba9a7d8 | fix(listener): persist Hysteria2 Realm TLS options | ||
|
|
e91cbb923e | feat(listener): expose Hysteria2 Realm TLS options | ||
|
|
3d4c2a8288 | test(converter): keep Hysteria2 realm options server-side | ||
|
|
cccd13784d | fix(converter): do not export Hysteria2 server realm options to clients | ||
|
|
5792bd1978 | test(uri): cover Sudoku nested URI serialization | ||
|
|
726781b7be | fix(uri): serialize Sudoku listener options without object coercion | ||
|
|
3d0b9d40be | fix(listener): correct Sudoku server fields and custom table serialization | ||
|
|
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 |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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 |
||
|
|
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>
|
||
|
|
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 |
||
|
|
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. |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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). |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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). |
||
|
|
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. |
||
|
|
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. |
||
|
|
200d9790ec | docs: add Star History chart to all README languages | ||
|
|
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). |
||
|
|
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 |
||
|
|
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. |
||
|
|
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 |
||
|
|
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) |
||
|
|
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 |
||
|
|
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. |
||
|
|
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 |
||
|
|
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 |
||
|
|
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> |
||
|
|
7d00aa2bd5 | Merge pull request #72 from kazeyukiro/security-fixes/ssrf-and-deps | ||
|
|
b4be3dbc68 | Merge pull request #62 from kazeyukiro/security-fixes/ssrf-and-deps | ||
|
|
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 |
||
|
|
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 |
||
|
|
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). |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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. |
||
|
|
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) |
||
|
|
8cf969abd8 | fix(ui): drop Settings minHeight:480 on mobile to avoid empty gap | ||
|
|
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. |
||
|
|
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). |
||
|
|
efa39ee5ed | fix(ui): define isMobile in SharePage for compact layout props | ||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
7a43346588 | fix(ui): repair Listeners versions table render corrupted by size prop injection | ||
|
|
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 |
||
|
|
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. |
||
|
|
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 |
||
|
|
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. |