Commit Graph
961 Commits
Author SHA1 Message Date
kazeyukiro eb78dec2d8 ci(release): restore Changes-since notes like v1.3.7
Build formal release body from conventional commits between the
previous SemVer tag and this release, then append install notes.
ci(release): restore Changes-since notes like v1.3.7
Build formal release body from conventional commits between the
previous SemVer tag and this release, then append install notes.
ci(release): restore Changes-since notes like v1.3.7
Build formal release body from conventional commits between the
previous SemVer tag and this release, then append install notes.
ci(release): restore Changes-since notes like v1.3.7
Build formal release body from conventional commits between the
previous SemVer tag and this release, then append install notes.
ci(release): restore Changes-since notes like v1.3.7
Build formal release body from conventional commits between the
previous SemVer tag and this release, then append install notes.
ci(release): restore Changes-since notes like v1.3.7
Build formal release body from conventional commits between the
previous SemVer tag and this release, then append install notes.
ci(release): restore Changes-since notes like v1.3.7
Build formal release body from conventional commits between the
previous SemVer tag and this release, then append install notes.
ci(release): restore Changes-since notes like v1.3.7
Build formal release body from conventional commits between the
previous SemVer tag and this release, then append install notes.
ci(release): restore Changes-since notes like v1.3.7
Build formal release body from conventional commits between the
previous SemVer tag and this release, then append install notes.
ci(release): restore Changes-since notes like v1.3.7
Build formal release body from conventional commits between the
previous SemVer tag and this release, then append install notes.
ci(release): restore Changes-since notes like v1.3.7
Build formal release body from conventional commits between the
previous SemVer tag and this release, then append install notes.
ci(release): restore Changes-since notes like v1.3.7
Build formal release body from conventional commits between the
previous SemVer tag and this release, then append install notes.
2026-09-27 03:46:22 +00:00
kazeyukiro aa1b51b28c fix(ui): drop useless generic wiki tooltip stubs
Remove fallback *Hint strings that only said "see wiki". Tooltips now
use fieldTip() and render only when real copy exists.
fix(ui): drop useless generic wiki tooltip stubs
Remove fallback *Hint strings that only said "see wiki". Tooltips now
use fieldTip() and render only when real copy exists.
fix(ui): drop useless generic wiki tooltip stubs
Remove fallback *Hint strings that only said "see wiki". Tooltips now
use fieldTip() and render only when real copy exists.
fix(ui): drop useless generic wiki tooltip stubs
Remove fallback *Hint strings that only said "see wiki". Tooltips now
use fieldTip() and render only when real copy exists.
fix(ui): drop useless generic wiki tooltip stubs
Remove fallback *Hint strings that only said "see wiki". Tooltips now
use fieldTip() and render only when real copy exists.
fix(ui): drop useless generic wiki tooltip stubs
Remove fallback *Hint strings that only said "see wiki". Tooltips now
use fieldTip() and render only when real copy exists.
fix(ui): drop useless generic wiki tooltip stubs
Remove fallback *Hint strings that only said "see wiki". Tooltips now
use fieldTip() and render only when real copy exists.
fix(ui): drop useless generic wiki tooltip stubs
Remove fallback *Hint strings that only said "see wiki". Tooltips now
use fieldTip() and render only when real copy exists.
fix(ui): drop useless generic wiki tooltip stubs
Remove fallback *Hint strings that only said "see wiki". Tooltips now
use fieldTip() and render only when real copy exists.
fix(ui): drop useless generic wiki tooltip stubs
Remove fallback *Hint strings that only said "see wiki". Tooltips now
use fieldTip() and render only when real copy exists.
fix(ui): drop useless generic wiki tooltip stubs
Remove fallback *Hint strings that only said "see wiki". Tooltips now
use fieldTip() and render only when real copy exists.
fix(ui): drop useless generic wiki tooltip stubs
Remove fallback *Hint strings that only said "see wiki". Tooltips now
use fieldTip() and render only when real copy exists.
2026-09-27 03:36:52 +00:00
kazeyukiro 8c5b0692ad fix(ui): pass t into capability renderFields (TS2304) 2026-09-27 03:08:34 +00:00
kazeyukiro 1622893434 fix(i18n): restore missing commas after locale key dedupe 2026-09-27 03:02:50 +00:00
kazeyukiro 2349bd0cb8 fix(i18n): remove duplicate locale object keys breaking tsc 2026-09-27 02:59:09 +00:00
kazeyukiro 4b21501deb feat(ui): in-panel field help tooltips + i18n (wiki-aligned)
Expand protocol capability descriptions from Mihomo Meta concepts,
auto-attach Form.Item tooltips on listeners/users/settings, and fill
listeners/users/settings *Hint keys across all locale packs.
feat(ui): in-panel field help tooltips + i18n (wiki-aligned)
Expand protocol capability descriptions from Mihomo Meta concepts,
auto-attach Form.Item tooltips on listeners/users/settings, and fill
listeners/users/settings *Hint keys across all locale packs.
feat(ui): in-panel field help tooltips + i18n (wiki-aligned)
Expand protocol capability descriptions from Mihomo Meta concepts,
auto-attach Form.Item tooltips on listeners/users/settings, and fill
listeners/users/settings *Hint keys across all locale packs.
feat(ui): in-panel field help tooltips + i18n (wiki-aligned)
Expand protocol capability descriptions from Mihomo Meta concepts,
auto-attach Form.Item tooltips on listeners/users/settings, and fill
listeners/users/settings *Hint keys across all locale packs.
feat(ui): in-panel field help tooltips + i18n (wiki-aligned)
Expand protocol capability descriptions from Mihomo Meta concepts,
auto-attach Form.Item tooltips on listeners/users/settings, and fill
listeners/users/settings *Hint keys across all locale packs.
feat(ui): in-panel field help tooltips + i18n (wiki-aligned)
Expand protocol capability descriptions from Mihomo Meta concepts,
auto-attach Form.Item tooltips on listeners/users/settings, and fill
listeners/users/settings *Hint keys across all locale packs.
feat(ui): in-panel field help tooltips + i18n (wiki-aligned)
Expand protocol capability descriptions from Mihomo Meta concepts,
auto-attach Form.Item tooltips on listeners/users/settings, and fill
listeners/users/settings *Hint keys across all locale packs.
feat(ui): in-panel field help tooltips + i18n (wiki-aligned)
Expand protocol capability descriptions from Mihomo Meta concepts,
auto-attach Form.Item tooltips on listeners/users/settings, and fill
listeners/users/settings *Hint keys across all locale packs.
feat(ui): in-panel field help tooltips + i18n (wiki-aligned)
Expand protocol capability descriptions from Mihomo Meta concepts,
auto-attach Form.Item tooltips on listeners/users/settings, and fill
listeners/users/settings *Hint keys across all locale packs.
feat(ui): in-panel field help tooltips + i18n (wiki-aligned)
Expand protocol capability descriptions from Mihomo Meta concepts,
auto-attach Form.Item tooltips on listeners/users/settings, and fill
listeners/users/settings *Hint keys across all locale packs.
feat(ui): in-panel field help tooltips + i18n (wiki-aligned)
Expand protocol capability descriptions from Mihomo Meta concepts,
auto-attach Form.Item tooltips on listeners/users/settings, and fill
listeners/users/settings *Hint keys across all locale packs.
feat(ui): in-panel field help tooltips + i18n (wiki-aligned)
Expand protocol capability descriptions from Mihomo Meta concepts,
auto-attach Form.Item tooltips on listeners/users/settings, and fill
listeners/users/settings *Hint keys across all locale packs.
2026-09-27 02:56:05 +00:00
kazeyukiro bcee7502ff docs: align panel SSL with DNS-01 wildcards
Add panel-ssl.md; update README, installation, batch-certificate, and
OpenAPI /system/ssl for challenge, Cloudflare token, and recovery notes.
docs: align panel SSL with DNS-01 wildcards
Add panel-ssl.md; update README, installation, batch-certificate, and
OpenAPI /system/ssl for challenge, Cloudflare token, and recovery notes.
docs: align panel SSL with DNS-01 wildcards
Add panel-ssl.md; update README, installation, batch-certificate, and
OpenAPI /system/ssl for challenge, Cloudflare token, and recovery notes.
docs: align panel SSL with DNS-01 wildcards
Add panel-ssl.md; update README, installation, batch-certificate, and
OpenAPI /system/ssl for challenge, Cloudflare token, and recovery notes.
docs: align panel SSL with DNS-01 wildcards
Add panel-ssl.md; update README, installation, batch-certificate, and
OpenAPI /system/ssl for challenge, Cloudflare token, and recovery notes.
docs: align panel SSL with DNS-01 wildcards
Add panel-ssl.md; update README, installation, batch-certificate, and
OpenAPI /system/ssl for challenge, Cloudflare token, and recovery notes.
docs: align panel SSL with DNS-01 wildcards
Add panel-ssl.md; update README, installation, batch-certificate, and
OpenAPI /system/ssl for challenge, Cloudflare token, and recovery notes.
docs: align panel SSL with DNS-01 wildcards
Add panel-ssl.md; update README, installation, batch-certificate, and
OpenAPI /system/ssl for challenge, Cloudflare token, and recovery notes.
docs: align panel SSL with DNS-01 wildcards
Add panel-ssl.md; update README, installation, batch-certificate, and
OpenAPI /system/ssl for challenge, Cloudflare token, and recovery notes.
docs: align panel SSL with DNS-01 wildcards
Add panel-ssl.md; update README, installation, batch-certificate, and
OpenAPI /system/ssl for challenge, Cloudflare token, and recovery notes.
docs: align panel SSL with DNS-01 wildcards
Add panel-ssl.md; update README, installation, batch-certificate, and
OpenAPI /system/ssl for challenge, Cloudflare token, and recovery notes.
docs: align panel SSL with DNS-01 wildcards
Add panel-ssl.md; update README, installation, batch-certificate, and
OpenAPI /system/ssl for challenge, Cloudflare token, and recovery notes.
2026-09-27 02:49:50 +00:00
kazeyukiro fcda0763f0 feat(ssl): Let's Encrypt wildcard via DNS-01 (Cloudflare)
Add DNS-01 issuance with acmez for *.example.com (apex included).
Settings: challenge, dns_provider, dns_token, dns_zone. Token is
redacted on GET and preserved when left blank on save. HTTP-01
autocert path unchanged for single hostnames.
feat(ssl): Let's Encrypt wildcard via DNS-01 (Cloudflare)
Add DNS-01 issuance with acmez for *.example.com (apex included).
Settings: challenge, dns_provider, dns_token, dns_zone. Token is
redacted on GET and preserved when left blank on save. HTTP-01
autocert path unchanged for single hostnames.
feat(ssl): Let's Encrypt wildcard via DNS-01 (Cloudflare)
Add DNS-01 issuance with acmez for *.example.com (apex included).
Settings: challenge, dns_provider, dns_token, dns_zone. Token is
redacted on GET and preserved when left blank on save. HTTP-01
autocert path unchanged for single hostnames.
feat(ssl): Let's Encrypt wildcard via DNS-01 (Cloudflare)
Add DNS-01 issuance with acmez for *.example.com (apex included).
Settings: challenge, dns_provider, dns_token, dns_zone. Token is
redacted on GET and preserved when left blank on save. HTTP-01
autocert path unchanged for single hostnames.
feat(ssl): Let's Encrypt wildcard via DNS-01 (Cloudflare)
Add DNS-01 issuance with acmez for *.example.com (apex included).
Settings: challenge, dns_provider, dns_token, dns_zone. Token is
redacted on GET and preserved when left blank on save. HTTP-01
autocert path unchanged for single hostnames.
feat(ssl): Let's Encrypt wildcard via DNS-01 (Cloudflare)
Add DNS-01 issuance with acmez for *.example.com (apex included).
Settings: challenge, dns_provider, dns_token, dns_zone. Token is
redacted on GET and preserved when left blank on save. HTTP-01
autocert path unchanged for single hostnames.
feat(ssl): Let's Encrypt wildcard via DNS-01 (Cloudflare)
Add DNS-01 issuance with acmez for *.example.com (apex included).
Settings: challenge, dns_provider, dns_token, dns_zone. Token is
redacted on GET and preserved when left blank on save. HTTP-01
autocert path unchanged for single hostnames.
feat(ssl): Let's Encrypt wildcard via DNS-01 (Cloudflare)
Add DNS-01 issuance with acmez for *.example.com (apex included).
Settings: challenge, dns_provider, dns_token, dns_zone. Token is
redacted on GET and preserved when left blank on save. HTTP-01
autocert path unchanged for single hostnames.
feat(ssl): Let's Encrypt wildcard via DNS-01 (Cloudflare)
Add DNS-01 issuance with acmez for *.example.com (apex included).
Settings: challenge, dns_provider, dns_token, dns_zone. Token is
redacted on GET and preserved when left blank on save. HTTP-01
autocert path unchanged for single hostnames.
feat(ssl): Let's Encrypt wildcard via DNS-01 (Cloudflare)
Add DNS-01 issuance with acmez for *.example.com (apex included).
Settings: challenge, dns_provider, dns_token, dns_zone. Token is
redacted on GET and preserved when left blank on save. HTTP-01
autocert path unchanged for single hostnames.
feat(ssl): Let's Encrypt wildcard via DNS-01 (Cloudflare)
Add DNS-01 issuance with acmez for *.example.com (apex included).
Settings: challenge, dns_provider, dns_token, dns_zone. Token is
redacted on GET and preserved when left blank on save. HTTP-01
autocert path unchanged for single hostnames.
feat(ssl): Let's Encrypt wildcard via DNS-01 (Cloudflare)
Add DNS-01 issuance with acmez for *.example.com (apex included).
Settings: challenge, dns_provider, dns_token, dns_zone. Token is
redacted on GET and preserved when left blank on save. HTTP-01
autocert path unchanged for single hostnames.
2026-09-27 02:46:59 +00:00
kazeyukiro 05b4be66d6 fix(metrics): read unit cgroup path for process memory split
memory.current was taken from the hierarchy root, so the panel never saw
system.slice/3m-ui.service figures and fell back to raw RSS. Resolve the
path from /proc/self/cgroup and split that unit working set by RSS share.
fix(metrics): read unit cgroup path for process memory split
memory.current was taken from the hierarchy root, so the panel never saw
system.slice/3m-ui.service figures and fell back to raw RSS. Resolve the
path from /proc/self/cgroup and split that unit working set by RSS share.
fix(metrics): read unit cgroup path for process memory split
memory.current was taken from the hierarchy root, so the panel never saw
system.slice/3m-ui.service figures and fell back to raw RSS. Resolve the
path from /proc/self/cgroup and split that unit working set by RSS share.
fix(metrics): read unit cgroup path for process memory split
memory.current was taken from the hierarchy root, so the panel never saw
system.slice/3m-ui.service figures and fell back to raw RSS. Resolve the
path from /proc/self/cgroup and split that unit working set by RSS share.
fix(metrics): read unit cgroup path for process memory split
memory.current was taken from the hierarchy root, so the panel never saw
system.slice/3m-ui.service figures and fell back to raw RSS. Resolve the
path from /proc/self/cgroup and split that unit working set by RSS share.
fix(metrics): read unit cgroup path for process memory split
memory.current was taken from the hierarchy root, so the panel never saw
system.slice/3m-ui.service figures and fell back to raw RSS. Resolve the
path from /proc/self/cgroup and split that unit working set by RSS share.
fix(metrics): read unit cgroup path for process memory split
memory.current was taken from the hierarchy root, so the panel never saw
system.slice/3m-ui.service figures and fell back to raw RSS. Resolve the
path from /proc/self/cgroup and split that unit working set by RSS share.
fix(metrics): read unit cgroup path for process memory split
memory.current was taken from the hierarchy root, so the panel never saw
system.slice/3m-ui.service figures and fell back to raw RSS. Resolve the
path from /proc/self/cgroup and split that unit working set by RSS share.
fix(metrics): read unit cgroup path for process memory split
memory.current was taken from the hierarchy root, so the panel never saw
system.slice/3m-ui.service figures and fell back to raw RSS. Resolve the
path from /proc/self/cgroup and split that unit working set by RSS share.
fix(metrics): read unit cgroup path for process memory split
memory.current was taken from the hierarchy root, so the panel never saw
system.slice/3m-ui.service figures and fell back to raw RSS. Resolve the
path from /proc/self/cgroup and split that unit working set by RSS share.
fix(metrics): read unit cgroup path for process memory split
memory.current was taken from the hierarchy root, so the panel never saw
system.slice/3m-ui.service figures and fell back to raw RSS. Resolve the
path from /proc/self/cgroup and split that unit working set by RSS share.
fix(metrics): read unit cgroup path for process memory split
memory.current was taken from the hierarchy root, so the panel never saw
system.slice/3m-ui.service figures and fell back to raw RSS. Resolve the
path from /proc/self/cgroup and split that unit working set by RSS share.
2026-09-27 02:28:53 +00:00
kazeyukiro 4cafa139cf fix(metrics): report process memory on cgroup working-set basis
Split the unit cgroup working set between panel and core by RSS share so
dashboard rows sum to the same figure as the system card and systemd
MemoryCurrent, instead of double-counting shared library RSS.
fix(metrics): report process memory on cgroup working-set basis
Split the unit cgroup working set between panel and core by RSS share so
dashboard rows sum to the same figure as the system card and systemd
MemoryCurrent, instead of double-counting shared library RSS.
fix(metrics): report process memory on cgroup working-set basis
Split the unit cgroup working set between panel and core by RSS share so
dashboard rows sum to the same figure as the system card and systemd
MemoryCurrent, instead of double-counting shared library RSS.
fix(metrics): report process memory on cgroup working-set basis
Split the unit cgroup working set between panel and core by RSS share so
dashboard rows sum to the same figure as the system card and systemd
MemoryCurrent, instead of double-counting shared library RSS.
fix(metrics): report process memory on cgroup working-set basis
Split the unit cgroup working set between panel and core by RSS share so
dashboard rows sum to the same figure as the system card and systemd
MemoryCurrent, instead of double-counting shared library RSS.
fix(metrics): report process memory on cgroup working-set basis
Split the unit cgroup working set between panel and core by RSS share so
dashboard rows sum to the same figure as the system card and systemd
MemoryCurrent, instead of double-counting shared library RSS.
fix(metrics): report process memory on cgroup working-set basis
Split the unit cgroup working set between panel and core by RSS share so
dashboard rows sum to the same figure as the system card and systemd
MemoryCurrent, instead of double-counting shared library RSS.
fix(metrics): report process memory on cgroup working-set basis
Split the unit cgroup working set between panel and core by RSS share so
dashboard rows sum to the same figure as the system card and systemd
MemoryCurrent, instead of double-counting shared library RSS.
fix(metrics): report process memory on cgroup working-set basis
Split the unit cgroup working set between panel and core by RSS share so
dashboard rows sum to the same figure as the system card and systemd
MemoryCurrent, instead of double-counting shared library RSS.
fix(metrics): report process memory on cgroup working-set basis
Split the unit cgroup working set between panel and core by RSS share so
dashboard rows sum to the same figure as the system card and systemd
MemoryCurrent, instead of double-counting shared library RSS.
fix(metrics): report process memory on cgroup working-set basis
Split the unit cgroup working set between panel and core by RSS share so
dashboard rows sum to the same figure as the system card and systemd
MemoryCurrent, instead of double-counting shared library RSS.
fix(metrics): report process memory on cgroup working-set basis
Split the unit cgroup working set between panel and core by RSS share so
dashboard rows sum to the same figure as the system card and systemd
MemoryCurrent, instead of double-counting shared library RSS.
2026-09-27 01:52:58 +00:00
風雪and風雪 7129cc346d perf(memory): keep install, update and runtime inside a small-RAM budget (#83)
* perf(memory): keep install, update and runtime inside a small-RAM budget
Small NAT/VPS targets have 128-512MB of RAM and no swap, so anything that
cannot be reclaimed is fatal. This tuning touches no swap and no mmap; it
works by not allocating in the first place.
Installer (covers install and update; update.sh only execs install.sh):
- Put the scratch directory under DATA_DIR instead of mktemp -d. /tmp is
tmpfs on many of these hosts, and without swap every byte there is pinned
in RAM: bundle, extracted release and a copy of the data directory were
held at once, exceeding total RAM of the machines being targeted.
- Hash assets on the way to disk instead of re-reading them. Download is a
single pass now; verification still completes before tar sees the bytes.
- Release the intermediate data.tar right after extraction rather than
holding the data directory twice for the rest of the run.
- Check scratch capacity before downloading, so we fail before the old
service is stopped rather than halfway through.
- Refresh Environment=GOMEMLIMIT/GOGC/THREE_M_UI_CORE_LOW_MEMORY and
MemoryMax on an existing unit. write_service() keeps an operator's unit,
which meant the values baked in at install time never tracked updates.
Panel:
- Bound the Go heap from the process itself via debug.SetMemoryLimit, so the
tuning reaches manual runs, containers and the installer's own init and
healthcheck instead of only a systemd unit. It acts in-process, so the
Mihomo core never inherits the panel's tighter GC target. An explicit
GOMEMLIMIT or GOGC always wins.
- Emit geodata-loader: memconservative for the core when low-memory mode is
on. The key is omitempty and pinned after user fragments merge, so nothing
changes for anyone who does not opt in, and no fragment can undo it.
No new calls to Mmap/Munmap/Madvise, no swap or mmap anywhere in the tree.
* fix(ci): keep the new scratch directory out of the installer's own paths
Two problems CI caught on the small-memory change, both from moving the
scratch directory from mktemp into DATA_DIR.
restore_files() rebuilds the data directory by deleting everything in it
except backups. The scratch directory now lives inside it, so a rollback
deleted the very directory it was reading its own rollback out of, and the
restore died looking for restore/ENABLED. Rollback therefore had to be
skipped, exempting the scratch directory from that purge; the cleanup trap
already removes it.
The test curl also modelled only one of curl's output modes. Writing to
stdout and answering -I are both normal curl behaviour, and both are now
covered, so the mock no longer pins the installer to a single invocation
shape. The distribution fake likewise emits its tampered asset on stdout
instead of through --output, keeping that test about checksum rejection
rather than about which flag was used.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
perf(memory): keep install, update and runtime inside a small-RAM budget (#83)
* perf(memory): keep install, update and runtime inside a small-RAM budget
Small NAT/VPS targets have 128-512MB of RAM and no swap, so anything that
cannot be reclaimed is fatal. This tuning touches no swap and no mmap; it
works by not allocating in the first place.
Installer (covers install and update; update.sh only execs install.sh):
- Put the scratch directory under DATA_DIR instead of mktemp -d. /tmp is
tmpfs on many of these hosts, and without swap every byte there is pinned
in RAM: bundle, extracted release and a copy of the data directory were
held at once, exceeding total RAM of the machines being targeted.
- Hash assets on the way to disk instead of re-reading them. Download is a
single pass now; verification still completes before tar sees the bytes.
- Release the intermediate data.tar right after extraction rather than
holding the data directory twice for the rest of the run.
- Check scratch capacity before downloading, so we fail before the old
service is stopped rather than halfway through.
- Refresh Environment=GOMEMLIMIT/GOGC/THREE_M_UI_CORE_LOW_MEMORY and
MemoryMax on an existing unit. write_service() keeps an operator's unit,
which meant the values baked in at install time never tracked updates.
Panel:
- Bound the Go heap from the process itself via debug.SetMemoryLimit, so the
tuning reaches manual runs, containers and the installer's own init and
healthcheck instead of only a systemd unit. It acts in-process, so the
Mihomo core never inherits the panel's tighter GC target. An explicit
GOMEMLIMIT or GOGC always wins.
- Emit geodata-loader: memconservative for the core when low-memory mode is
on. The key is omitempty and pinned after user fragments merge, so nothing
changes for anyone who does not opt in, and no fragment can undo it.
No new calls to Mmap/Munmap/Madvise, no swap or mmap anywhere in the tree.
* fix(ci): keep the new scratch directory out of the installer's own paths
Two problems CI caught on the small-memory change, both from moving the
scratch directory from mktemp into DATA_DIR.
restore_files() rebuilds the data directory by deleting everything in it
except backups. The scratch directory now lives inside it, so a rollback
deleted the very directory it was reading its own rollback out of, and the
restore died looking for restore/ENABLED. Rollback therefore had to be
skipped, exempting the scratch directory from that purge; the cleanup trap
already removes it.
The test curl also modelled only one of curl's output modes. Writing to
stdout and answering -I are both normal curl behaviour, and both are now
covered, so the mock no longer pins the installer to a single invocation
shape. The distribution fake likewise emits its tampered asset on stdout
instead of through --output, keeping that test about checksum rejection
rather than about which flag was used.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
perf(memory): keep install, update and runtime inside a small-RAM budget (#83)
* perf(memory): keep install, update and runtime inside a small-RAM budget
Small NAT/VPS targets have 128-512MB of RAM and no swap, so anything that
cannot be reclaimed is fatal. This tuning touches no swap and no mmap; it
works by not allocating in the first place.
Installer (covers install and update; update.sh only execs install.sh):
- Put the scratch directory under DATA_DIR instead of mktemp -d. /tmp is
tmpfs on many of these hosts, and without swap every byte there is pinned
in RAM: bundle, extracted release and a copy of the data directory were
held at once, exceeding total RAM of the machines being targeted.
- Hash assets on the way to disk instead of re-reading them. Download is a
single pass now; verification still completes before tar sees the bytes.
- Release the intermediate data.tar right after extraction rather than
holding the data directory twice for the rest of the run.
- Check scratch capacity before downloading, so we fail before the old
service is stopped rather than halfway through.
- Refresh Environment=GOMEMLIMIT/GOGC/THREE_M_UI_CORE_LOW_MEMORY and
MemoryMax on an existing unit. write_service() keeps an operator's unit,
which meant the values baked in at install time never tracked updates.
Panel:
- Bound the Go heap from the process itself via debug.SetMemoryLimit, so the
tuning reaches manual runs, containers and the installer's own init and
healthcheck instead of only a systemd unit. It acts in-process, so the
Mihomo core never inherits the panel's tighter GC target. An explicit
GOMEMLIMIT or GOGC always wins.
- Emit geodata-loader: memconservative for the core when low-memory mode is
on. The key is omitempty and pinned after user fragments merge, so nothing
changes for anyone who does not opt in, and no fragment can undo it.
No new calls to Mmap/Munmap/Madvise, no swap or mmap anywhere in the tree.
* fix(ci): keep the new scratch directory out of the installer's own paths
Two problems CI caught on the small-memory change, both from moving the
scratch directory from mktemp into DATA_DIR.
restore_files() rebuilds the data directory by deleting everything in it
except backups. The scratch directory now lives inside it, so a rollback
deleted the very directory it was reading its own rollback out of, and the
restore died looking for restore/ENABLED. Rollback therefore had to be
skipped, exempting the scratch directory from that purge; the cleanup trap
already removes it.
The test curl also modelled only one of curl's output modes. Writing to
stdout and answering -I are both normal curl behaviour, and both are now
covered, so the mock no longer pins the installer to a single invocation
shape. The distribution fake likewise emits its tampered asset on stdout
instead of through --output, keeping that test about checksum rejection
rather than about which flag was used.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
perf(memory): keep install, update and runtime inside a small-RAM budget (#83)
* perf(memory): keep install, update and runtime inside a small-RAM budget
Small NAT/VPS targets have 128-512MB of RAM and no swap, so anything that
cannot be reclaimed is fatal. This tuning touches no swap and no mmap; it
works by not allocating in the first place.
Installer (covers install and update; update.sh only execs install.sh):
- Put the scratch directory under DATA_DIR instead of mktemp -d. /tmp is
tmpfs on many of these hosts, and without swap every byte there is pinned
in RAM: bundle, extracted release and a copy of the data directory were
held at once, exceeding total RAM of the machines being targeted.
- Hash assets on the way to disk instead of re-reading them. Download is a
single pass now; verification still completes before tar sees the bytes.
- Release the intermediate data.tar right after extraction rather than
holding the data directory twice for the rest of the run.
- Check scratch capacity before downloading, so we fail before the old
service is stopped rather than halfway through.
- Refresh Environment=GOMEMLIMIT/GOGC/THREE_M_UI_CORE_LOW_MEMORY and
MemoryMax on an existing unit. write_service() keeps an operator's unit,
which meant the values baked in at install time never tracked updates.
Panel:
- Bound the Go heap from the process itself via debug.SetMemoryLimit, so the
tuning reaches manual runs, containers and the installer's own init and
healthcheck instead of only a systemd unit. It acts in-process, so the
Mihomo core never inherits the panel's tighter GC target. An explicit
GOMEMLIMIT or GOGC always wins.
- Emit geodata-loader: memconservative for the core when low-memory mode is
on. The key is omitempty and pinned after user fragments merge, so nothing
changes for anyone who does not opt in, and no fragment can undo it.
No new calls to Mmap/Munmap/Madvise, no swap or mmap anywhere in the tree.
* fix(ci): keep the new scratch directory out of the installer's own paths
Two problems CI caught on the small-memory change, both from moving the
scratch directory from mktemp into DATA_DIR.
restore_files() rebuilds the data directory by deleting everything in it
except backups. The scratch directory now lives inside it, so a rollback
deleted the very directory it was reading its own rollback out of, and the
restore died looking for restore/ENABLED. Rollback therefore had to be
skipped, exempting the scratch directory from that purge; the cleanup trap
already removes it.
The test curl also modelled only one of curl's output modes. Writing to
stdout and answering -I are both normal curl behaviour, and both are now
covered, so the mock no longer pins the installer to a single invocation
shape. The distribution fake likewise emits its tampered asset on stdout
instead of through --output, keeping that test about checksum rejection
rather than about which flag was used.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
perf(memory): keep install, update and runtime inside a small-RAM budget (#83)
* perf(memory): keep install, update and runtime inside a small-RAM budget
Small NAT/VPS targets have 128-512MB of RAM and no swap, so anything that
cannot be reclaimed is fatal. This tuning touches no swap and no mmap; it
works by not allocating in the first place.
Installer (covers install and update; update.sh only execs install.sh):
- Put the scratch directory under DATA_DIR instead of mktemp -d. /tmp is
tmpfs on many of these hosts, and without swap every byte there is pinned
in RAM: bundle, extracted release and a copy of the data directory were
held at once, exceeding total RAM of the machines being targeted.
- Hash assets on the way to disk instead of re-reading them. Download is a
single pass now; verification still completes before tar sees the bytes.
- Release the intermediate data.tar right after extraction rather than
holding the data directory twice for the rest of the run.
- Check scratch capacity before downloading, so we fail before the old
service is stopped rather than halfway through.
- Refresh Environment=GOMEMLIMIT/GOGC/THREE_M_UI_CORE_LOW_MEMORY and
MemoryMax on an existing unit. write_service() keeps an operator's unit,
which meant the values baked in at install time never tracked updates.
Panel:
- Bound the Go heap from the process itself via debug.SetMemoryLimit, so the
tuning reaches manual runs, containers and the installer's own init and
healthcheck instead of only a systemd unit. It acts in-process, so the
Mihomo core never inherits the panel's tighter GC target. An explicit
GOMEMLIMIT or GOGC always wins.
- Emit geodata-loader: memconservative for the core when low-memory mode is
on. The key is omitempty and pinned after user fragments merge, so nothing
changes for anyone who does not opt in, and no fragment can undo it.
No new calls to Mmap/Munmap/Madvise, no swap or mmap anywhere in the tree.
* fix(ci): keep the new scratch directory out of the installer's own paths
Two problems CI caught on the small-memory change, both from moving the
scratch directory from mktemp into DATA_DIR.
restore_files() rebuilds the data directory by deleting everything in it
except backups. The scratch directory now lives inside it, so a rollback
deleted the very directory it was reading its own rollback out of, and the
restore died looking for restore/ENABLED. Rollback therefore had to be
skipped, exempting the scratch directory from that purge; the cleanup trap
already removes it.
The test curl also modelled only one of curl's output modes. Writing to
stdout and answering -I are both normal curl behaviour, and both are now
covered, so the mock no longer pins the installer to a single invocation
shape. The distribution fake likewise emits its tampered asset on stdout
instead of through --output, keeping that test about checksum rejection
rather than about which flag was used.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
perf(memory): keep install, update and runtime inside a small-RAM budget (#83)
* perf(memory): keep install, update and runtime inside a small-RAM budget
Small NAT/VPS targets have 128-512MB of RAM and no swap, so anything that
cannot be reclaimed is fatal. This tuning touches no swap and no mmap; it
works by not allocating in the first place.
Installer (covers install and update; update.sh only execs install.sh):
- Put the scratch directory under DATA_DIR instead of mktemp -d. /tmp is
tmpfs on many of these hosts, and without swap every byte there is pinned
in RAM: bundle, extracted release and a copy of the data directory were
held at once, exceeding total RAM of the machines being targeted.
- Hash assets on the way to disk instead of re-reading them. Download is a
single pass now; verification still completes before tar sees the bytes.
- Release the intermediate data.tar right after extraction rather than
holding the data directory twice for the rest of the run.
- Check scratch capacity before downloading, so we fail before the old
service is stopped rather than halfway through.
- Refresh Environment=GOMEMLIMIT/GOGC/THREE_M_UI_CORE_LOW_MEMORY and
MemoryMax on an existing unit. write_service() keeps an operator's unit,
which meant the values baked in at install time never tracked updates.
Panel:
- Bound the Go heap from the process itself via debug.SetMemoryLimit, so the
tuning reaches manual runs, containers and the installer's own init and
healthcheck instead of only a systemd unit. It acts in-process, so the
Mihomo core never inherits the panel's tighter GC target. An explicit
GOMEMLIMIT or GOGC always wins.
- Emit geodata-loader: memconservative for the core when low-memory mode is
on. The key is omitempty and pinned after user fragments merge, so nothing
changes for anyone who does not opt in, and no fragment can undo it.
No new calls to Mmap/Munmap/Madvise, no swap or mmap anywhere in the tree.
* fix(ci): keep the new scratch directory out of the installer's own paths
Two problems CI caught on the small-memory change, both from moving the
scratch directory from mktemp into DATA_DIR.
restore_files() rebuilds the data directory by deleting everything in it
except backups. The scratch directory now lives inside it, so a rollback
deleted the very directory it was reading its own rollback out of, and the
restore died looking for restore/ENABLED. Rollback therefore had to be
skipped, exempting the scratch directory from that purge; the cleanup trap
already removes it.
The test curl also modelled only one of curl's output modes. Writing to
stdout and answering -I are both normal curl behaviour, and both are now
covered, so the mock no longer pins the installer to a single invocation
shape. The distribution fake likewise emits its tampered asset on stdout
instead of through --output, keeping that test about checksum rejection
rather than about which flag was used.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
perf(memory): keep install, update and runtime inside a small-RAM budget (#83)
* perf(memory): keep install, update and runtime inside a small-RAM budget
Small NAT/VPS targets have 128-512MB of RAM and no swap, so anything that
cannot be reclaimed is fatal. This tuning touches no swap and no mmap; it
works by not allocating in the first place.
Installer (covers install and update; update.sh only execs install.sh):
- Put the scratch directory under DATA_DIR instead of mktemp -d. /tmp is
tmpfs on many of these hosts, and without swap every byte there is pinned
in RAM: bundle, extracted release and a copy of the data directory were
held at once, exceeding total RAM of the machines being targeted.
- Hash assets on the way to disk instead of re-reading them. Download is a
single pass now; verification still completes before tar sees the bytes.
- Release the intermediate data.tar right after extraction rather than
holding the data directory twice for the rest of the run.
- Check scratch capacity before downloading, so we fail before the old
service is stopped rather than halfway through.
- Refresh Environment=GOMEMLIMIT/GOGC/THREE_M_UI_CORE_LOW_MEMORY and
MemoryMax on an existing unit. write_service() keeps an operator's unit,
which meant the values baked in at install time never tracked updates.
Panel:
- Bound the Go heap from the process itself via debug.SetMemoryLimit, so the
tuning reaches manual runs, containers and the installer's own init and
healthcheck instead of only a systemd unit. It acts in-process, so the
Mihomo core never inherits the panel's tighter GC target. An explicit
GOMEMLIMIT or GOGC always wins.
- Emit geodata-loader: memconservative for the core when low-memory mode is
on. The key is omitempty and pinned after user fragments merge, so nothing
changes for anyone who does not opt in, and no fragment can undo it.
No new calls to Mmap/Munmap/Madvise, no swap or mmap anywhere in the tree.
* fix(ci): keep the new scratch directory out of the installer's own paths
Two problems CI caught on the small-memory change, both from moving the
scratch directory from mktemp into DATA_DIR.
restore_files() rebuilds the data directory by deleting everything in it
except backups. The scratch directory now lives inside it, so a rollback
deleted the very directory it was reading its own rollback out of, and the
restore died looking for restore/ENABLED. Rollback therefore had to be
skipped, exempting the scratch directory from that purge; the cleanup trap
already removes it.
The test curl also modelled only one of curl's output modes. Writing to
stdout and answering -I are both normal curl behaviour, and both are now
covered, so the mock no longer pins the installer to a single invocation
shape. The distribution fake likewise emits its tampered asset on stdout
instead of through --output, keeping that test about checksum rejection
rather than about which flag was used.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
perf(memory): keep install, update and runtime inside a small-RAM budget (#83)
* perf(memory): keep install, update and runtime inside a small-RAM budget
Small NAT/VPS targets have 128-512MB of RAM and no swap, so anything that
cannot be reclaimed is fatal. This tuning touches no swap and no mmap; it
works by not allocating in the first place.
Installer (covers install and update; update.sh only execs install.sh):
- Put the scratch directory under DATA_DIR instead of mktemp -d. /tmp is
tmpfs on many of these hosts, and without swap every byte there is pinned
in RAM: bundle, extracted release and a copy of the data directory were
held at once, exceeding total RAM of the machines being targeted.
- Hash assets on the way to disk instead of re-reading them. Download is a
single pass now; verification still completes before tar sees the bytes.
- Release the intermediate data.tar right after extraction rather than
holding the data directory twice for the rest of the run.
- Check scratch capacity before downloading, so we fail before the old
service is stopped rather than halfway through.
- Refresh Environment=GOMEMLIMIT/GOGC/THREE_M_UI_CORE_LOW_MEMORY and
MemoryMax on an existing unit. write_service() keeps an operator's unit,
which meant the values baked in at install time never tracked updates.
Panel:
- Bound the Go heap from the process itself via debug.SetMemoryLimit, so the
tuning reaches manual runs, containers and the installer's own init and
healthcheck instead of only a systemd unit. It acts in-process, so the
Mihomo core never inherits the panel's tighter GC target. An explicit
GOMEMLIMIT or GOGC always wins.
- Emit geodata-loader: memconservative for the core when low-memory mode is
on. The key is omitempty and pinned after user fragments merge, so nothing
changes for anyone who does not opt in, and no fragment can undo it.
No new calls to Mmap/Munmap/Madvise, no swap or mmap anywhere in the tree.
* fix(ci): keep the new scratch directory out of the installer's own paths
Two problems CI caught on the small-memory change, both from moving the
scratch directory from mktemp into DATA_DIR.
restore_files() rebuilds the data directory by deleting everything in it
except backups. The scratch directory now lives inside it, so a rollback
deleted the very directory it was reading its own rollback out of, and the
restore died looking for restore/ENABLED. Rollback therefore had to be
skipped, exempting the scratch directory from that purge; the cleanup trap
already removes it.
The test curl also modelled only one of curl's output modes. Writing to
stdout and answering -I are both normal curl behaviour, and both are now
covered, so the mock no longer pins the installer to a single invocation
shape. The distribution fake likewise emits its tampered asset on stdout
instead of through --output, keeping that test about checksum rejection
rather than about which flag was used.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
perf(memory): keep install, update and runtime inside a small-RAM budget (#83)
* perf(memory): keep install, update and runtime inside a small-RAM budget
Small NAT/VPS targets have 128-512MB of RAM and no swap, so anything that
cannot be reclaimed is fatal. This tuning touches no swap and no mmap; it
works by not allocating in the first place.
Installer (covers install and update; update.sh only execs install.sh):
- Put the scratch directory under DATA_DIR instead of mktemp -d. /tmp is
tmpfs on many of these hosts, and without swap every byte there is pinned
in RAM: bundle, extracted release and a copy of the data directory were
held at once, exceeding total RAM of the machines being targeted.
- Hash assets on the way to disk instead of re-reading them. Download is a
single pass now; verification still completes before tar sees the bytes.
- Release the intermediate data.tar right after extraction rather than
holding the data directory twice for the rest of the run.
- Check scratch capacity before downloading, so we fail before the old
service is stopped rather than halfway through.
- Refresh Environment=GOMEMLIMIT/GOGC/THREE_M_UI_CORE_LOW_MEMORY and
MemoryMax on an existing unit. write_service() keeps an operator's unit,
which meant the values baked in at install time never tracked updates.
Panel:
- Bound the Go heap from the process itself via debug.SetMemoryLimit, so the
tuning reaches manual runs, containers and the installer's own init and
healthcheck instead of only a systemd unit. It acts in-process, so the
Mihomo core never inherits the panel's tighter GC target. An explicit
GOMEMLIMIT or GOGC always wins.
- Emit geodata-loader: memconservative for the core when low-memory mode is
on. The key is omitempty and pinned after user fragments merge, so nothing
changes for anyone who does not opt in, and no fragment can undo it.
No new calls to Mmap/Munmap/Madvise, no swap or mmap anywhere in the tree.
* fix(ci): keep the new scratch directory out of the installer's own paths
Two problems CI caught on the small-memory change, both from moving the
scratch directory from mktemp into DATA_DIR.
restore_files() rebuilds the data directory by deleting everything in it
except backups. The scratch directory now lives inside it, so a rollback
deleted the very directory it was reading its own rollback out of, and the
restore died looking for restore/ENABLED. Rollback therefore had to be
skipped, exempting the scratch directory from that purge; the cleanup trap
already removes it.
The test curl also modelled only one of curl's output modes. Writing to
stdout and answering -I are both normal curl behaviour, and both are now
covered, so the mock no longer pins the installer to a single invocation
shape. The distribution fake likewise emits its tampered asset on stdout
instead of through --output, keeping that test about checksum rejection
rather than about which flag was used.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
perf(memory): keep install, update and runtime inside a small-RAM budget (#83)
* perf(memory): keep install, update and runtime inside a small-RAM budget
Small NAT/VPS targets have 128-512MB of RAM and no swap, so anything that
cannot be reclaimed is fatal. This tuning touches no swap and no mmap; it
works by not allocating in the first place.
Installer (covers install and update; update.sh only execs install.sh):
- Put the scratch directory under DATA_DIR instead of mktemp -d. /tmp is
tmpfs on many of these hosts, and without swap every byte there is pinned
in RAM: bundle, extracted release and a copy of the data directory were
held at once, exceeding total RAM of the machines being targeted.
- Hash assets on the way to disk instead of re-reading them. Download is a
single pass now; verification still completes before tar sees the bytes.
- Release the intermediate data.tar right after extraction rather than
holding the data directory twice for the rest of the run.
- Check scratch capacity before downloading, so we fail before the old
service is stopped rather than halfway through.
- Refresh Environment=GOMEMLIMIT/GOGC/THREE_M_UI_CORE_LOW_MEMORY and
MemoryMax on an existing unit. write_service() keeps an operator's unit,
which meant the values baked in at install time never tracked updates.
Panel:
- Bound the Go heap from the process itself via debug.SetMemoryLimit, so the
tuning reaches manual runs, containers and the installer's own init and
healthcheck instead of only a systemd unit. It acts in-process, so the
Mihomo core never inherits the panel's tighter GC target. An explicit
GOMEMLIMIT or GOGC always wins.
- Emit geodata-loader: memconservative for the core when low-memory mode is
on. The key is omitempty and pinned after user fragments merge, so nothing
changes for anyone who does not opt in, and no fragment can undo it.
No new calls to Mmap/Munmap/Madvise, no swap or mmap anywhere in the tree.
* fix(ci): keep the new scratch directory out of the installer's own paths
Two problems CI caught on the small-memory change, both from moving the
scratch directory from mktemp into DATA_DIR.
restore_files() rebuilds the data directory by deleting everything in it
except backups. The scratch directory now lives inside it, so a rollback
deleted the very directory it was reading its own rollback out of, and the
restore died looking for restore/ENABLED. Rollback therefore had to be
skipped, exempting the scratch directory from that purge; the cleanup trap
already removes it.
The test curl also modelled only one of curl's output modes. Writing to
stdout and answering -I are both normal curl behaviour, and both are now
covered, so the mock no longer pins the installer to a single invocation
shape. The distribution fake likewise emits its tampered asset on stdout
instead of through --output, keeping that test about checksum rejection
rather than about which flag was used.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
perf(memory): keep install, update and runtime inside a small-RAM budget (#83)
* perf(memory): keep install, update and runtime inside a small-RAM budget
Small NAT/VPS targets have 128-512MB of RAM and no swap, so anything that
cannot be reclaimed is fatal. This tuning touches no swap and no mmap; it
works by not allocating in the first place.
Installer (covers install and update; update.sh only execs install.sh):
- Put the scratch directory under DATA_DIR instead of mktemp -d. /tmp is
tmpfs on many of these hosts, and without swap every byte there is pinned
in RAM: bundle, extracted release and a copy of the data directory were
held at once, exceeding total RAM of the machines being targeted.
- Hash assets on the way to disk instead of re-reading them. Download is a
single pass now; verification still completes before tar sees the bytes.
- Release the intermediate data.tar right after extraction rather than
holding the data directory twice for the rest of the run.
- Check scratch capacity before downloading, so we fail before the old
service is stopped rather than halfway through.
- Refresh Environment=GOMEMLIMIT/GOGC/THREE_M_UI_CORE_LOW_MEMORY and
MemoryMax on an existing unit. write_service() keeps an operator's unit,
which meant the values baked in at install time never tracked updates.
Panel:
- Bound the Go heap from the process itself via debug.SetMemoryLimit, so the
tuning reaches manual runs, containers and the installer's own init and
healthcheck instead of only a systemd unit. It acts in-process, so the
Mihomo core never inherits the panel's tighter GC target. An explicit
GOMEMLIMIT or GOGC always wins.
- Emit geodata-loader: memconservative for the core when low-memory mode is
on. The key is omitempty and pinned after user fragments merge, so nothing
changes for anyone who does not opt in, and no fragment can undo it.
No new calls to Mmap/Munmap/Madvise, no swap or mmap anywhere in the tree.
* fix(ci): keep the new scratch directory out of the installer's own paths
Two problems CI caught on the small-memory change, both from moving the
scratch directory from mktemp into DATA_DIR.
restore_files() rebuilds the data directory by deleting everything in it
except backups. The scratch directory now lives inside it, so a rollback
deleted the very directory it was reading its own rollback out of, and the
restore died looking for restore/ENABLED. Rollback therefore had to be
skipped, exempting the scratch directory from that purge; the cleanup trap
already removes it.
The test curl also modelled only one of curl's output modes. Writing to
stdout and answering -I are both normal curl behaviour, and both are now
covered, so the mock no longer pins the installer to a single invocation
shape. The distribution fake likewise emits its tampered asset on stdout
instead of through --output, keeping that test about checksum rejection
rather than about which flag was used.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
perf(memory): keep install, update and runtime inside a small-RAM budget (#83)
* perf(memory): keep install, update and runtime inside a small-RAM budget
Small NAT/VPS targets have 128-512MB of RAM and no swap, so anything that
cannot be reclaimed is fatal. This tuning touches no swap and no mmap; it
works by not allocating in the first place.
Installer (covers install and update; update.sh only execs install.sh):
- Put the scratch directory under DATA_DIR instead of mktemp -d. /tmp is
tmpfs on many of these hosts, and without swap every byte there is pinned
in RAM: bundle, extracted release and a copy of the data directory were
held at once, exceeding total RAM of the machines being targeted.
- Hash assets on the way to disk instead of re-reading them. Download is a
single pass now; verification still completes before tar sees the bytes.
- Release the intermediate data.tar right after extraction rather than
holding the data directory twice for the rest of the run.
- Check scratch capacity before downloading, so we fail before the old
service is stopped rather than halfway through.
- Refresh Environment=GOMEMLIMIT/GOGC/THREE_M_UI_CORE_LOW_MEMORY and
MemoryMax on an existing unit. write_service() keeps an operator's unit,
which meant the values baked in at install time never tracked updates.
Panel:
- Bound the Go heap from the process itself via debug.SetMemoryLimit, so the
tuning reaches manual runs, containers and the installer's own init and
healthcheck instead of only a systemd unit. It acts in-process, so the
Mihomo core never inherits the panel's tighter GC target. An explicit
GOMEMLIMIT or GOGC always wins.
- Emit geodata-loader: memconservative for the core when low-memory mode is
on. The key is omitempty and pinned after user fragments merge, so nothing
changes for anyone who does not opt in, and no fragment can undo it.
No new calls to Mmap/Munmap/Madvise, no swap or mmap anywhere in the tree.
* fix(ci): keep the new scratch directory out of the installer's own paths
Two problems CI caught on the small-memory change, both from moving the
scratch directory from mktemp into DATA_DIR.
restore_files() rebuilds the data directory by deleting everything in it
except backups. The scratch directory now lives inside it, so a rollback
deleted the very directory it was reading its own rollback out of, and the
restore died looking for restore/ENABLED. Rollback therefore had to be
skipped, exempting the scratch directory from that purge; the cleanup trap
already removes it.
The test curl also modelled only one of curl's output modes. Writing to
stdout and answering -I are both normal curl behaviour, and both are now
covered, so the mock no longer pins the installer to a single invocation
shape. The distribution fake likewise emits its tampered asset on stdout
instead of through --output, keeping that test about checksum rejection
rather than about which flag was used.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
2026-09-27 09:27:35 +08:00
風雪and風雪 a089b14908 fix(metrics): measure process and system CPU by delta, honour cgroup limits (#82)
* fix(metrics): measure process and system CPU by delta, honour cgroup limits
The process card called gopsutil's Process.CPUPercent(), which returns
100 * lifetimeCPUTime / uptime -- an average over the process' entire
history rather than what it is doing now. Two failure modes followed:
- a long-lived panel settles on a near-constant figure that lags real
spikes by hours;
- when the start time cannot be resolved (BootTime from sysinfo
disagrees with /proc/stat inside some containers) totalTime goes
negative and the call silently yields 0. Measured here: a permanent
0.0% regardless of load.
The system card had the opposite bias inside a container: cpu.Percent()
reads /proc/stat, so a container capped at 4 of 32 host cores topped out
around 21% while actually being throttled. It also only described 200ms
of each second and blocked the request thread doing it.
Both cards now difference cumulative counters across two samples:
- CPU:     cgroup v2 cpu.stat / v1 cpuacct.usage, host /proc/stat fallback
- process: /proc/<pid>/stat user+system (Iowait excluded: it is block
I/O delay, waiting rather than consuming)
- shared denominator cpuCapacity() = min(CFS quota, cpuset, online CPUs)
so the process and system cards finally speak one unit.
Measured on a 4-of-32-core container:
system CPU busy    21%  -> 100%
process CPU busy    0%  ->  89%   (returns to ~0% once idle)
Also fixed along the way:
- GetSystemStats sampled outside its lock, so two concurrent requests
differenced the network counters over ~0s and reported huge spikes.
Sampling now serialises behind a barrier with a minimum interval.
- The disk card hard-coded "/" even when the SQLite database lives on a
mounted volume; it now anchors on the database directory, which
disk.Usage resolves to its containing filesystem via statfs.
- The Telegram resource alerts used the same broken samplers, so a CPU
threshold inside a limited container could never be reached and the
alert would never fire.
New tests cover load tracking, cgroup and quota arithmetic, cpuset
parsing and the already-guarded process memory denominator invariant.
Full suite plus -race passes; darwin/linux-arm64/linux-386 cross-compile.
* fix(memory): report cgroup working set instead of gross usage
The previous commit moved the memory card onto cgroup accounting, which
fixed the two cards disagreeing about their source but swapped in a new
error on the other side: it used memory.current verbatim, and that is a
gross figure billing the cgroup for reclaimable page cache.
Measured on this host against an 8GB cap:
memory.current        4.44 GB
anon                0.90 GB   <- real process memory
inactive_file       3.05 GB   <- reclaimable by the kernel on demand
current / limit       51.7%     <- what the card showed
working set / limit   16.2%     <- actual pressure
So the bar sat in the orange band (>=50%) while 84% of the budget was
free. Reading a large file once -- mihomo's geoip database, a rule
provider download, SQLite growth -- is enough to trigger it, because the
kernel keeps those pages cached and simply hands them back under demand.
"Used" is now the kubelet working-set figure, usage minus inactive_file,
which is what container runtimes report as a percentage. Only *inactive*
cache is discounted, so pages actively mapped and in use stay charged and
the bar can still hold every process' RSS. Three related points:
- The label finally means the same thing on both branches. The
/proc/meminfo fallback already excluded buffers and cache, so gross
cgroup usage and host "used" were two different quantities behind one
label depending on how the panel was deployed.
- The denominator no longer requires a cap to use cgroup accounting. An
unlimited container previously fell back to host-wide figures and so
tracked other tenants' activity, while the CPU card (which always
uses the container's own counter) did not. It falls back to the host
total now only because that genuinely is the cap.
- PAGE_COUNTER_MAX from cgroup v1 is still treated as "no limit".
New tests pin the arithmetic against the real numbers above, the three
ways the limit resolves, and the clamp when cache exceeds usage.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
fix(metrics): measure process and system CPU by delta, honour cgroup limits (#82)
* fix(metrics): measure process and system CPU by delta, honour cgroup limits
The process card called gopsutil's Process.CPUPercent(), which returns
100 * lifetimeCPUTime / uptime -- an average over the process' entire
history rather than what it is doing now. Two failure modes followed:
- a long-lived panel settles on a near-constant figure that lags real
spikes by hours;
- when the start time cannot be resolved (BootTime from sysinfo
disagrees with /proc/stat inside some containers) totalTime goes
negative and the call silently yields 0. Measured here: a permanent
0.0% regardless of load.
The system card had the opposite bias inside a container: cpu.Percent()
reads /proc/stat, so a container capped at 4 of 32 host cores topped out
around 21% while actually being throttled. It also only described 200ms
of each second and blocked the request thread doing it.
Both cards now difference cumulative counters across two samples:
- CPU:     cgroup v2 cpu.stat / v1 cpuacct.usage, host /proc/stat fallback
- process: /proc/<pid>/stat user+system (Iowait excluded: it is block
I/O delay, waiting rather than consuming)
- shared denominator cpuCapacity() = min(CFS quota, cpuset, online CPUs)
so the process and system cards finally speak one unit.
Measured on a 4-of-32-core container:
system CPU busy    21%  -> 100%
process CPU busy    0%  ->  89%   (returns to ~0% once idle)
Also fixed along the way:
- GetSystemStats sampled outside its lock, so two concurrent requests
differenced the network counters over ~0s and reported huge spikes.
Sampling now serialises behind a barrier with a minimum interval.
- The disk card hard-coded "/" even when the SQLite database lives on a
mounted volume; it now anchors on the database directory, which
disk.Usage resolves to its containing filesystem via statfs.
- The Telegram resource alerts used the same broken samplers, so a CPU
threshold inside a limited container could never be reached and the
alert would never fire.
New tests cover load tracking, cgroup and quota arithmetic, cpuset
parsing and the already-guarded process memory denominator invariant.
Full suite plus -race passes; darwin/linux-arm64/linux-386 cross-compile.
* fix(memory): report cgroup working set instead of gross usage
The previous commit moved the memory card onto cgroup accounting, which
fixed the two cards disagreeing about their source but swapped in a new
error on the other side: it used memory.current verbatim, and that is a
gross figure billing the cgroup for reclaimable page cache.
Measured on this host against an 8GB cap:
memory.current        4.44 GB
anon                0.90 GB   <- real process memory
inactive_file       3.05 GB   <- reclaimable by the kernel on demand
current / limit       51.7%     <- what the card showed
working set / limit   16.2%     <- actual pressure
So the bar sat in the orange band (>=50%) while 84% of the budget was
free. Reading a large file once -- mihomo's geoip database, a rule
provider download, SQLite growth -- is enough to trigger it, because the
kernel keeps those pages cached and simply hands them back under demand.
"Used" is now the kubelet working-set figure, usage minus inactive_file,
which is what container runtimes report as a percentage. Only *inactive*
cache is discounted, so pages actively mapped and in use stay charged and
the bar can still hold every process' RSS. Three related points:
- The label finally means the same thing on both branches. The
/proc/meminfo fallback already excluded buffers and cache, so gross
cgroup usage and host "used" were two different quantities behind one
label depending on how the panel was deployed.
- The denominator no longer requires a cap to use cgroup accounting. An
unlimited container previously fell back to host-wide figures and so
tracked other tenants' activity, while the CPU card (which always
uses the container's own counter) did not. It falls back to the host
total now only because that genuinely is the cap.
- PAGE_COUNTER_MAX from cgroup v1 is still treated as "no limit".
New tests pin the arithmetic against the real numbers above, the three
ways the limit resolves, and the clamp when cache exceeds usage.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
fix(metrics): measure process and system CPU by delta, honour cgroup limits (#82)
* fix(metrics): measure process and system CPU by delta, honour cgroup limits
The process card called gopsutil's Process.CPUPercent(), which returns
100 * lifetimeCPUTime / uptime -- an average over the process' entire
history rather than what it is doing now. Two failure modes followed:
- a long-lived panel settles on a near-constant figure that lags real
spikes by hours;
- when the start time cannot be resolved (BootTime from sysinfo
disagrees with /proc/stat inside some containers) totalTime goes
negative and the call silently yields 0. Measured here: a permanent
0.0% regardless of load.
The system card had the opposite bias inside a container: cpu.Percent()
reads /proc/stat, so a container capped at 4 of 32 host cores topped out
around 21% while actually being throttled. It also only described 200ms
of each second and blocked the request thread doing it.
Both cards now difference cumulative counters across two samples:
- CPU:     cgroup v2 cpu.stat / v1 cpuacct.usage, host /proc/stat fallback
- process: /proc/<pid>/stat user+system (Iowait excluded: it is block
I/O delay, waiting rather than consuming)
- shared denominator cpuCapacity() = min(CFS quota, cpuset, online CPUs)
so the process and system cards finally speak one unit.
Measured on a 4-of-32-core container:
system CPU busy    21%  -> 100%
process CPU busy    0%  ->  89%   (returns to ~0% once idle)
Also fixed along the way:
- GetSystemStats sampled outside its lock, so two concurrent requests
differenced the network counters over ~0s and reported huge spikes.
Sampling now serialises behind a barrier with a minimum interval.
- The disk card hard-coded "/" even when the SQLite database lives on a
mounted volume; it now anchors on the database directory, which
disk.Usage resolves to its containing filesystem via statfs.
- The Telegram resource alerts used the same broken samplers, so a CPU
threshold inside a limited container could never be reached and the
alert would never fire.
New tests cover load tracking, cgroup and quota arithmetic, cpuset
parsing and the already-guarded process memory denominator invariant.
Full suite plus -race passes; darwin/linux-arm64/linux-386 cross-compile.
* fix(memory): report cgroup working set instead of gross usage
The previous commit moved the memory card onto cgroup accounting, which
fixed the two cards disagreeing about their source but swapped in a new
error on the other side: it used memory.current verbatim, and that is a
gross figure billing the cgroup for reclaimable page cache.
Measured on this host against an 8GB cap:
memory.current        4.44 GB
anon                0.90 GB   <- real process memory
inactive_file       3.05 GB   <- reclaimable by the kernel on demand
current / limit       51.7%     <- what the card showed
working set / limit   16.2%     <- actual pressure
So the bar sat in the orange band (>=50%) while 84% of the budget was
free. Reading a large file once -- mihomo's geoip database, a rule
provider download, SQLite growth -- is enough to trigger it, because the
kernel keeps those pages cached and simply hands them back under demand.
"Used" is now the kubelet working-set figure, usage minus inactive_file,
which is what container runtimes report as a percentage. Only *inactive*
cache is discounted, so pages actively mapped and in use stay charged and
the bar can still hold every process' RSS. Three related points:
- The label finally means the same thing on both branches. The
/proc/meminfo fallback already excluded buffers and cache, so gross
cgroup usage and host "used" were two different quantities behind one
label depending on how the panel was deployed.
- The denominator no longer requires a cap to use cgroup accounting. An
unlimited container previously fell back to host-wide figures and so
tracked other tenants' activity, while the CPU card (which always
uses the container's own counter) did not. It falls back to the host
total now only because that genuinely is the cap.
- PAGE_COUNTER_MAX from cgroup v1 is still treated as "no limit".
New tests pin the arithmetic against the real numbers above, the three
ways the limit resolves, and the clamp when cache exceeds usage.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
fix(metrics): measure process and system CPU by delta, honour cgroup limits (#82)
* fix(metrics): measure process and system CPU by delta, honour cgroup limits
The process card called gopsutil's Process.CPUPercent(), which returns
100 * lifetimeCPUTime / uptime -- an average over the process' entire
history rather than what it is doing now. Two failure modes followed:
- a long-lived panel settles on a near-constant figure that lags real
spikes by hours;
- when the start time cannot be resolved (BootTime from sysinfo
disagrees with /proc/stat inside some containers) totalTime goes
negative and the call silently yields 0. Measured here: a permanent
0.0% regardless of load.
The system card had the opposite bias inside a container: cpu.Percent()
reads /proc/stat, so a container capped at 4 of 32 host cores topped out
around 21% while actually being throttled. It also only described 200ms
of each second and blocked the request thread doing it.
Both cards now difference cumulative counters across two samples:
- CPU:     cgroup v2 cpu.stat / v1 cpuacct.usage, host /proc/stat fallback
- process: /proc/<pid>/stat user+system (Iowait excluded: it is block
I/O delay, waiting rather than consuming)
- shared denominator cpuCapacity() = min(CFS quota, cpuset, online CPUs)
so the process and system cards finally speak one unit.
Measured on a 4-of-32-core container:
system CPU busy    21%  -> 100%
process CPU busy    0%  ->  89%   (returns to ~0% once idle)
Also fixed along the way:
- GetSystemStats sampled outside its lock, so two concurrent requests
differenced the network counters over ~0s and reported huge spikes.
Sampling now serialises behind a barrier with a minimum interval.
- The disk card hard-coded "/" even when the SQLite database lives on a
mounted volume; it now anchors on the database directory, which
disk.Usage resolves to its containing filesystem via statfs.
- The Telegram resource alerts used the same broken samplers, so a CPU
threshold inside a limited container could never be reached and the
alert would never fire.
New tests cover load tracking, cgroup and quota arithmetic, cpuset
parsing and the already-guarded process memory denominator invariant.
Full suite plus -race passes; darwin/linux-arm64/linux-386 cross-compile.
* fix(memory): report cgroup working set instead of gross usage
The previous commit moved the memory card onto cgroup accounting, which
fixed the two cards disagreeing about their source but swapped in a new
error on the other side: it used memory.current verbatim, and that is a
gross figure billing the cgroup for reclaimable page cache.
Measured on this host against an 8GB cap:
memory.current        4.44 GB
anon                0.90 GB   <- real process memory
inactive_file       3.05 GB   <- reclaimable by the kernel on demand
current / limit       51.7%     <- what the card showed
working set / limit   16.2%     <- actual pressure
So the bar sat in the orange band (>=50%) while 84% of the budget was
free. Reading a large file once -- mihomo's geoip database, a rule
provider download, SQLite growth -- is enough to trigger it, because the
kernel keeps those pages cached and simply hands them back under demand.
"Used" is now the kubelet working-set figure, usage minus inactive_file,
which is what container runtimes report as a percentage. Only *inactive*
cache is discounted, so pages actively mapped and in use stay charged and
the bar can still hold every process' RSS. Three related points:
- The label finally means the same thing on both branches. The
/proc/meminfo fallback already excluded buffers and cache, so gross
cgroup usage and host "used" were two different quantities behind one
label depending on how the panel was deployed.
- The denominator no longer requires a cap to use cgroup accounting. An
unlimited container previously fell back to host-wide figures and so
tracked other tenants' activity, while the CPU card (which always
uses the container's own counter) did not. It falls back to the host
total now only because that genuinely is the cap.
- PAGE_COUNTER_MAX from cgroup v1 is still treated as "no limit".
New tests pin the arithmetic against the real numbers above, the three
ways the limit resolves, and the clamp when cache exceeds usage.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
fix(metrics): measure process and system CPU by delta, honour cgroup limits (#82)
* fix(metrics): measure process and system CPU by delta, honour cgroup limits
The process card called gopsutil's Process.CPUPercent(), which returns
100 * lifetimeCPUTime / uptime -- an average over the process' entire
history rather than what it is doing now. Two failure modes followed:
- a long-lived panel settles on a near-constant figure that lags real
spikes by hours;
- when the start time cannot be resolved (BootTime from sysinfo
disagrees with /proc/stat inside some containers) totalTime goes
negative and the call silently yields 0. Measured here: a permanent
0.0% regardless of load.
The system card had the opposite bias inside a container: cpu.Percent()
reads /proc/stat, so a container capped at 4 of 32 host cores topped out
around 21% while actually being throttled. It also only described 200ms
of each second and blocked the request thread doing it.
Both cards now difference cumulative counters across two samples:
- CPU:     cgroup v2 cpu.stat / v1 cpuacct.usage, host /proc/stat fallback
- process: /proc/<pid>/stat user+system (Iowait excluded: it is block
I/O delay, waiting rather than consuming)
- shared denominator cpuCapacity() = min(CFS quota, cpuset, online CPUs)
so the process and system cards finally speak one unit.
Measured on a 4-of-32-core container:
system CPU busy    21%  -> 100%
process CPU busy    0%  ->  89%   (returns to ~0% once idle)
Also fixed along the way:
- GetSystemStats sampled outside its lock, so two concurrent requests
differenced the network counters over ~0s and reported huge spikes.
Sampling now serialises behind a barrier with a minimum interval.
- The disk card hard-coded "/" even when the SQLite database lives on a
mounted volume; it now anchors on the database directory, which
disk.Usage resolves to its containing filesystem via statfs.
- The Telegram resource alerts used the same broken samplers, so a CPU
threshold inside a limited container could never be reached and the
alert would never fire.
New tests cover load tracking, cgroup and quota arithmetic, cpuset
parsing and the already-guarded process memory denominator invariant.
Full suite plus -race passes; darwin/linux-arm64/linux-386 cross-compile.
* fix(memory): report cgroup working set instead of gross usage
The previous commit moved the memory card onto cgroup accounting, which
fixed the two cards disagreeing about their source but swapped in a new
error on the other side: it used memory.current verbatim, and that is a
gross figure billing the cgroup for reclaimable page cache.
Measured on this host against an 8GB cap:
memory.current        4.44 GB
anon                0.90 GB   <- real process memory
inactive_file       3.05 GB   <- reclaimable by the kernel on demand
current / limit       51.7%     <- what the card showed
working set / limit   16.2%     <- actual pressure
So the bar sat in the orange band (>=50%) while 84% of the budget was
free. Reading a large file once -- mihomo's geoip database, a rule
provider download, SQLite growth -- is enough to trigger it, because the
kernel keeps those pages cached and simply hands them back under demand.
"Used" is now the kubelet working-set figure, usage minus inactive_file,
which is what container runtimes report as a percentage. Only *inactive*
cache is discounted, so pages actively mapped and in use stay charged and
the bar can still hold every process' RSS. Three related points:
- The label finally means the same thing on both branches. The
/proc/meminfo fallback already excluded buffers and cache, so gross
cgroup usage and host "used" were two different quantities behind one
label depending on how the panel was deployed.
- The denominator no longer requires a cap to use cgroup accounting. An
unlimited container previously fell back to host-wide figures and so
tracked other tenants' activity, while the CPU card (which always
uses the container's own counter) did not. It falls back to the host
total now only because that genuinely is the cap.
- PAGE_COUNTER_MAX from cgroup v1 is still treated as "no limit".
New tests pin the arithmetic against the real numbers above, the three
ways the limit resolves, and the clamp when cache exceeds usage.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
fix(metrics): measure process and system CPU by delta, honour cgroup limits (#82)
* fix(metrics): measure process and system CPU by delta, honour cgroup limits
The process card called gopsutil's Process.CPUPercent(), which returns
100 * lifetimeCPUTime / uptime -- an average over the process' entire
history rather than what it is doing now. Two failure modes followed:
- a long-lived panel settles on a near-constant figure that lags real
spikes by hours;
- when the start time cannot be resolved (BootTime from sysinfo
disagrees with /proc/stat inside some containers) totalTime goes
negative and the call silently yields 0. Measured here: a permanent
0.0% regardless of load.
The system card had the opposite bias inside a container: cpu.Percent()
reads /proc/stat, so a container capped at 4 of 32 host cores topped out
around 21% while actually being throttled. It also only described 200ms
of each second and blocked the request thread doing it.
Both cards now difference cumulative counters across two samples:
- CPU:     cgroup v2 cpu.stat / v1 cpuacct.usage, host /proc/stat fallback
- process: /proc/<pid>/stat user+system (Iowait excluded: it is block
I/O delay, waiting rather than consuming)
- shared denominator cpuCapacity() = min(CFS quota, cpuset, online CPUs)
so the process and system cards finally speak one unit.
Measured on a 4-of-32-core container:
system CPU busy    21%  -> 100%
process CPU busy    0%  ->  89%   (returns to ~0% once idle)
Also fixed along the way:
- GetSystemStats sampled outside its lock, so two concurrent requests
differenced the network counters over ~0s and reported huge spikes.
Sampling now serialises behind a barrier with a minimum interval.
- The disk card hard-coded "/" even when the SQLite database lives on a
mounted volume; it now anchors on the database directory, which
disk.Usage resolves to its containing filesystem via statfs.
- The Telegram resource alerts used the same broken samplers, so a CPU
threshold inside a limited container could never be reached and the
alert would never fire.
New tests cover load tracking, cgroup and quota arithmetic, cpuset
parsing and the already-guarded process memory denominator invariant.
Full suite plus -race passes; darwin/linux-arm64/linux-386 cross-compile.
* fix(memory): report cgroup working set instead of gross usage
The previous commit moved the memory card onto cgroup accounting, which
fixed the two cards disagreeing about their source but swapped in a new
error on the other side: it used memory.current verbatim, and that is a
gross figure billing the cgroup for reclaimable page cache.
Measured on this host against an 8GB cap:
memory.current        4.44 GB
anon                0.90 GB   <- real process memory
inactive_file       3.05 GB   <- reclaimable by the kernel on demand
current / limit       51.7%     <- what the card showed
working set / limit   16.2%     <- actual pressure
So the bar sat in the orange band (>=50%) while 84% of the budget was
free. Reading a large file once -- mihomo's geoip database, a rule
provider download, SQLite growth -- is enough to trigger it, because the
kernel keeps those pages cached and simply hands them back under demand.
"Used" is now the kubelet working-set figure, usage minus inactive_file,
which is what container runtimes report as a percentage. Only *inactive*
cache is discounted, so pages actively mapped and in use stay charged and
the bar can still hold every process' RSS. Three related points:
- The label finally means the same thing on both branches. The
/proc/meminfo fallback already excluded buffers and cache, so gross
cgroup usage and host "used" were two different quantities behind one
label depending on how the panel was deployed.
- The denominator no longer requires a cap to use cgroup accounting. An
unlimited container previously fell back to host-wide figures and so
tracked other tenants' activity, while the CPU card (which always
uses the container's own counter) did not. It falls back to the host
total now only because that genuinely is the cap.
- PAGE_COUNTER_MAX from cgroup v1 is still treated as "no limit".
New tests pin the arithmetic against the real numbers above, the three
ways the limit resolves, and the clamp when cache exceeds usage.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
fix(metrics): measure process and system CPU by delta, honour cgroup limits (#82)
* fix(metrics): measure process and system CPU by delta, honour cgroup limits
The process card called gopsutil's Process.CPUPercent(), which returns
100 * lifetimeCPUTime / uptime -- an average over the process' entire
history rather than what it is doing now. Two failure modes followed:
- a long-lived panel settles on a near-constant figure that lags real
spikes by hours;
- when the start time cannot be resolved (BootTime from sysinfo
disagrees with /proc/stat inside some containers) totalTime goes
negative and the call silently yields 0. Measured here: a permanent
0.0% regardless of load.
The system card had the opposite bias inside a container: cpu.Percent()
reads /proc/stat, so a container capped at 4 of 32 host cores topped out
around 21% while actually being throttled. It also only described 200ms
of each second and blocked the request thread doing it.
Both cards now difference cumulative counters across two samples:
- CPU:     cgroup v2 cpu.stat / v1 cpuacct.usage, host /proc/stat fallback
- process: /proc/<pid>/stat user+system (Iowait excluded: it is block
I/O delay, waiting rather than consuming)
- shared denominator cpuCapacity() = min(CFS quota, cpuset, online CPUs)
so the process and system cards finally speak one unit.
Measured on a 4-of-32-core container:
system CPU busy    21%  -> 100%
process CPU busy    0%  ->  89%   (returns to ~0% once idle)
Also fixed along the way:
- GetSystemStats sampled outside its lock, so two concurrent requests
differenced the network counters over ~0s and reported huge spikes.
Sampling now serialises behind a barrier with a minimum interval.
- The disk card hard-coded "/" even when the SQLite database lives on a
mounted volume; it now anchors on the database directory, which
disk.Usage resolves to its containing filesystem via statfs.
- The Telegram resource alerts used the same broken samplers, so a CPU
threshold inside a limited container could never be reached and the
alert would never fire.
New tests cover load tracking, cgroup and quota arithmetic, cpuset
parsing and the already-guarded process memory denominator invariant.
Full suite plus -race passes; darwin/linux-arm64/linux-386 cross-compile.
* fix(memory): report cgroup working set instead of gross usage
The previous commit moved the memory card onto cgroup accounting, which
fixed the two cards disagreeing about their source but swapped in a new
error on the other side: it used memory.current verbatim, and that is a
gross figure billing the cgroup for reclaimable page cache.
Measured on this host against an 8GB cap:
memory.current        4.44 GB
anon                0.90 GB   <- real process memory
inactive_file       3.05 GB   <- reclaimable by the kernel on demand
current / limit       51.7%     <- what the card showed
working set / limit   16.2%     <- actual pressure
So the bar sat in the orange band (>=50%) while 84% of the budget was
free. Reading a large file once -- mihomo's geoip database, a rule
provider download, SQLite growth -- is enough to trigger it, because the
kernel keeps those pages cached and simply hands them back under demand.
"Used" is now the kubelet working-set figure, usage minus inactive_file,
which is what container runtimes report as a percentage. Only *inactive*
cache is discounted, so pages actively mapped and in use stay charged and
the bar can still hold every process' RSS. Three related points:
- The label finally means the same thing on both branches. The
/proc/meminfo fallback already excluded buffers and cache, so gross
cgroup usage and host "used" were two different quantities behind one
label depending on how the panel was deployed.
- The denominator no longer requires a cap to use cgroup accounting. An
unlimited container previously fell back to host-wide figures and so
tracked other tenants' activity, while the CPU card (which always
uses the container's own counter) did not. It falls back to the host
total now only because that genuinely is the cap.
- PAGE_COUNTER_MAX from cgroup v1 is still treated as "no limit".
New tests pin the arithmetic against the real numbers above, the three
ways the limit resolves, and the clamp when cache exceeds usage.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
fix(metrics): measure process and system CPU by delta, honour cgroup limits (#82)
* fix(metrics): measure process and system CPU by delta, honour cgroup limits
The process card called gopsutil's Process.CPUPercent(), which returns
100 * lifetimeCPUTime / uptime -- an average over the process' entire
history rather than what it is doing now. Two failure modes followed:
- a long-lived panel settles on a near-constant figure that lags real
spikes by hours;
- when the start time cannot be resolved (BootTime from sysinfo
disagrees with /proc/stat inside some containers) totalTime goes
negative and the call silently yields 0. Measured here: a permanent
0.0% regardless of load.
The system card had the opposite bias inside a container: cpu.Percent()
reads /proc/stat, so a container capped at 4 of 32 host cores topped out
around 21% while actually being throttled. It also only described 200ms
of each second and blocked the request thread doing it.
Both cards now difference cumulative counters across two samples:
- CPU:     cgroup v2 cpu.stat / v1 cpuacct.usage, host /proc/stat fallback
- process: /proc/<pid>/stat user+system (Iowait excluded: it is block
I/O delay, waiting rather than consuming)
- shared denominator cpuCapacity() = min(CFS quota, cpuset, online CPUs)
so the process and system cards finally speak one unit.
Measured on a 4-of-32-core container:
system CPU busy    21%  -> 100%
process CPU busy    0%  ->  89%   (returns to ~0% once idle)
Also fixed along the way:
- GetSystemStats sampled outside its lock, so two concurrent requests
differenced the network counters over ~0s and reported huge spikes.
Sampling now serialises behind a barrier with a minimum interval.
- The disk card hard-coded "/" even when the SQLite database lives on a
mounted volume; it now anchors on the database directory, which
disk.Usage resolves to its containing filesystem via statfs.
- The Telegram resource alerts used the same broken samplers, so a CPU
threshold inside a limited container could never be reached and the
alert would never fire.
New tests cover load tracking, cgroup and quota arithmetic, cpuset
parsing and the already-guarded process memory denominator invariant.
Full suite plus -race passes; darwin/linux-arm64/linux-386 cross-compile.
* fix(memory): report cgroup working set instead of gross usage
The previous commit moved the memory card onto cgroup accounting, which
fixed the two cards disagreeing about their source but swapped in a new
error on the other side: it used memory.current verbatim, and that is a
gross figure billing the cgroup for reclaimable page cache.
Measured on this host against an 8GB cap:
memory.current        4.44 GB
anon                0.90 GB   <- real process memory
inactive_file       3.05 GB   <- reclaimable by the kernel on demand
current / limit       51.7%     <- what the card showed
working set / limit   16.2%     <- actual pressure
So the bar sat in the orange band (>=50%) while 84% of the budget was
free. Reading a large file once -- mihomo's geoip database, a rule
provider download, SQLite growth -- is enough to trigger it, because the
kernel keeps those pages cached and simply hands them back under demand.
"Used" is now the kubelet working-set figure, usage minus inactive_file,
which is what container runtimes report as a percentage. Only *inactive*
cache is discounted, so pages actively mapped and in use stay charged and
the bar can still hold every process' RSS. Three related points:
- The label finally means the same thing on both branches. The
/proc/meminfo fallback already excluded buffers and cache, so gross
cgroup usage and host "used" were two different quantities behind one
label depending on how the panel was deployed.
- The denominator no longer requires a cap to use cgroup accounting. An
unlimited container previously fell back to host-wide figures and so
tracked other tenants' activity, while the CPU card (which always
uses the container's own counter) did not. It falls back to the host
total now only because that genuinely is the cap.
- PAGE_COUNTER_MAX from cgroup v1 is still treated as "no limit".
New tests pin the arithmetic against the real numbers above, the three
ways the limit resolves, and the clamp when cache exceeds usage.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
fix(metrics): measure process and system CPU by delta, honour cgroup limits (#82)
* fix(metrics): measure process and system CPU by delta, honour cgroup limits
The process card called gopsutil's Process.CPUPercent(), which returns
100 * lifetimeCPUTime / uptime -- an average over the process' entire
history rather than what it is doing now. Two failure modes followed:
- a long-lived panel settles on a near-constant figure that lags real
spikes by hours;
- when the start time cannot be resolved (BootTime from sysinfo
disagrees with /proc/stat inside some containers) totalTime goes
negative and the call silently yields 0. Measured here: a permanent
0.0% regardless of load.
The system card had the opposite bias inside a container: cpu.Percent()
reads /proc/stat, so a container capped at 4 of 32 host cores topped out
around 21% while actually being throttled. It also only described 200ms
of each second and blocked the request thread doing it.
Both cards now difference cumulative counters across two samples:
- CPU:     cgroup v2 cpu.stat / v1 cpuacct.usage, host /proc/stat fallback
- process: /proc/<pid>/stat user+system (Iowait excluded: it is block
I/O delay, waiting rather than consuming)
- shared denominator cpuCapacity() = min(CFS quota, cpuset, online CPUs)
so the process and system cards finally speak one unit.
Measured on a 4-of-32-core container:
system CPU busy    21%  -> 100%
process CPU busy    0%  ->  89%   (returns to ~0% once idle)
Also fixed along the way:
- GetSystemStats sampled outside its lock, so two concurrent requests
differenced the network counters over ~0s and reported huge spikes.
Sampling now serialises behind a barrier with a minimum interval.
- The disk card hard-coded "/" even when the SQLite database lives on a
mounted volume; it now anchors on the database directory, which
disk.Usage resolves to its containing filesystem via statfs.
- The Telegram resource alerts used the same broken samplers, so a CPU
threshold inside a limited container could never be reached and the
alert would never fire.
New tests cover load tracking, cgroup and quota arithmetic, cpuset
parsing and the already-guarded process memory denominator invariant.
Full suite plus -race passes; darwin/linux-arm64/linux-386 cross-compile.
* fix(memory): report cgroup working set instead of gross usage
The previous commit moved the memory card onto cgroup accounting, which
fixed the two cards disagreeing about their source but swapped in a new
error on the other side: it used memory.current verbatim, and that is a
gross figure billing the cgroup for reclaimable page cache.
Measured on this host against an 8GB cap:
memory.current        4.44 GB
anon                0.90 GB   <- real process memory
inactive_file       3.05 GB   <- reclaimable by the kernel on demand
current / limit       51.7%     <- what the card showed
working set / limit   16.2%     <- actual pressure
So the bar sat in the orange band (>=50%) while 84% of the budget was
free. Reading a large file once -- mihomo's geoip database, a rule
provider download, SQLite growth -- is enough to trigger it, because the
kernel keeps those pages cached and simply hands them back under demand.
"Used" is now the kubelet working-set figure, usage minus inactive_file,
which is what container runtimes report as a percentage. Only *inactive*
cache is discounted, so pages actively mapped and in use stay charged and
the bar can still hold every process' RSS. Three related points:
- The label finally means the same thing on both branches. The
/proc/meminfo fallback already excluded buffers and cache, so gross
cgroup usage and host "used" were two different quantities behind one
label depending on how the panel was deployed.
- The denominator no longer requires a cap to use cgroup accounting. An
unlimited container previously fell back to host-wide figures and so
tracked other tenants' activity, while the CPU card (which always
uses the container's own counter) did not. It falls back to the host
total now only because that genuinely is the cap.
- PAGE_COUNTER_MAX from cgroup v1 is still treated as "no limit".
New tests pin the arithmetic against the real numbers above, the three
ways the limit resolves, and the clamp when cache exceeds usage.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
fix(metrics): measure process and system CPU by delta, honour cgroup limits (#82)
* fix(metrics): measure process and system CPU by delta, honour cgroup limits
The process card called gopsutil's Process.CPUPercent(), which returns
100 * lifetimeCPUTime / uptime -- an average over the process' entire
history rather than what it is doing now. Two failure modes followed:
- a long-lived panel settles on a near-constant figure that lags real
spikes by hours;
- when the start time cannot be resolved (BootTime from sysinfo
disagrees with /proc/stat inside some containers) totalTime goes
negative and the call silently yields 0. Measured here: a permanent
0.0% regardless of load.
The system card had the opposite bias inside a container: cpu.Percent()
reads /proc/stat, so a container capped at 4 of 32 host cores topped out
around 21% while actually being throttled. It also only described 200ms
of each second and blocked the request thread doing it.
Both cards now difference cumulative counters across two samples:
- CPU:     cgroup v2 cpu.stat / v1 cpuacct.usage, host /proc/stat fallback
- process: /proc/<pid>/stat user+system (Iowait excluded: it is block
I/O delay, waiting rather than consuming)
- shared denominator cpuCapacity() = min(CFS quota, cpuset, online CPUs)
so the process and system cards finally speak one unit.
Measured on a 4-of-32-core container:
system CPU busy    21%  -> 100%
process CPU busy    0%  ->  89%   (returns to ~0% once idle)
Also fixed along the way:
- GetSystemStats sampled outside its lock, so two concurrent requests
differenced the network counters over ~0s and reported huge spikes.
Sampling now serialises behind a barrier with a minimum interval.
- The disk card hard-coded "/" even when the SQLite database lives on a
mounted volume; it now anchors on the database directory, which
disk.Usage resolves to its containing filesystem via statfs.
- The Telegram resource alerts used the same broken samplers, so a CPU
threshold inside a limited container could never be reached and the
alert would never fire.
New tests cover load tracking, cgroup and quota arithmetic, cpuset
parsing and the already-guarded process memory denominator invariant.
Full suite plus -race passes; darwin/linux-arm64/linux-386 cross-compile.
* fix(memory): report cgroup working set instead of gross usage
The previous commit moved the memory card onto cgroup accounting, which
fixed the two cards disagreeing about their source but swapped in a new
error on the other side: it used memory.current verbatim, and that is a
gross figure billing the cgroup for reclaimable page cache.
Measured on this host against an 8GB cap:
memory.current        4.44 GB
anon                0.90 GB   <- real process memory
inactive_file       3.05 GB   <- reclaimable by the kernel on demand
current / limit       51.7%     <- what the card showed
working set / limit   16.2%     <- actual pressure
So the bar sat in the orange band (>=50%) while 84% of the budget was
free. Reading a large file once -- mihomo's geoip database, a rule
provider download, SQLite growth -- is enough to trigger it, because the
kernel keeps those pages cached and simply hands them back under demand.
"Used" is now the kubelet working-set figure, usage minus inactive_file,
which is what container runtimes report as a percentage. Only *inactive*
cache is discounted, so pages actively mapped and in use stay charged and
the bar can still hold every process' RSS. Three related points:
- The label finally means the same thing on both branches. The
/proc/meminfo fallback already excluded buffers and cache, so gross
cgroup usage and host "used" were two different quantities behind one
label depending on how the panel was deployed.
- The denominator no longer requires a cap to use cgroup accounting. An
unlimited container previously fell back to host-wide figures and so
tracked other tenants' activity, while the CPU card (which always
uses the container's own counter) did not. It falls back to the host
total now only because that genuinely is the cap.
- PAGE_COUNTER_MAX from cgroup v1 is still treated as "no limit".
New tests pin the arithmetic against the real numbers above, the three
ways the limit resolves, and the clamp when cache exceeds usage.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
fix(metrics): measure process and system CPU by delta, honour cgroup limits (#82)
* fix(metrics): measure process and system CPU by delta, honour cgroup limits
The process card called gopsutil's Process.CPUPercent(), which returns
100 * lifetimeCPUTime / uptime -- an average over the process' entire
history rather than what it is doing now. Two failure modes followed:
- a long-lived panel settles on a near-constant figure that lags real
spikes by hours;
- when the start time cannot be resolved (BootTime from sysinfo
disagrees with /proc/stat inside some containers) totalTime goes
negative and the call silently yields 0. Measured here: a permanent
0.0% regardless of load.
The system card had the opposite bias inside a container: cpu.Percent()
reads /proc/stat, so a container capped at 4 of 32 host cores topped out
around 21% while actually being throttled. It also only described 200ms
of each second and blocked the request thread doing it.
Both cards now difference cumulative counters across two samples:
- CPU:     cgroup v2 cpu.stat / v1 cpuacct.usage, host /proc/stat fallback
- process: /proc/<pid>/stat user+system (Iowait excluded: it is block
I/O delay, waiting rather than consuming)
- shared denominator cpuCapacity() = min(CFS quota, cpuset, online CPUs)
so the process and system cards finally speak one unit.
Measured on a 4-of-32-core container:
system CPU busy    21%  -> 100%
process CPU busy    0%  ->  89%   (returns to ~0% once idle)
Also fixed along the way:
- GetSystemStats sampled outside its lock, so two concurrent requests
differenced the network counters over ~0s and reported huge spikes.
Sampling now serialises behind a barrier with a minimum interval.
- The disk card hard-coded "/" even when the SQLite database lives on a
mounted volume; it now anchors on the database directory, which
disk.Usage resolves to its containing filesystem via statfs.
- The Telegram resource alerts used the same broken samplers, so a CPU
threshold inside a limited container could never be reached and the
alert would never fire.
New tests cover load tracking, cgroup and quota arithmetic, cpuset
parsing and the already-guarded process memory denominator invariant.
Full suite plus -race passes; darwin/linux-arm64/linux-386 cross-compile.
* fix(memory): report cgroup working set instead of gross usage
The previous commit moved the memory card onto cgroup accounting, which
fixed the two cards disagreeing about their source but swapped in a new
error on the other side: it used memory.current verbatim, and that is a
gross figure billing the cgroup for reclaimable page cache.
Measured on this host against an 8GB cap:
memory.current        4.44 GB
anon                0.90 GB   <- real process memory
inactive_file       3.05 GB   <- reclaimable by the kernel on demand
current / limit       51.7%     <- what the card showed
working set / limit   16.2%     <- actual pressure
So the bar sat in the orange band (>=50%) while 84% of the budget was
free. Reading a large file once -- mihomo's geoip database, a rule
provider download, SQLite growth -- is enough to trigger it, because the
kernel keeps those pages cached and simply hands them back under demand.
"Used" is now the kubelet working-set figure, usage minus inactive_file,
which is what container runtimes report as a percentage. Only *inactive*
cache is discounted, so pages actively mapped and in use stay charged and
the bar can still hold every process' RSS. Three related points:
- The label finally means the same thing on both branches. The
/proc/meminfo fallback already excluded buffers and cache, so gross
cgroup usage and host "used" were two different quantities behind one
label depending on how the panel was deployed.
- The denominator no longer requires a cap to use cgroup accounting. An
unlimited container previously fell back to host-wide figures and so
tracked other tenants' activity, while the CPU card (which always
uses the container's own counter) did not. It falls back to the host
total now only because that genuinely is the cap.
- PAGE_COUNTER_MAX from cgroup v1 is still treated as "no limit".
New tests pin the arithmetic against the real numbers above, the three
ways the limit resolves, and the clamp when cache exceeds usage.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
fix(metrics): measure process and system CPU by delta, honour cgroup limits (#82)
* fix(metrics): measure process and system CPU by delta, honour cgroup limits
The process card called gopsutil's Process.CPUPercent(), which returns
100 * lifetimeCPUTime / uptime -- an average over the process' entire
history rather than what it is doing now. Two failure modes followed:
- a long-lived panel settles on a near-constant figure that lags real
spikes by hours;
- when the start time cannot be resolved (BootTime from sysinfo
disagrees with /proc/stat inside some containers) totalTime goes
negative and the call silently yields 0. Measured here: a permanent
0.0% regardless of load.
The system card had the opposite bias inside a container: cpu.Percent()
reads /proc/stat, so a container capped at 4 of 32 host cores topped out
around 21% while actually being throttled. It also only described 200ms
of each second and blocked the request thread doing it.
Both cards now difference cumulative counters across two samples:
- CPU:     cgroup v2 cpu.stat / v1 cpuacct.usage, host /proc/stat fallback
- process: /proc/<pid>/stat user+system (Iowait excluded: it is block
I/O delay, waiting rather than consuming)
- shared denominator cpuCapacity() = min(CFS quota, cpuset, online CPUs)
so the process and system cards finally speak one unit.
Measured on a 4-of-32-core container:
system CPU busy    21%  -> 100%
process CPU busy    0%  ->  89%   (returns to ~0% once idle)
Also fixed along the way:
- GetSystemStats sampled outside its lock, so two concurrent requests
differenced the network counters over ~0s and reported huge spikes.
Sampling now serialises behind a barrier with a minimum interval.
- The disk card hard-coded "/" even when the SQLite database lives on a
mounted volume; it now anchors on the database directory, which
disk.Usage resolves to its containing filesystem via statfs.
- The Telegram resource alerts used the same broken samplers, so a CPU
threshold inside a limited container could never be reached and the
alert would never fire.
New tests cover load tracking, cgroup and quota arithmetic, cpuset
parsing and the already-guarded process memory denominator invariant.
Full suite plus -race passes; darwin/linux-arm64/linux-386 cross-compile.
* fix(memory): report cgroup working set instead of gross usage
The previous commit moved the memory card onto cgroup accounting, which
fixed the two cards disagreeing about their source but swapped in a new
error on the other side: it used memory.current verbatim, and that is a
gross figure billing the cgroup for reclaimable page cache.
Measured on this host against an 8GB cap:
memory.current        4.44 GB
anon                0.90 GB   <- real process memory
inactive_file       3.05 GB   <- reclaimable by the kernel on demand
current / limit       51.7%     <- what the card showed
working set / limit   16.2%     <- actual pressure
So the bar sat in the orange band (>=50%) while 84% of the budget was
free. Reading a large file once -- mihomo's geoip database, a rule
provider download, SQLite growth -- is enough to trigger it, because the
kernel keeps those pages cached and simply hands them back under demand.
"Used" is now the kubelet working-set figure, usage minus inactive_file,
which is what container runtimes report as a percentage. Only *inactive*
cache is discounted, so pages actively mapped and in use stay charged and
the bar can still hold every process' RSS. Three related points:
- The label finally means the same thing on both branches. The
/proc/meminfo fallback already excluded buffers and cache, so gross
cgroup usage and host "used" were two different quantities behind one
label depending on how the panel was deployed.
- The denominator no longer requires a cap to use cgroup accounting. An
unlimited container previously fell back to host-wide figures and so
tracked other tenants' activity, while the CPU card (which always
uses the container's own counter) did not. It falls back to the host
total now only because that genuinely is the cap.
- PAGE_COUNTER_MAX from cgroup v1 is still treated as "no limit".
New tests pin the arithmetic against the real numbers above, the three
ways the limit resolves, and the clamp when cache exceeds usage.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
2026-09-27 00:10:01 +08:00
Z User ee5647710c fix(metrics): use cgroup memory accounting for consistent dashboard
System memory and process memory were reading different sources:
- System card: /proc/meminfo (host-wide, includes page cache)
- Process card: per-process RSS (includes shared libs)
This made system-used appear SMALLER than the sum of process RSS,
which is logically impossible and confused operators.
Fix: system memory now prefers cgroup accounting:
- cgroup v2: /sys/fs/cgroup/memory.current + memory.max
- cgroup v1: /sys/fs/cgroup/memory/memory.usage_in_bytes + limit_in_bytes
- Falls back to /proc/meminfo when cgroups unavailable (bare metal)
Process memory percent now uses the SAME total as the system card
(cgroup limit or host RAM), so both cards agree on what 100% means.
Added TestSystemMemoryMatchesProcessMemory to guard the invariant:
process RSS can never exceed system-used, and both use the same
denominator for percentage calculation.
Patch by user. Build + tests PASS (including new test).
fix(metrics): use cgroup memory accounting for consistent dashboard
System memory and process memory were reading different sources:
- System card: /proc/meminfo (host-wide, includes page cache)
- Process card: per-process RSS (includes shared libs)
This made system-used appear SMALLER than the sum of process RSS,
which is logically impossible and confused operators.
Fix: system memory now prefers cgroup accounting:
- cgroup v2: /sys/fs/cgroup/memory.current + memory.max
- cgroup v1: /sys/fs/cgroup/memory/memory.usage_in_bytes + limit_in_bytes
- Falls back to /proc/meminfo when cgroups unavailable (bare metal)
Process memory percent now uses the SAME total as the system card
(cgroup limit or host RAM), so both cards agree on what 100% means.
Added TestSystemMemoryMatchesProcessMemory to guard the invariant:
process RSS can never exceed system-used, and both use the same
denominator for percentage calculation.
Patch by user. Build + tests PASS (including new test).
fix(metrics): use cgroup memory accounting for consistent dashboard
System memory and process memory were reading different sources:
- System card: /proc/meminfo (host-wide, includes page cache)
- Process card: per-process RSS (includes shared libs)
This made system-used appear SMALLER than the sum of process RSS,
which is logically impossible and confused operators.
Fix: system memory now prefers cgroup accounting:
- cgroup v2: /sys/fs/cgroup/memory.current + memory.max
- cgroup v1: /sys/fs/cgroup/memory/memory.usage_in_bytes + limit_in_bytes
- Falls back to /proc/meminfo when cgroups unavailable (bare metal)
Process memory percent now uses the SAME total as the system card
(cgroup limit or host RAM), so both cards agree on what 100% means.
Added TestSystemMemoryMatchesProcessMemory to guard the invariant:
process RSS can never exceed system-used, and both use the same
denominator for percentage calculation.
Patch by user. Build + tests PASS (including new test).
fix(metrics): use cgroup memory accounting for consistent dashboard
System memory and process memory were reading different sources:
- System card: /proc/meminfo (host-wide, includes page cache)
- Process card: per-process RSS (includes shared libs)
This made system-used appear SMALLER than the sum of process RSS,
which is logically impossible and confused operators.
Fix: system memory now prefers cgroup accounting:
- cgroup v2: /sys/fs/cgroup/memory.current + memory.max
- cgroup v1: /sys/fs/cgroup/memory/memory.usage_in_bytes + limit_in_bytes
- Falls back to /proc/meminfo when cgroups unavailable (bare metal)
Process memory percent now uses the SAME total as the system card
(cgroup limit or host RAM), so both cards agree on what 100% means.
Added TestSystemMemoryMatchesProcessMemory to guard the invariant:
process RSS can never exceed system-used, and both use the same
denominator for percentage calculation.
Patch by user. Build + tests PASS (including new test).
fix(metrics): use cgroup memory accounting for consistent dashboard
System memory and process memory were reading different sources:
- System card: /proc/meminfo (host-wide, includes page cache)
- Process card: per-process RSS (includes shared libs)
This made system-used appear SMALLER than the sum of process RSS,
which is logically impossible and confused operators.
Fix: system memory now prefers cgroup accounting:
- cgroup v2: /sys/fs/cgroup/memory.current + memory.max
- cgroup v1: /sys/fs/cgroup/memory/memory.usage_in_bytes + limit_in_bytes
- Falls back to /proc/meminfo when cgroups unavailable (bare metal)
Process memory percent now uses the SAME total as the system card
(cgroup limit or host RAM), so both cards agree on what 100% means.
Added TestSystemMemoryMatchesProcessMemory to guard the invariant:
process RSS can never exceed system-used, and both use the same
denominator for percentage calculation.
Patch by user. Build + tests PASS (including new test).
fix(metrics): use cgroup memory accounting for consistent dashboard
System memory and process memory were reading different sources:
- System card: /proc/meminfo (host-wide, includes page cache)
- Process card: per-process RSS (includes shared libs)
This made system-used appear SMALLER than the sum of process RSS,
which is logically impossible and confused operators.
Fix: system memory now prefers cgroup accounting:
- cgroup v2: /sys/fs/cgroup/memory.current + memory.max
- cgroup v1: /sys/fs/cgroup/memory/memory.usage_in_bytes + limit_in_bytes
- Falls back to /proc/meminfo when cgroups unavailable (bare metal)
Process memory percent now uses the SAME total as the system card
(cgroup limit or host RAM), so both cards agree on what 100% means.
Added TestSystemMemoryMatchesProcessMemory to guard the invariant:
process RSS can never exceed system-used, and both use the same
denominator for percentage calculation.
Patch by user. Build + tests PASS (including new test).
fix(metrics): use cgroup memory accounting for consistent dashboard
System memory and process memory were reading different sources:
- System card: /proc/meminfo (host-wide, includes page cache)
- Process card: per-process RSS (includes shared libs)
This made system-used appear SMALLER than the sum of process RSS,
which is logically impossible and confused operators.
Fix: system memory now prefers cgroup accounting:
- cgroup v2: /sys/fs/cgroup/memory.current + memory.max
- cgroup v1: /sys/fs/cgroup/memory/memory.usage_in_bytes + limit_in_bytes
- Falls back to /proc/meminfo when cgroups unavailable (bare metal)
Process memory percent now uses the SAME total as the system card
(cgroup limit or host RAM), so both cards agree on what 100% means.
Added TestSystemMemoryMatchesProcessMemory to guard the invariant:
process RSS can never exceed system-used, and both use the same
denominator for percentage calculation.
Patch by user. Build + tests PASS (including new test).
fix(metrics): use cgroup memory accounting for consistent dashboard
System memory and process memory were reading different sources:
- System card: /proc/meminfo (host-wide, includes page cache)
- Process card: per-process RSS (includes shared libs)
This made system-used appear SMALLER than the sum of process RSS,
which is logically impossible and confused operators.
Fix: system memory now prefers cgroup accounting:
- cgroup v2: /sys/fs/cgroup/memory.current + memory.max
- cgroup v1: /sys/fs/cgroup/memory/memory.usage_in_bytes + limit_in_bytes
- Falls back to /proc/meminfo when cgroups unavailable (bare metal)
Process memory percent now uses the SAME total as the system card
(cgroup limit or host RAM), so both cards agree on what 100% means.
Added TestSystemMemoryMatchesProcessMemory to guard the invariant:
process RSS can never exceed system-used, and both use the same
denominator for percentage calculation.
Patch by user. Build + tests PASS (including new test).
fix(metrics): use cgroup memory accounting for consistent dashboard
System memory and process memory were reading different sources:
- System card: /proc/meminfo (host-wide, includes page cache)
- Process card: per-process RSS (includes shared libs)
This made system-used appear SMALLER than the sum of process RSS,
which is logically impossible and confused operators.
Fix: system memory now prefers cgroup accounting:
- cgroup v2: /sys/fs/cgroup/memory.current + memory.max
- cgroup v1: /sys/fs/cgroup/memory/memory.usage_in_bytes + limit_in_bytes
- Falls back to /proc/meminfo when cgroups unavailable (bare metal)
Process memory percent now uses the SAME total as the system card
(cgroup limit or host RAM), so both cards agree on what 100% means.
Added TestSystemMemoryMatchesProcessMemory to guard the invariant:
process RSS can never exceed system-used, and both use the same
denominator for percentage calculation.
Patch by user. Build + tests PASS (including new test).
fix(metrics): use cgroup memory accounting for consistent dashboard
System memory and process memory were reading different sources:
- System card: /proc/meminfo (host-wide, includes page cache)
- Process card: per-process RSS (includes shared libs)
This made system-used appear SMALLER than the sum of process RSS,
which is logically impossible and confused operators.
Fix: system memory now prefers cgroup accounting:
- cgroup v2: /sys/fs/cgroup/memory.current + memory.max
- cgroup v1: /sys/fs/cgroup/memory/memory.usage_in_bytes + limit_in_bytes
- Falls back to /proc/meminfo when cgroups unavailable (bare metal)
Process memory percent now uses the SAME total as the system card
(cgroup limit or host RAM), so both cards agree on what 100% means.
Added TestSystemMemoryMatchesProcessMemory to guard the invariant:
process RSS can never exceed system-used, and both use the same
denominator for percentage calculation.
Patch by user. Build + tests PASS (including new test).
fix(metrics): use cgroup memory accounting for consistent dashboard
System memory and process memory were reading different sources:
- System card: /proc/meminfo (host-wide, includes page cache)
- Process card: per-process RSS (includes shared libs)
This made system-used appear SMALLER than the sum of process RSS,
which is logically impossible and confused operators.
Fix: system memory now prefers cgroup accounting:
- cgroup v2: /sys/fs/cgroup/memory.current + memory.max
- cgroup v1: /sys/fs/cgroup/memory/memory.usage_in_bytes + limit_in_bytes
- Falls back to /proc/meminfo when cgroups unavailable (bare metal)
Process memory percent now uses the SAME total as the system card
(cgroup limit or host RAM), so both cards agree on what 100% means.
Added TestSystemMemoryMatchesProcessMemory to guard the invariant:
process RSS can never exceed system-used, and both use the same
denominator for percentage calculation.
Patch by user. Build + tests PASS (including new test).
fix(metrics): use cgroup memory accounting for consistent dashboard
System memory and process memory were reading different sources:
- System card: /proc/meminfo (host-wide, includes page cache)
- Process card: per-process RSS (includes shared libs)
This made system-used appear SMALLER than the sum of process RSS,
which is logically impossible and confused operators.
Fix: system memory now prefers cgroup accounting:
- cgroup v2: /sys/fs/cgroup/memory.current + memory.max
- cgroup v1: /sys/fs/cgroup/memory/memory.usage_in_bytes + limit_in_bytes
- Falls back to /proc/meminfo when cgroups unavailable (bare metal)
Process memory percent now uses the SAME total as the system card
(cgroup limit or host RAM), so both cards agree on what 100% means.
Added TestSystemMemoryMatchesProcessMemory to guard the invariant:
process RSS can never exceed system-used, and both use the same
denominator for percentage calculation.
Patch by user. Build + tests PASS (including new test).
2026-09-26 12:47:13 +00:00
Z User d50664377f feat(mem): Go runtime memory auto-tuning (no swap, no mmap)
Pure Go runtime tuning via GOMEMLIMIT + GOGC, applied at every stage
of install AND update. No swap file creation. No mmap tricks.
4-tier auto-detection based on /proc/meminfo:
RAM ≤ 128MB:  GOMEMLIMIT=48MiB  GOGC=20  MemoryMax=72M
RAM ≤ 256MB:  GOMEMLIMIT=96MiB  GOGC=50  MemoryMax=144M
RAM ≤ 512MB:  GOMEMLIMIT=192MiB GOGC=75  MemoryMax=288M
RAM > 512MB:  off               GOGC=100 MemoryMax=infinity
Applied to:
1. Early: export GOMEMLIMIT/GOGC before install_deps so download +
extract + init all inherit the limits.
2. init step: explicitly passed GOMEMLIMIT/GOGC to the Go binary.
3. healthcheck: explicitly passed to the Go binary.
4. systemd unit: Environment=GOMEMLIMIT=... + Environment=GOGC=...
+ MemoryMax=... (hard OOM cap, kills panel not SSH).
5. OpenRC unit (Alpine): export GOMEMLIMIT/GOGC (OpenRC doesn't
support MemoryMax, but GOMEMLIMIT handles it at Go runtime level).
Override: THREE_M_UI_GOMEMLIMIT=80MiB THREE_M_UI_GOGC=75
Why this works without swap:
- GOMEMLIMIT tells Go GC 'you have X MiB of heap, GC aggressively
to stay under it'. Go runtime respects this natively (Go 1.19+).
- GOGC=20 means GC triggers at 1.2x live heap (vs default 2x),
keeping RSS tight at the cost of ~3-5% more CPU.
- MemoryMax is the systemd safety net: if Go somehow leaks past
GOMEMLIMIT×1.5, systemd kills the panel (which Restart=always
brings back), not SSH.
- No swap needed: the goal is to keep RSS within physical RAM,
not to extend virtual memory.
feat(mem): Go runtime memory auto-tuning (no swap, no mmap)
Pure Go runtime tuning via GOMEMLIMIT + GOGC, applied at every stage
of install AND update. No swap file creation. No mmap tricks.
4-tier auto-detection based on /proc/meminfo:
RAM ≤ 128MB:  GOMEMLIMIT=48MiB  GOGC=20  MemoryMax=72M
RAM ≤ 256MB:  GOMEMLIMIT=96MiB  GOGC=50  MemoryMax=144M
RAM ≤ 512MB:  GOMEMLIMIT=192MiB GOGC=75  MemoryMax=288M
RAM > 512MB:  off               GOGC=100 MemoryMax=infinity
Applied to:
1. Early: export GOMEMLIMIT/GOGC before install_deps so download +
extract + init all inherit the limits.
2. init step: explicitly passed GOMEMLIMIT/GOGC to the Go binary.
3. healthcheck: explicitly passed to the Go binary.
4. systemd unit: Environment=GOMEMLIMIT=... + Environment=GOGC=...
+ MemoryMax=... (hard OOM cap, kills panel not SSH).
5. OpenRC unit (Alpine): export GOMEMLIMIT/GOGC (OpenRC doesn't
support MemoryMax, but GOMEMLIMIT handles it at Go runtime level).
Override: THREE_M_UI_GOMEMLIMIT=80MiB THREE_M_UI_GOGC=75
Why this works without swap:
- GOMEMLIMIT tells Go GC 'you have X MiB of heap, GC aggressively
to stay under it'. Go runtime respects this natively (Go 1.19+).
- GOGC=20 means GC triggers at 1.2x live heap (vs default 2x),
keeping RSS tight at the cost of ~3-5% more CPU.
- MemoryMax is the systemd safety net: if Go somehow leaks past
GOMEMLIMIT×1.5, systemd kills the panel (which Restart=always
brings back), not SSH.
- No swap needed: the goal is to keep RSS within physical RAM,
not to extend virtual memory.
feat(mem): Go runtime memory auto-tuning (no swap, no mmap)
Pure Go runtime tuning via GOMEMLIMIT + GOGC, applied at every stage
of install AND update. No swap file creation. No mmap tricks.
4-tier auto-detection based on /proc/meminfo:
RAM ≤ 128MB:  GOMEMLIMIT=48MiB  GOGC=20  MemoryMax=72M
RAM ≤ 256MB:  GOMEMLIMIT=96MiB  GOGC=50  MemoryMax=144M
RAM ≤ 512MB:  GOMEMLIMIT=192MiB GOGC=75  MemoryMax=288M
RAM > 512MB:  off               GOGC=100 MemoryMax=infinity
Applied to:
1. Early: export GOMEMLIMIT/GOGC before install_deps so download +
extract + init all inherit the limits.
2. init step: explicitly passed GOMEMLIMIT/GOGC to the Go binary.
3. healthcheck: explicitly passed to the Go binary.
4. systemd unit: Environment=GOMEMLIMIT=... + Environment=GOGC=...
+ MemoryMax=... (hard OOM cap, kills panel not SSH).
5. OpenRC unit (Alpine): export GOMEMLIMIT/GOGC (OpenRC doesn't
support MemoryMax, but GOMEMLIMIT handles it at Go runtime level).
Override: THREE_M_UI_GOMEMLIMIT=80MiB THREE_M_UI_GOGC=75
Why this works without swap:
- GOMEMLIMIT tells Go GC 'you have X MiB of heap, GC aggressively
to stay under it'. Go runtime respects this natively (Go 1.19+).
- GOGC=20 means GC triggers at 1.2x live heap (vs default 2x),
keeping RSS tight at the cost of ~3-5% more CPU.
- MemoryMax is the systemd safety net: if Go somehow leaks past
GOMEMLIMIT×1.5, systemd kills the panel (which Restart=always
brings back), not SSH.
- No swap needed: the goal is to keep RSS within physical RAM,
not to extend virtual memory.
feat(mem): Go runtime memory auto-tuning (no swap, no mmap)
Pure Go runtime tuning via GOMEMLIMIT + GOGC, applied at every stage
of install AND update. No swap file creation. No mmap tricks.
4-tier auto-detection based on /proc/meminfo:
RAM ≤ 128MB:  GOMEMLIMIT=48MiB  GOGC=20  MemoryMax=72M
RAM ≤ 256MB:  GOMEMLIMIT=96MiB  GOGC=50  MemoryMax=144M
RAM ≤ 512MB:  GOMEMLIMIT=192MiB GOGC=75  MemoryMax=288M
RAM > 512MB:  off               GOGC=100 MemoryMax=infinity
Applied to:
1. Early: export GOMEMLIMIT/GOGC before install_deps so download +
extract + init all inherit the limits.
2. init step: explicitly passed GOMEMLIMIT/GOGC to the Go binary.
3. healthcheck: explicitly passed to the Go binary.
4. systemd unit: Environment=GOMEMLIMIT=... + Environment=GOGC=...
+ MemoryMax=... (hard OOM cap, kills panel not SSH).
5. OpenRC unit (Alpine): export GOMEMLIMIT/GOGC (OpenRC doesn't
support MemoryMax, but GOMEMLIMIT handles it at Go runtime level).
Override: THREE_M_UI_GOMEMLIMIT=80MiB THREE_M_UI_GOGC=75
Why this works without swap:
- GOMEMLIMIT tells Go GC 'you have X MiB of heap, GC aggressively
to stay under it'. Go runtime respects this natively (Go 1.19+).
- GOGC=20 means GC triggers at 1.2x live heap (vs default 2x),
keeping RSS tight at the cost of ~3-5% more CPU.
- MemoryMax is the systemd safety net: if Go somehow leaks past
GOMEMLIMIT×1.5, systemd kills the panel (which Restart=always
brings back), not SSH.
- No swap needed: the goal is to keep RSS within physical RAM,
not to extend virtual memory.
feat(mem): Go runtime memory auto-tuning (no swap, no mmap)
Pure Go runtime tuning via GOMEMLIMIT + GOGC, applied at every stage
of install AND update. No swap file creation. No mmap tricks.
4-tier auto-detection based on /proc/meminfo:
RAM ≤ 128MB:  GOMEMLIMIT=48MiB  GOGC=20  MemoryMax=72M
RAM ≤ 256MB:  GOMEMLIMIT=96MiB  GOGC=50  MemoryMax=144M
RAM ≤ 512MB:  GOMEMLIMIT=192MiB GOGC=75  MemoryMax=288M
RAM > 512MB:  off               GOGC=100 MemoryMax=infinity
Applied to:
1. Early: export GOMEMLIMIT/GOGC before install_deps so download +
extract + init all inherit the limits.
2. init step: explicitly passed GOMEMLIMIT/GOGC to the Go binary.
3. healthcheck: explicitly passed to the Go binary.
4. systemd unit: Environment=GOMEMLIMIT=... + Environment=GOGC=...
+ MemoryMax=... (hard OOM cap, kills panel not SSH).
5. OpenRC unit (Alpine): export GOMEMLIMIT/GOGC (OpenRC doesn't
support MemoryMax, but GOMEMLIMIT handles it at Go runtime level).
Override: THREE_M_UI_GOMEMLIMIT=80MiB THREE_M_UI_GOGC=75
Why this works without swap:
- GOMEMLIMIT tells Go GC 'you have X MiB of heap, GC aggressively
to stay under it'. Go runtime respects this natively (Go 1.19+).
- GOGC=20 means GC triggers at 1.2x live heap (vs default 2x),
keeping RSS tight at the cost of ~3-5% more CPU.
- MemoryMax is the systemd safety net: if Go somehow leaks past
GOMEMLIMIT×1.5, systemd kills the panel (which Restart=always
brings back), not SSH.
- No swap needed: the goal is to keep RSS within physical RAM,
not to extend virtual memory.
feat(mem): Go runtime memory auto-tuning (no swap, no mmap)
Pure Go runtime tuning via GOMEMLIMIT + GOGC, applied at every stage
of install AND update. No swap file creation. No mmap tricks.
4-tier auto-detection based on /proc/meminfo:
RAM ≤ 128MB:  GOMEMLIMIT=48MiB  GOGC=20  MemoryMax=72M
RAM ≤ 256MB:  GOMEMLIMIT=96MiB  GOGC=50  MemoryMax=144M
RAM ≤ 512MB:  GOMEMLIMIT=192MiB GOGC=75  MemoryMax=288M
RAM > 512MB:  off               GOGC=100 MemoryMax=infinity
Applied to:
1. Early: export GOMEMLIMIT/GOGC before install_deps so download +
extract + init all inherit the limits.
2. init step: explicitly passed GOMEMLIMIT/GOGC to the Go binary.
3. healthcheck: explicitly passed to the Go binary.
4. systemd unit: Environment=GOMEMLIMIT=... + Environment=GOGC=...
+ MemoryMax=... (hard OOM cap, kills panel not SSH).
5. OpenRC unit (Alpine): export GOMEMLIMIT/GOGC (OpenRC doesn't
support MemoryMax, but GOMEMLIMIT handles it at Go runtime level).
Override: THREE_M_UI_GOMEMLIMIT=80MiB THREE_M_UI_GOGC=75
Why this works without swap:
- GOMEMLIMIT tells Go GC 'you have X MiB of heap, GC aggressively
to stay under it'. Go runtime respects this natively (Go 1.19+).
- GOGC=20 means GC triggers at 1.2x live heap (vs default 2x),
keeping RSS tight at the cost of ~3-5% more CPU.
- MemoryMax is the systemd safety net: if Go somehow leaks past
GOMEMLIMIT×1.5, systemd kills the panel (which Restart=always
brings back), not SSH.
- No swap needed: the goal is to keep RSS within physical RAM,
not to extend virtual memory.
feat(mem): Go runtime memory auto-tuning (no swap, no mmap)
Pure Go runtime tuning via GOMEMLIMIT + GOGC, applied at every stage
of install AND update. No swap file creation. No mmap tricks.
4-tier auto-detection based on /proc/meminfo:
RAM ≤ 128MB:  GOMEMLIMIT=48MiB  GOGC=20  MemoryMax=72M
RAM ≤ 256MB:  GOMEMLIMIT=96MiB  GOGC=50  MemoryMax=144M
RAM ≤ 512MB:  GOMEMLIMIT=192MiB GOGC=75  MemoryMax=288M
RAM > 512MB:  off               GOGC=100 MemoryMax=infinity
Applied to:
1. Early: export GOMEMLIMIT/GOGC before install_deps so download +
extract + init all inherit the limits.
2. init step: explicitly passed GOMEMLIMIT/GOGC to the Go binary.
3. healthcheck: explicitly passed to the Go binary.
4. systemd unit: Environment=GOMEMLIMIT=... + Environment=GOGC=...
+ MemoryMax=... (hard OOM cap, kills panel not SSH).
5. OpenRC unit (Alpine): export GOMEMLIMIT/GOGC (OpenRC doesn't
support MemoryMax, but GOMEMLIMIT handles it at Go runtime level).
Override: THREE_M_UI_GOMEMLIMIT=80MiB THREE_M_UI_GOGC=75
Why this works without swap:
- GOMEMLIMIT tells Go GC 'you have X MiB of heap, GC aggressively
to stay under it'. Go runtime respects this natively (Go 1.19+).
- GOGC=20 means GC triggers at 1.2x live heap (vs default 2x),
keeping RSS tight at the cost of ~3-5% more CPU.
- MemoryMax is the systemd safety net: if Go somehow leaks past
GOMEMLIMIT×1.5, systemd kills the panel (which Restart=always
brings back), not SSH.
- No swap needed: the goal is to keep RSS within physical RAM,
not to extend virtual memory.
feat(mem): Go runtime memory auto-tuning (no swap, no mmap)
Pure Go runtime tuning via GOMEMLIMIT + GOGC, applied at every stage
of install AND update. No swap file creation. No mmap tricks.
4-tier auto-detection based on /proc/meminfo:
RAM ≤ 128MB:  GOMEMLIMIT=48MiB  GOGC=20  MemoryMax=72M
RAM ≤ 256MB:  GOMEMLIMIT=96MiB  GOGC=50  MemoryMax=144M
RAM ≤ 512MB:  GOMEMLIMIT=192MiB GOGC=75  MemoryMax=288M
RAM > 512MB:  off               GOGC=100 MemoryMax=infinity
Applied to:
1. Early: export GOMEMLIMIT/GOGC before install_deps so download +
extract + init all inherit the limits.
2. init step: explicitly passed GOMEMLIMIT/GOGC to the Go binary.
3. healthcheck: explicitly passed to the Go binary.
4. systemd unit: Environment=GOMEMLIMIT=... + Environment=GOGC=...
+ MemoryMax=... (hard OOM cap, kills panel not SSH).
5. OpenRC unit (Alpine): export GOMEMLIMIT/GOGC (OpenRC doesn't
support MemoryMax, but GOMEMLIMIT handles it at Go runtime level).
Override: THREE_M_UI_GOMEMLIMIT=80MiB THREE_M_UI_GOGC=75
Why this works without swap:
- GOMEMLIMIT tells Go GC 'you have X MiB of heap, GC aggressively
to stay under it'. Go runtime respects this natively (Go 1.19+).
- GOGC=20 means GC triggers at 1.2x live heap (vs default 2x),
keeping RSS tight at the cost of ~3-5% more CPU.
- MemoryMax is the systemd safety net: if Go somehow leaks past
GOMEMLIMIT×1.5, systemd kills the panel (which Restart=always
brings back), not SSH.
- No swap needed: the goal is to keep RSS within physical RAM,
not to extend virtual memory.
feat(mem): Go runtime memory auto-tuning (no swap, no mmap)
Pure Go runtime tuning via GOMEMLIMIT + GOGC, applied at every stage
of install AND update. No swap file creation. No mmap tricks.
4-tier auto-detection based on /proc/meminfo:
RAM ≤ 128MB:  GOMEMLIMIT=48MiB  GOGC=20  MemoryMax=72M
RAM ≤ 256MB:  GOMEMLIMIT=96MiB  GOGC=50  MemoryMax=144M
RAM ≤ 512MB:  GOMEMLIMIT=192MiB GOGC=75  MemoryMax=288M
RAM > 512MB:  off               GOGC=100 MemoryMax=infinity
Applied to:
1. Early: export GOMEMLIMIT/GOGC before install_deps so download +
extract + init all inherit the limits.
2. init step: explicitly passed GOMEMLIMIT/GOGC to the Go binary.
3. healthcheck: explicitly passed to the Go binary.
4. systemd unit: Environment=GOMEMLIMIT=... + Environment=GOGC=...
+ MemoryMax=... (hard OOM cap, kills panel not SSH).
5. OpenRC unit (Alpine): export GOMEMLIMIT/GOGC (OpenRC doesn't
support MemoryMax, but GOMEMLIMIT handles it at Go runtime level).
Override: THREE_M_UI_GOMEMLIMIT=80MiB THREE_M_UI_GOGC=75
Why this works without swap:
- GOMEMLIMIT tells Go GC 'you have X MiB of heap, GC aggressively
to stay under it'. Go runtime respects this natively (Go 1.19+).
- GOGC=20 means GC triggers at 1.2x live heap (vs default 2x),
keeping RSS tight at the cost of ~3-5% more CPU.
- MemoryMax is the systemd safety net: if Go somehow leaks past
GOMEMLIMIT×1.5, systemd kills the panel (which Restart=always
brings back), not SSH.
- No swap needed: the goal is to keep RSS within physical RAM,
not to extend virtual memory.
feat(mem): Go runtime memory auto-tuning (no swap, no mmap)
Pure Go runtime tuning via GOMEMLIMIT + GOGC, applied at every stage
of install AND update. No swap file creation. No mmap tricks.
4-tier auto-detection based on /proc/meminfo:
RAM ≤ 128MB:  GOMEMLIMIT=48MiB  GOGC=20  MemoryMax=72M
RAM ≤ 256MB:  GOMEMLIMIT=96MiB  GOGC=50  MemoryMax=144M
RAM ≤ 512MB:  GOMEMLIMIT=192MiB GOGC=75  MemoryMax=288M
RAM > 512MB:  off               GOGC=100 MemoryMax=infinity
Applied to:
1. Early: export GOMEMLIMIT/GOGC before install_deps so download +
extract + init all inherit the limits.
2. init step: explicitly passed GOMEMLIMIT/GOGC to the Go binary.
3. healthcheck: explicitly passed to the Go binary.
4. systemd unit: Environment=GOMEMLIMIT=... + Environment=GOGC=...
+ MemoryMax=... (hard OOM cap, kills panel not SSH).
5. OpenRC unit (Alpine): export GOMEMLIMIT/GOGC (OpenRC doesn't
support MemoryMax, but GOMEMLIMIT handles it at Go runtime level).
Override: THREE_M_UI_GOMEMLIMIT=80MiB THREE_M_UI_GOGC=75
Why this works without swap:
- GOMEMLIMIT tells Go GC 'you have X MiB of heap, GC aggressively
to stay under it'. Go runtime respects this natively (Go 1.19+).
- GOGC=20 means GC triggers at 1.2x live heap (vs default 2x),
keeping RSS tight at the cost of ~3-5% more CPU.
- MemoryMax is the systemd safety net: if Go somehow leaks past
GOMEMLIMIT×1.5, systemd kills the panel (which Restart=always
brings back), not SSH.
- No swap needed: the goal is to keep RSS within physical RAM,
not to extend virtual memory.
feat(mem): Go runtime memory auto-tuning (no swap, no mmap)
Pure Go runtime tuning via GOMEMLIMIT + GOGC, applied at every stage
of install AND update. No swap file creation. No mmap tricks.
4-tier auto-detection based on /proc/meminfo:
RAM ≤ 128MB:  GOMEMLIMIT=48MiB  GOGC=20  MemoryMax=72M
RAM ≤ 256MB:  GOMEMLIMIT=96MiB  GOGC=50  MemoryMax=144M
RAM ≤ 512MB:  GOMEMLIMIT=192MiB GOGC=75  MemoryMax=288M
RAM > 512MB:  off               GOGC=100 MemoryMax=infinity
Applied to:
1. Early: export GOMEMLIMIT/GOGC before install_deps so download +
extract + init all inherit the limits.
2. init step: explicitly passed GOMEMLIMIT/GOGC to the Go binary.
3. healthcheck: explicitly passed to the Go binary.
4. systemd unit: Environment=GOMEMLIMIT=... + Environment=GOGC=...
+ MemoryMax=... (hard OOM cap, kills panel not SSH).
5. OpenRC unit (Alpine): export GOMEMLIMIT/GOGC (OpenRC doesn't
support MemoryMax, but GOMEMLIMIT handles it at Go runtime level).
Override: THREE_M_UI_GOMEMLIMIT=80MiB THREE_M_UI_GOGC=75
Why this works without swap:
- GOMEMLIMIT tells Go GC 'you have X MiB of heap, GC aggressively
to stay under it'. Go runtime respects this natively (Go 1.19+).
- GOGC=20 means GC triggers at 1.2x live heap (vs default 2x),
keeping RSS tight at the cost of ~3-5% more CPU.
- MemoryMax is the systemd safety net: if Go somehow leaks past
GOMEMLIMIT×1.5, systemd kills the panel (which Restart=always
brings back), not SSH.
- No swap needed: the goal is to keep RSS within physical RAM,
not to extend virtual memory.
feat(mem): Go runtime memory auto-tuning (no swap, no mmap)
Pure Go runtime tuning via GOMEMLIMIT + GOGC, applied at every stage
of install AND update. No swap file creation. No mmap tricks.
4-tier auto-detection based on /proc/meminfo:
RAM ≤ 128MB:  GOMEMLIMIT=48MiB  GOGC=20  MemoryMax=72M
RAM ≤ 256MB:  GOMEMLIMIT=96MiB  GOGC=50  MemoryMax=144M
RAM ≤ 512MB:  GOMEMLIMIT=192MiB GOGC=75  MemoryMax=288M
RAM > 512MB:  off               GOGC=100 MemoryMax=infinity
Applied to:
1. Early: export GOMEMLIMIT/GOGC before install_deps so download +
extract + init all inherit the limits.
2. init step: explicitly passed GOMEMLIMIT/GOGC to the Go binary.
3. healthcheck: explicitly passed to the Go binary.
4. systemd unit: Environment=GOMEMLIMIT=... + Environment=GOGC=...
+ MemoryMax=... (hard OOM cap, kills panel not SSH).
5. OpenRC unit (Alpine): export GOMEMLIMIT/GOGC (OpenRC doesn't
support MemoryMax, but GOMEMLIMIT handles it at Go runtime level).
Override: THREE_M_UI_GOMEMLIMIT=80MiB THREE_M_UI_GOGC=75
Why this works without swap:
- GOMEMLIMIT tells Go GC 'you have X MiB of heap, GC aggressively
to stay under it'. Go runtime respects this natively (Go 1.19+).
- GOGC=20 means GC triggers at 1.2x live heap (vs default 2x),
keeping RSS tight at the cost of ~3-5% more CPU.
- MemoryMax is the systemd safety net: if Go somehow leaks past
GOMEMLIMIT×1.5, systemd kills the panel (which Restart=always
brings back), not SSH.
- No swap needed: the goal is to keep RSS within physical RAM,
not to extend virtual memory.
2026-09-26 07:28:17 +00:00
kazeyukiro 0b51c95fe4 fix(ci,ui): repair Form.Item syntax; soften core restart errors
Fix tooltip injection that broke obfs_opts_host rules (oxlint).
Restart Mihomo with longer timeout + network retry; treat transient
API drops after restart as success after status refresh ( style).
fix(ci,ui): repair Form.Item syntax; soften core restart errors
Fix tooltip injection that broke obfs_opts_host rules (oxlint).
Restart Mihomo with longer timeout + network retry; treat transient
API drops after restart as success after status refresh ( style).
fix(ci,ui): repair Form.Item syntax; soften core restart errors
Fix tooltip injection that broke obfs_opts_host rules (oxlint).
Restart Mihomo with longer timeout + network retry; treat transient
API drops after restart as success after status refresh ( style).
fix(ci,ui): repair Form.Item syntax; soften core restart errors
Fix tooltip injection that broke obfs_opts_host rules (oxlint).
Restart Mihomo with longer timeout + network retry; treat transient
API drops after restart as success after status refresh ( style).
fix(ci,ui): repair Form.Item syntax; soften core restart errors
Fix tooltip injection that broke obfs_opts_host rules (oxlint).
Restart Mihomo with longer timeout + network retry; treat transient
API drops after restart as success after status refresh ( style).
fix(ci,ui): repair Form.Item syntax; soften core restart errors
Fix tooltip injection that broke obfs_opts_host rules (oxlint).
Restart Mihomo with longer timeout + network retry; treat transient
API drops after restart as success after status refresh ( style).
fix(ci,ui): repair Form.Item syntax; soften core restart errors
Fix tooltip injection that broke obfs_opts_host rules (oxlint).
Restart Mihomo with longer timeout + network retry; treat transient
API drops after restart as success after status refresh ( style).
fix(ci,ui): repair Form.Item syntax; soften core restart errors
Fix tooltip injection that broke obfs_opts_host rules (oxlint).
Restart Mihomo with longer timeout + network retry; treat transient
API drops after restart as success after status refresh ( style).
fix(ci,ui): repair Form.Item syntax; soften core restart errors
Fix tooltip injection that broke obfs_opts_host rules (oxlint).
Restart Mihomo with longer timeout + network retry; treat transient
API drops after restart as success after status refresh ( style).
fix(ci,ui): repair Form.Item syntax; soften core restart errors
Fix tooltip injection that broke obfs_opts_host rules (oxlint).
Restart Mihomo with longer timeout + network retry; treat transient
API drops after restart as success after status refresh ( style).
fix(ci,ui): repair Form.Item syntax; soften core restart errors
Fix tooltip injection that broke obfs_opts_host rules (oxlint).
Restart Mihomo with longer timeout + network retry; treat transient
API drops after restart as success after status refresh ( style).
fix(ci,ui): repair Form.Item syntax; soften core restart errors
Fix tooltip injection that broke obfs_opts_host rules (oxlint).
Restart Mihomo with longer timeout + network retry; treat transient
API drops after restart as success after status refresh ( style).
2026-09-26 02:23:49 +00:00
kazeyukiro 1e534e615d feat(ui): field help tooltips on listeners/nodes page
Add en/zh-CN hints for common protocol, TLS/REALITY, transport and
cert fields; wire Form.Item tooltips and a short form help banner.
feat(ui): field help tooltips on listeners/nodes page
Add en/zh-CN hints for common protocol, TLS/REALITY, transport and
cert fields; wire Form.Item tooltips and a short form help banner.
feat(ui): field help tooltips on listeners/nodes page
Add en/zh-CN hints for common protocol, TLS/REALITY, transport and
cert fields; wire Form.Item tooltips and a short form help banner.
feat(ui): field help tooltips on listeners/nodes page
Add en/zh-CN hints for common protocol, TLS/REALITY, transport and
cert fields; wire Form.Item tooltips and a short form help banner.
feat(ui): field help tooltips on listeners/nodes page
Add en/zh-CN hints for common protocol, TLS/REALITY, transport and
cert fields; wire Form.Item tooltips and a short form help banner.
feat(ui): field help tooltips on listeners/nodes page
Add en/zh-CN hints for common protocol, TLS/REALITY, transport and
cert fields; wire Form.Item tooltips and a short form help banner.
feat(ui): field help tooltips on listeners/nodes page
Add en/zh-CN hints for common protocol, TLS/REALITY, transport and
cert fields; wire Form.Item tooltips and a short form help banner.
feat(ui): field help tooltips on listeners/nodes page
Add en/zh-CN hints for common protocol, TLS/REALITY, transport and
cert fields; wire Form.Item tooltips and a short form help banner.
feat(ui): field help tooltips on listeners/nodes page
Add en/zh-CN hints for common protocol, TLS/REALITY, transport and
cert fields; wire Form.Item tooltips and a short form help banner.
feat(ui): field help tooltips on listeners/nodes page
Add en/zh-CN hints for common protocol, TLS/REALITY, transport and
cert fields; wire Form.Item tooltips and a short form help banner.
feat(ui): field help tooltips on listeners/nodes page
Add en/zh-CN hints for common protocol, TLS/REALITY, transport and
cert fields; wire Form.Item tooltips and a short form help banner.
feat(ui): field help tooltips on listeners/nodes page
Add en/zh-CN hints for common protocol, TLS/REALITY, transport and
cert fields; wire Form.Item tooltips and a short form help banner.
2026-09-26 02:17:09 +00:00
kazeyukiro 6169b3e106 feat(ui): add settings field help tooltips
Document Telegram notifies, GitHub OAuth (callback/public_url),
subscription page, panel SSL/ACME listens, certbot and proxy fields
in en/zh-CN with Form.Item tooltips on the Settings page.
feat(ui): add settings field help tooltips
Document Telegram notifies, GitHub OAuth (callback/public_url),
subscription page, panel SSL/ACME listens, certbot and proxy fields
in en/zh-CN with Form.Item tooltips on the Settings page.
feat(ui): add settings field help tooltips
Document Telegram notifies, GitHub OAuth (callback/public_url),
subscription page, panel SSL/ACME listens, certbot and proxy fields
in en/zh-CN with Form.Item tooltips on the Settings page.
feat(ui): add settings field help tooltips
Document Telegram notifies, GitHub OAuth (callback/public_url),
subscription page, panel SSL/ACME listens, certbot and proxy fields
in en/zh-CN with Form.Item tooltips on the Settings page.
feat(ui): add settings field help tooltips
Document Telegram notifies, GitHub OAuth (callback/public_url),
subscription page, panel SSL/ACME listens, certbot and proxy fields
in en/zh-CN with Form.Item tooltips on the Settings page.
feat(ui): add settings field help tooltips
Document Telegram notifies, GitHub OAuth (callback/public_url),
subscription page, panel SSL/ACME listens, certbot and proxy fields
in en/zh-CN with Form.Item tooltips on the Settings page.
feat(ui): add settings field help tooltips
Document Telegram notifies, GitHub OAuth (callback/public_url),
subscription page, panel SSL/ACME listens, certbot and proxy fields
in en/zh-CN with Form.Item tooltips on the Settings page.
feat(ui): add settings field help tooltips
Document Telegram notifies, GitHub OAuth (callback/public_url),
subscription page, panel SSL/ACME listens, certbot and proxy fields
in en/zh-CN with Form.Item tooltips on the Settings page.
feat(ui): add settings field help tooltips
Document Telegram notifies, GitHub OAuth (callback/public_url),
subscription page, panel SSL/ACME listens, certbot and proxy fields
in en/zh-CN with Form.Item tooltips on the Settings page.
feat(ui): add settings field help tooltips
Document Telegram notifies, GitHub OAuth (callback/public_url),
subscription page, panel SSL/ACME listens, certbot and proxy fields
in en/zh-CN with Form.Item tooltips on the Settings page.
feat(ui): add settings field help tooltips
Document Telegram notifies, GitHub OAuth (callback/public_url),
subscription page, panel SSL/ACME listens, certbot and proxy fields
in en/zh-CN with Form.Item tooltips on the Settings page.
feat(ui): add settings field help tooltips
Document Telegram notifies, GitHub OAuth (callback/public_url),
subscription page, panel SSL/ACME listens, certbot and proxy fields
in en/zh-CN with Form.Item tooltips on the Settings page.
2026-09-26 02:13:37 +00:00
kazeyukiro 2d87b68e44 fix(ci): replace remaining *Outlined imports after icon cleanup
Core/Users still used RollbackOutlined, ClearOutlined, CopyOutlined.
Map to IconUndo/IconClear/IconCopy and restore IconUndo export.
fix(ci): replace remaining *Outlined imports after icon cleanup
Core/Users still used RollbackOutlined, ClearOutlined, CopyOutlined.
Map to IconUndo/IconClear/IconCopy and restore IconUndo export.
fix(ci): replace remaining *Outlined imports after icon cleanup
Core/Users still used RollbackOutlined, ClearOutlined, CopyOutlined.
Map to IconUndo/IconClear/IconCopy and restore IconUndo export.
fix(ci): replace remaining *Outlined imports after icon cleanup
Core/Users still used RollbackOutlined, ClearOutlined, CopyOutlined.
Map to IconUndo/IconClear/IconCopy and restore IconUndo export.
fix(ci): replace remaining *Outlined imports after icon cleanup
Core/Users still used RollbackOutlined, ClearOutlined, CopyOutlined.
Map to IconUndo/IconClear/IconCopy and restore IconUndo export.
fix(ci): replace remaining *Outlined imports after icon cleanup
Core/Users still used RollbackOutlined, ClearOutlined, CopyOutlined.
Map to IconUndo/IconClear/IconCopy and restore IconUndo export.
fix(ci): replace remaining *Outlined imports after icon cleanup
Core/Users still used RollbackOutlined, ClearOutlined, CopyOutlined.
Map to IconUndo/IconClear/IconCopy and restore IconUndo export.
fix(ci): replace remaining *Outlined imports after icon cleanup
Core/Users still used RollbackOutlined, ClearOutlined, CopyOutlined.
Map to IconUndo/IconClear/IconCopy and restore IconUndo export.
fix(ci): replace remaining *Outlined imports after icon cleanup
Core/Users still used RollbackOutlined, ClearOutlined, CopyOutlined.
Map to IconUndo/IconClear/IconCopy and restore IconUndo export.
fix(ci): replace remaining *Outlined imports after icon cleanup
Core/Users still used RollbackOutlined, ClearOutlined, CopyOutlined.
Map to IconUndo/IconClear/IconCopy and restore IconUndo export.
fix(ci): replace remaining *Outlined imports after icon cleanup
Core/Users still used RollbackOutlined, ClearOutlined, CopyOutlined.
Map to IconUndo/IconClear/IconCopy and restore IconUndo export.
fix(ci): replace remaining *Outlined imports after icon cleanup
Core/Users still used RollbackOutlined, ClearOutlined, CopyOutlined.
Map to IconUndo/IconClear/IconCopy and restore IconUndo export.
2026-09-26 01:41:53 +00:00
kazeyukiro d7dda63c1d fix(ci): pass latest:false to pre image workflow 2026-09-26 01:40:50 +00:00
kazeyukiro de0195f571 fix(ci): publish rolling pre release via gh CLI
softprops/action-gh-release failed updating the pre tag with
Resource not accessible by integration. Delete+create with gh matches
the stable release path and works under GITHUB_TOKEN contents:write.
fix(ci): publish rolling pre release via gh CLI
softprops/action-gh-release failed updating the pre tag with
Resource not accessible by integration. Delete+create with gh matches
the stable release path and works under GITHUB_TOKEN contents:write.
fix(ci): publish rolling pre release via gh CLI
softprops/action-gh-release failed updating the pre tag with
Resource not accessible by integration. Delete+create with gh matches
the stable release path and works under GITHUB_TOKEN contents:write.
fix(ci): publish rolling pre release via gh CLI
softprops/action-gh-release failed updating the pre tag with
Resource not accessible by integration. Delete+create with gh matches
the stable release path and works under GITHUB_TOKEN contents:write.
fix(ci): publish rolling pre release via gh CLI
softprops/action-gh-release failed updating the pre tag with
Resource not accessible by integration. Delete+create with gh matches
the stable release path and works under GITHUB_TOKEN contents:write.
fix(ci): publish rolling pre release via gh CLI
softprops/action-gh-release failed updating the pre tag with
Resource not accessible by integration. Delete+create with gh matches
the stable release path and works under GITHUB_TOKEN contents:write.
fix(ci): publish rolling pre release via gh CLI
softprops/action-gh-release failed updating the pre tag with
Resource not accessible by integration. Delete+create with gh matches
the stable release path and works under GITHUB_TOKEN contents:write.
fix(ci): publish rolling pre release via gh CLI
softprops/action-gh-release failed updating the pre tag with
Resource not accessible by integration. Delete+create with gh matches
the stable release path and works under GITHUB_TOKEN contents:write.
fix(ci): publish rolling pre release via gh CLI
softprops/action-gh-release failed updating the pre tag with
Resource not accessible by integration. Delete+create with gh matches
the stable release path and works under GITHUB_TOKEN contents:write.
fix(ci): publish rolling pre release via gh CLI
softprops/action-gh-release failed updating the pre tag with
Resource not accessible by integration. Delete+create with gh matches
the stable release path and works under GITHUB_TOKEN contents:write.
fix(ci): publish rolling pre release via gh CLI
softprops/action-gh-release failed updating the pre tag with
Resource not accessible by integration. Delete+create with gh matches
the stable release path and works under GITHUB_TOKEN contents:write.
fix(ci): publish rolling pre release via gh CLI
softprops/action-gh-release failed updating the pre tag with
Resource not accessible by integration. Delete+create with gh matches
the stable release path and works under GITHUB_TOKEN contents:write.
2026-09-26 01:40:30 +00:00
kazeyukiro 3a7161b060 chore(ui): drop unused action icons and legacy *Outlined aliases
Remove IconBell/Timer/Target/Undo/SettingsData and the entire Ant-style
Outlined alias block; every remaining Icon* is referenced by a page.
chore(ui): drop unused action icons and legacy *Outlined aliases
Remove IconBell/Timer/Target/Undo/SettingsData and the entire Ant-style
Outlined alias block; every remaining Icon* is referenced by a page.
chore(ui): drop unused action icons and legacy *Outlined aliases
Remove IconBell/Timer/Target/Undo/SettingsData and the entire Ant-style
Outlined alias block; every remaining Icon* is referenced by a page.
chore(ui): drop unused action icons and legacy *Outlined aliases
Remove IconBell/Timer/Target/Undo/SettingsData and the entire Ant-style
Outlined alias block; every remaining Icon* is referenced by a page.
chore(ui): drop unused action icons and legacy *Outlined aliases
Remove IconBell/Timer/Target/Undo/SettingsData and the entire Ant-style
Outlined alias block; every remaining Icon* is referenced by a page.
chore(ui): drop unused action icons and legacy *Outlined aliases
Remove IconBell/Timer/Target/Undo/SettingsData and the entire Ant-style
Outlined alias block; every remaining Icon* is referenced by a page.
chore(ui): drop unused action icons and legacy *Outlined aliases
Remove IconBell/Timer/Target/Undo/SettingsData and the entire Ant-style
Outlined alias block; every remaining Icon* is referenced by a page.
chore(ui): drop unused action icons and legacy *Outlined aliases
Remove IconBell/Timer/Target/Undo/SettingsData and the entire Ant-style
Outlined alias block; every remaining Icon* is referenced by a page.
chore(ui): drop unused action icons and legacy *Outlined aliases
Remove IconBell/Timer/Target/Undo/SettingsData and the entire Ant-style
Outlined alias block; every remaining Icon* is referenced by a page.
chore(ui): drop unused action icons and legacy *Outlined aliases
Remove IconBell/Timer/Target/Undo/SettingsData and the entire Ant-style
Outlined alias block; every remaining Icon* is referenced by a page.
chore(ui): drop unused action icons and legacy *Outlined aliases
Remove IconBell/Timer/Target/Undo/SettingsData and the entire Ant-style
Outlined alias block; every remaining Icon* is referenced by a page.
chore(ui): drop unused action icons and legacy *Outlined aliases
Remove IconBell/Timer/Target/Undo/SettingsData and the entire Ant-style
Outlined alias block; every remaining Icon* is referenced by a page.
2026-09-26 01:36:21 +00:00
風雪 270f2669bd Delete .github/workflows/embed-frontend.yml
Delete .github/workflows/embed-frontend.yml
Delete .github/workflows/embed-frontend.yml
Delete .github/workflows/embed-frontend.yml
Delete .github/workflows/embed-frontend.yml
Delete .github/workflows/embed-frontend.yml
Delete .github/workflows/embed-frontend.yml
Delete .github/workflows/embed-frontend.yml
Delete .github/workflows/embed-frontend.yml
Delete .github/workflows/embed-frontend.yml
Delete .github/workflows/embed-frontend.yml
Delete .github/workflows/embed-frontend.yml
2026-09-26 09:35:06 +08:00
kazeyukiro ee684309e7 refactor(ui): route Routing page icons through icons.tsx
Add IconAddRule/AddGroup/MoveUp/Down/SaveRules/ApplyRules and drop
direct lucide-react imports from Routing.tsx.
refactor(ui): route Routing page icons through icons.tsx
Add IconAddRule/AddGroup/MoveUp/Down/SaveRules/ApplyRules and drop
direct lucide-react imports from Routing.tsx.
refactor(ui): route Routing page icons through icons.tsx
Add IconAddRule/AddGroup/MoveUp/Down/SaveRules/ApplyRules and drop
direct lucide-react imports from Routing.tsx.
refactor(ui): route Routing page icons through icons.tsx
Add IconAddRule/AddGroup/MoveUp/Down/SaveRules/ApplyRules and drop
direct lucide-react imports from Routing.tsx.
refactor(ui): route Routing page icons through icons.tsx
Add IconAddRule/AddGroup/MoveUp/Down/SaveRules/ApplyRules and drop
direct lucide-react imports from Routing.tsx.
refactor(ui): route Routing page icons through icons.tsx
Add IconAddRule/AddGroup/MoveUp/Down/SaveRules/ApplyRules and drop
direct lucide-react imports from Routing.tsx.
refactor(ui): route Routing page icons through icons.tsx
Add IconAddRule/AddGroup/MoveUp/Down/SaveRules/ApplyRules and drop
direct lucide-react imports from Routing.tsx.
refactor(ui): route Routing page icons through icons.tsx
Add IconAddRule/AddGroup/MoveUp/Down/SaveRules/ApplyRules and drop
direct lucide-react imports from Routing.tsx.
refactor(ui): route Routing page icons through icons.tsx
Add IconAddRule/AddGroup/MoveUp/Down/SaveRules/ApplyRules and drop
direct lucide-react imports from Routing.tsx.
refactor(ui): route Routing page icons through icons.tsx
Add IconAddRule/AddGroup/MoveUp/Down/SaveRules/ApplyRules and drop
direct lucide-react imports from Routing.tsx.
refactor(ui): route Routing page icons through icons.tsx
Add IconAddRule/AddGroup/MoveUp/Down/SaveRules/ApplyRules and drop
direct lucide-react imports from Routing.tsx.
refactor(ui): route Routing page icons through icons.tsx
Add IconAddRule/AddGroup/MoveUp/Down/SaveRules/ApplyRules and drop
direct lucide-react imports from Routing.tsx.
2026-09-26 01:32:50 +00:00
kazeyukiro 6dcb6e2cd8 fix(ui): dedupe Routing page Lucide icons vs central registry 2026-09-26 01:30:14 +00:00
kazeyukiro f34206ad74 fix(ui): unique Lucide glyphs for shared icon slots
Deduplicate mappings so settings/nav/actions no longer share the same
glyph (Telegram, SSL, proxy, about, update, traffic, panel, add-field).
fix(ui): unique Lucide glyphs for shared icon slots
Deduplicate mappings so settings/nav/actions no longer share the same
glyph (Telegram, SSL, proxy, about, update, traffic, panel, add-field).
fix(ui): unique Lucide glyphs for shared icon slots
Deduplicate mappings so settings/nav/actions no longer share the same
glyph (Telegram, SSL, proxy, about, update, traffic, panel, add-field).
fix(ui): unique Lucide glyphs for shared icon slots
Deduplicate mappings so settings/nav/actions no longer share the same
glyph (Telegram, SSL, proxy, about, update, traffic, panel, add-field).
fix(ui): unique Lucide glyphs for shared icon slots
Deduplicate mappings so settings/nav/actions no longer share the same
glyph (Telegram, SSL, proxy, about, update, traffic, panel, add-field).
fix(ui): unique Lucide glyphs for shared icon slots
Deduplicate mappings so settings/nav/actions no longer share the same
glyph (Telegram, SSL, proxy, about, update, traffic, panel, add-field).
fix(ui): unique Lucide glyphs for shared icon slots
Deduplicate mappings so settings/nav/actions no longer share the same
glyph (Telegram, SSL, proxy, about, update, traffic, panel, add-field).
fix(ui): unique Lucide glyphs for shared icon slots
Deduplicate mappings so settings/nav/actions no longer share the same
glyph (Telegram, SSL, proxy, about, update, traffic, panel, add-field).
fix(ui): unique Lucide glyphs for shared icon slots
Deduplicate mappings so settings/nav/actions no longer share the same
glyph (Telegram, SSL, proxy, about, update, traffic, panel, add-field).
fix(ui): unique Lucide glyphs for shared icon slots
Deduplicate mappings so settings/nav/actions no longer share the same
glyph (Telegram, SSL, proxy, about, update, traffic, panel, add-field).
fix(ui): unique Lucide glyphs for shared icon slots
Deduplicate mappings so settings/nav/actions no longer share the same
glyph (Telegram, SSL, proxy, about, update, traffic, panel, add-field).
fix(ui): unique Lucide glyphs for shared icon slots
Deduplicate mappings so settings/nav/actions no longer share the same
glyph (Telegram, SSL, proxy, about, update, traffic, panel, add-field).
2026-09-26 01:29:55 +00:00
kazeyukiro d283f38638 chore: move localized READMEs under docs/readme
Keep root README.md as the canonical entry. Translations live in
docs/readme/ with fixed relative links and a language index.
chore: move localized READMEs under docs/readme
Keep root README.md as the canonical entry. Translations live in
docs/readme/ with fixed relative links and a language index.
chore: move localized READMEs under docs/readme
Keep root README.md as the canonical entry. Translations live in
docs/readme/ with fixed relative links and a language index.
chore: move localized READMEs under docs/readme
Keep root README.md as the canonical entry. Translations live in
docs/readme/ with fixed relative links and a language index.
chore: move localized READMEs under docs/readme
Keep root README.md as the canonical entry. Translations live in
docs/readme/ with fixed relative links and a language index.
chore: move localized READMEs under docs/readme
Keep root README.md as the canonical entry. Translations live in
docs/readme/ with fixed relative links and a language index.
chore: move localized READMEs under docs/readme
Keep root README.md as the canonical entry. Translations live in
docs/readme/ with fixed relative links and a language index.
chore: move localized READMEs under docs/readme
Keep root README.md as the canonical entry. Translations live in
docs/readme/ with fixed relative links and a language index.
chore: move localized READMEs under docs/readme
Keep root README.md as the canonical entry. Translations live in
docs/readme/ with fixed relative links and a language index.
chore: move localized READMEs under docs/readme
Keep root README.md as the canonical entry. Translations live in
docs/readme/ with fixed relative links and a language index.
chore: move localized READMEs under docs/readme
Keep root README.md as the canonical entry. Translations live in
docs/readme/ with fixed relative links and a language index.
chore: move localized READMEs under docs/readme
Keep root README.md as the canonical entry. Translations live in
docs/readme/ with fixed relative links and a language index.
2026-09-26 01:16:47 +00:00
kazeyukiro b9e8d57971 fix(ci): repair i18n syntax; document GitHub OAuth
Remove double comma in en/zh-CN locale settings keys that broke oxlint.
Add OpenAPI paths, docs/github-oauth.md, and README security note.
fix(ci): repair i18n syntax; document GitHub OAuth
Remove double comma in en/zh-CN locale settings keys that broke oxlint.
Add OpenAPI paths, docs/github-oauth.md, and README security note.
fix(ci): repair i18n syntax; document GitHub OAuth
Remove double comma in en/zh-CN locale settings keys that broke oxlint.
Add OpenAPI paths, docs/github-oauth.md, and README security note.
fix(ci): repair i18n syntax; document GitHub OAuth
Remove double comma in en/zh-CN locale settings keys that broke oxlint.
Add OpenAPI paths, docs/github-oauth.md, and README security note.
fix(ci): repair i18n syntax; document GitHub OAuth
Remove double comma in en/zh-CN locale settings keys that broke oxlint.
Add OpenAPI paths, docs/github-oauth.md, and README security note.
fix(ci): repair i18n syntax; document GitHub OAuth
Remove double comma in en/zh-CN locale settings keys that broke oxlint.
Add OpenAPI paths, docs/github-oauth.md, and README security note.
fix(ci): repair i18n syntax; document GitHub OAuth
Remove double comma in en/zh-CN locale settings keys that broke oxlint.
Add OpenAPI paths, docs/github-oauth.md, and README security note.
fix(ci): repair i18n syntax; document GitHub OAuth
Remove double comma in en/zh-CN locale settings keys that broke oxlint.
Add OpenAPI paths, docs/github-oauth.md, and README security note.
fix(ci): repair i18n syntax; document GitHub OAuth
Remove double comma in en/zh-CN locale settings keys that broke oxlint.
Add OpenAPI paths, docs/github-oauth.md, and README security note.
fix(ci): repair i18n syntax; document GitHub OAuth
Remove double comma in en/zh-CN locale settings keys that broke oxlint.
Add OpenAPI paths, docs/github-oauth.md, and README security note.
fix(ci): repair i18n syntax; document GitHub OAuth
Remove double comma in en/zh-CN locale settings keys that broke oxlint.
Add OpenAPI paths, docs/github-oauth.md, and README security note.
fix(ci): repair i18n syntax; document GitHub OAuth
Remove double comma in en/zh-CN locale settings keys that broke oxlint.
Add OpenAPI paths, docs/github-oauth.md, and README security note.
2026-09-26 01:14:01 +00:00
kazeyukiro c79e0b7a17 feat(auth): optional GitHub OAuth for panel login
Store client id/secret and allowed GitHub usernames in settings.
OAuth start/callback issues the same session JWT as password login;
first allowed login can bind to matching or sole admin account.
Login page shows Continue with GitHub when enabled.
feat(auth): optional GitHub OAuth for panel login
Store client id/secret and allowed GitHub usernames in settings.
OAuth start/callback issues the same session JWT as password login;
first allowed login can bind to matching or sole admin account.
Login page shows Continue with GitHub when enabled.
feat(auth): optional GitHub OAuth for panel login
Store client id/secret and allowed GitHub usernames in settings.
OAuth start/callback issues the same session JWT as password login;
first allowed login can bind to matching or sole admin account.
Login page shows Continue with GitHub when enabled.
feat(auth): optional GitHub OAuth for panel login
Store client id/secret and allowed GitHub usernames in settings.
OAuth start/callback issues the same session JWT as password login;
first allowed login can bind to matching or sole admin account.
Login page shows Continue with GitHub when enabled.
feat(auth): optional GitHub OAuth for panel login
Store client id/secret and allowed GitHub usernames in settings.
OAuth start/callback issues the same session JWT as password login;
first allowed login can bind to matching or sole admin account.
Login page shows Continue with GitHub when enabled.
feat(auth): optional GitHub OAuth for panel login
Store client id/secret and allowed GitHub usernames in settings.
OAuth start/callback issues the same session JWT as password login;
first allowed login can bind to matching or sole admin account.
Login page shows Continue with GitHub when enabled.
feat(auth): optional GitHub OAuth for panel login
Store client id/secret and allowed GitHub usernames in settings.
OAuth start/callback issues the same session JWT as password login;
first allowed login can bind to matching or sole admin account.
Login page shows Continue with GitHub when enabled.
feat(auth): optional GitHub OAuth for panel login
Store client id/secret and allowed GitHub usernames in settings.
OAuth start/callback issues the same session JWT as password login;
first allowed login can bind to matching or sole admin account.
Login page shows Continue with GitHub when enabled.
feat(auth): optional GitHub OAuth for panel login
Store client id/secret and allowed GitHub usernames in settings.
OAuth start/callback issues the same session JWT as password login;
first allowed login can bind to matching or sole admin account.
Login page shows Continue with GitHub when enabled.
feat(auth): optional GitHub OAuth for panel login
Store client id/secret and allowed GitHub usernames in settings.
OAuth start/callback issues the same session JWT as password login;
first allowed login can bind to matching or sole admin account.
Login page shows Continue with GitHub when enabled.
feat(auth): optional GitHub OAuth for panel login
Store client id/secret and allowed GitHub usernames in settings.
OAuth start/callback issues the same session JWT as password login;
first allowed login can bind to matching or sole admin account.
Login page shows Continue with GitHub when enabled.
feat(auth): optional GitHub OAuth for panel login
Store client id/secret and allowed GitHub usernames in settings.
OAuth start/callback issues the same session JWT as password login;
first allowed login can bind to matching or sole admin account.
Login page shows Continue with GitHub when enabled.
2026-09-26 01:05:10 +00:00
kazeyukiro 0918929195 fix(update): run self-update outside 3m-ui.service cgroup
Panel web update and any updater still inside the service were killed by
systemctl stop before start. Schedule via systemd-run (fallback setsid)
and re-escape in install.sh when cgroup is 3m-ui.service.
fix(update): run self-update outside 3m-ui.service cgroup
Panel web update and any updater still inside the service were killed by
systemctl stop before start. Schedule via systemd-run (fallback setsid)
and re-escape in install.sh when cgroup is 3m-ui.service.
fix(update): run self-update outside 3m-ui.service cgroup
Panel web update and any updater still inside the service were killed by
systemctl stop before start. Schedule via systemd-run (fallback setsid)
and re-escape in install.sh when cgroup is 3m-ui.service.
fix(update): run self-update outside 3m-ui.service cgroup
Panel web update and any updater still inside the service were killed by
systemctl stop before start. Schedule via systemd-run (fallback setsid)
and re-escape in install.sh when cgroup is 3m-ui.service.
fix(update): run self-update outside 3m-ui.service cgroup
Panel web update and any updater still inside the service were killed by
systemctl stop before start. Schedule via systemd-run (fallback setsid)
and re-escape in install.sh when cgroup is 3m-ui.service.
fix(update): run self-update outside 3m-ui.service cgroup
Panel web update and any updater still inside the service were killed by
systemctl stop before start. Schedule via systemd-run (fallback setsid)
and re-escape in install.sh when cgroup is 3m-ui.service.
fix(update): run self-update outside 3m-ui.service cgroup
Panel web update and any updater still inside the service were killed by
systemctl stop before start. Schedule via systemd-run (fallback setsid)
and re-escape in install.sh when cgroup is 3m-ui.service.
fix(update): run self-update outside 3m-ui.service cgroup
Panel web update and any updater still inside the service were killed by
systemctl stop before start. Schedule via systemd-run (fallback setsid)
and re-escape in install.sh when cgroup is 3m-ui.service.
fix(update): run self-update outside 3m-ui.service cgroup
Panel web update and any updater still inside the service were killed by
systemctl stop before start. Schedule via systemd-run (fallback setsid)
and re-escape in install.sh when cgroup is 3m-ui.service.
fix(update): run self-update outside 3m-ui.service cgroup
Panel web update and any updater still inside the service were killed by
systemctl stop before start. Schedule via systemd-run (fallback setsid)
and re-escape in install.sh when cgroup is 3m-ui.service.
fix(update): run self-update outside 3m-ui.service cgroup
Panel web update and any updater still inside the service were killed by
systemctl stop before start. Schedule via systemd-run (fallback setsid)
and re-escape in install.sh when cgroup is 3m-ui.service.
fix(update): run self-update outside 3m-ui.service cgroup
Panel web update and any updater still inside the service were killed by
systemctl stop before start. Schedule via systemd-run (fallback setsid)
and re-escape in install.sh when cgroup is 3m-ui.service.
2026-09-25 15:46:19 +00:00
Z User fff9539ca6 fix(ssl): sanitizeIPFilename must use _ not - to match acme package
The sanitizeIPFilename helper in listener/p2.go used '.' → '-' and
':' → '-', but acme/ip_issuer.go:sanitizeIPFilename uses '.' → '_'
and ':' → '_'.
For IP 85.149.212.214:
- p2.go generated: ip-85-149-212-214-cert.pem  (WRONG)
- acme generated:  ip-85_149_212_214-cert.pem  (CORRECT)
The mismatch meant os.Stat never found the IP cert file, so
resolveCertificatePair fell through to the autocert DirCache path
(which also failed because the domain IS an IP, not a hostname),
ultimately returning the confusing error:
panel SSL uses ACME with domain "85.149.212.214". The autocert
cache PEM was not found at /var/lib/3m-ui/acme/85.149.212.214 ...
Fix: change p2.go's sanitizeIPFilename to match acme/ip_issuer.go
exactly (dots and colons → underscore, not hyphen).
fix(ssl): sanitizeIPFilename must use _ not - to match acme package
The sanitizeIPFilename helper in listener/p2.go used '.' → '-' and
':' → '-', but acme/ip_issuer.go:sanitizeIPFilename uses '.' → '_'
and ':' → '_'.
For IP 85.149.212.214:
- p2.go generated: ip-85-149-212-214-cert.pem  (WRONG)
- acme generated:  ip-85_149_212_214-cert.pem  (CORRECT)
The mismatch meant os.Stat never found the IP cert file, so
resolveCertificatePair fell through to the autocert DirCache path
(which also failed because the domain IS an IP, not a hostname),
ultimately returning the confusing error:
panel SSL uses ACME with domain "85.149.212.214". The autocert
cache PEM was not found at /var/lib/3m-ui/acme/85.149.212.214 ...
Fix: change p2.go's sanitizeIPFilename to match acme/ip_issuer.go
exactly (dots and colons → underscore, not hyphen).
fix(ssl): sanitizeIPFilename must use _ not - to match acme package
The sanitizeIPFilename helper in listener/p2.go used '.' → '-' and
':' → '-', but acme/ip_issuer.go:sanitizeIPFilename uses '.' → '_'
and ':' → '_'.
For IP 85.149.212.214:
- p2.go generated: ip-85-149-212-214-cert.pem  (WRONG)
- acme generated:  ip-85_149_212_214-cert.pem  (CORRECT)
The mismatch meant os.Stat never found the IP cert file, so
resolveCertificatePair fell through to the autocert DirCache path
(which also failed because the domain IS an IP, not a hostname),
ultimately returning the confusing error:
panel SSL uses ACME with domain "85.149.212.214". The autocert
cache PEM was not found at /var/lib/3m-ui/acme/85.149.212.214 ...
Fix: change p2.go's sanitizeIPFilename to match acme/ip_issuer.go
exactly (dots and colons → underscore, not hyphen).
fix(ssl): sanitizeIPFilename must use _ not - to match acme package
The sanitizeIPFilename helper in listener/p2.go used '.' → '-' and
':' → '-', but acme/ip_issuer.go:sanitizeIPFilename uses '.' → '_'
and ':' → '_'.
For IP 85.149.212.214:
- p2.go generated: ip-85-149-212-214-cert.pem  (WRONG)
- acme generated:  ip-85_149_212_214-cert.pem  (CORRECT)
The mismatch meant os.Stat never found the IP cert file, so
resolveCertificatePair fell through to the autocert DirCache path
(which also failed because the domain IS an IP, not a hostname),
ultimately returning the confusing error:
panel SSL uses ACME with domain "85.149.212.214". The autocert
cache PEM was not found at /var/lib/3m-ui/acme/85.149.212.214 ...
Fix: change p2.go's sanitizeIPFilename to match acme/ip_issuer.go
exactly (dots and colons → underscore, not hyphen).
fix(ssl): sanitizeIPFilename must use _ not - to match acme package
The sanitizeIPFilename helper in listener/p2.go used '.' → '-' and
':' → '-', but acme/ip_issuer.go:sanitizeIPFilename uses '.' → '_'
and ':' → '_'.
For IP 85.149.212.214:
- p2.go generated: ip-85-149-212-214-cert.pem  (WRONG)
- acme generated:  ip-85_149_212_214-cert.pem  (CORRECT)
The mismatch meant os.Stat never found the IP cert file, so
resolveCertificatePair fell through to the autocert DirCache path
(which also failed because the domain IS an IP, not a hostname),
ultimately returning the confusing error:
panel SSL uses ACME with domain "85.149.212.214". The autocert
cache PEM was not found at /var/lib/3m-ui/acme/85.149.212.214 ...
Fix: change p2.go's sanitizeIPFilename to match acme/ip_issuer.go
exactly (dots and colons → underscore, not hyphen).
fix(ssl): sanitizeIPFilename must use _ not - to match acme package
The sanitizeIPFilename helper in listener/p2.go used '.' → '-' and
':' → '-', but acme/ip_issuer.go:sanitizeIPFilename uses '.' → '_'
and ':' → '_'.
For IP 85.149.212.214:
- p2.go generated: ip-85-149-212-214-cert.pem  (WRONG)
- acme generated:  ip-85_149_212_214-cert.pem  (CORRECT)
The mismatch meant os.Stat never found the IP cert file, so
resolveCertificatePair fell through to the autocert DirCache path
(which also failed because the domain IS an IP, not a hostname),
ultimately returning the confusing error:
panel SSL uses ACME with domain "85.149.212.214". The autocert
cache PEM was not found at /var/lib/3m-ui/acme/85.149.212.214 ...
Fix: change p2.go's sanitizeIPFilename to match acme/ip_issuer.go
exactly (dots and colons → underscore, not hyphen).
fix(ssl): sanitizeIPFilename must use _ not - to match acme package
The sanitizeIPFilename helper in listener/p2.go used '.' → '-' and
':' → '-', but acme/ip_issuer.go:sanitizeIPFilename uses '.' → '_'
and ':' → '_'.
For IP 85.149.212.214:
- p2.go generated: ip-85-149-212-214-cert.pem  (WRONG)
- acme generated:  ip-85_149_212_214-cert.pem  (CORRECT)
The mismatch meant os.Stat never found the IP cert file, so
resolveCertificatePair fell through to the autocert DirCache path
(which also failed because the domain IS an IP, not a hostname),
ultimately returning the confusing error:
panel SSL uses ACME with domain "85.149.212.214". The autocert
cache PEM was not found at /var/lib/3m-ui/acme/85.149.212.214 ...
Fix: change p2.go's sanitizeIPFilename to match acme/ip_issuer.go
exactly (dots and colons → underscore, not hyphen).
fix(ssl): sanitizeIPFilename must use _ not - to match acme package
The sanitizeIPFilename helper in listener/p2.go used '.' → '-' and
':' → '-', but acme/ip_issuer.go:sanitizeIPFilename uses '.' → '_'
and ':' → '_'.
For IP 85.149.212.214:
- p2.go generated: ip-85-149-212-214-cert.pem  (WRONG)
- acme generated:  ip-85_149_212_214-cert.pem  (CORRECT)
The mismatch meant os.Stat never found the IP cert file, so
resolveCertificatePair fell through to the autocert DirCache path
(which also failed because the domain IS an IP, not a hostname),
ultimately returning the confusing error:
panel SSL uses ACME with domain "85.149.212.214". The autocert
cache PEM was not found at /var/lib/3m-ui/acme/85.149.212.214 ...
Fix: change p2.go's sanitizeIPFilename to match acme/ip_issuer.go
exactly (dots and colons → underscore, not hyphen).
fix(ssl): sanitizeIPFilename must use _ not - to match acme package
The sanitizeIPFilename helper in listener/p2.go used '.' → '-' and
':' → '-', but acme/ip_issuer.go:sanitizeIPFilename uses '.' → '_'
and ':' → '_'.
For IP 85.149.212.214:
- p2.go generated: ip-85-149-212-214-cert.pem  (WRONG)
- acme generated:  ip-85_149_212_214-cert.pem  (CORRECT)
The mismatch meant os.Stat never found the IP cert file, so
resolveCertificatePair fell through to the autocert DirCache path
(which also failed because the domain IS an IP, not a hostname),
ultimately returning the confusing error:
panel SSL uses ACME with domain "85.149.212.214". The autocert
cache PEM was not found at /var/lib/3m-ui/acme/85.149.212.214 ...
Fix: change p2.go's sanitizeIPFilename to match acme/ip_issuer.go
exactly (dots and colons → underscore, not hyphen).
fix(ssl): sanitizeIPFilename must use _ not - to match acme package
The sanitizeIPFilename helper in listener/p2.go used '.' → '-' and
':' → '-', but acme/ip_issuer.go:sanitizeIPFilename uses '.' → '_'
and ':' → '_'.
For IP 85.149.212.214:
- p2.go generated: ip-85-149-212-214-cert.pem  (WRONG)
- acme generated:  ip-85_149_212_214-cert.pem  (CORRECT)
The mismatch meant os.Stat never found the IP cert file, so
resolveCertificatePair fell through to the autocert DirCache path
(which also failed because the domain IS an IP, not a hostname),
ultimately returning the confusing error:
panel SSL uses ACME with domain "85.149.212.214". The autocert
cache PEM was not found at /var/lib/3m-ui/acme/85.149.212.214 ...
Fix: change p2.go's sanitizeIPFilename to match acme/ip_issuer.go
exactly (dots and colons → underscore, not hyphen).
fix(ssl): sanitizeIPFilename must use _ not - to match acme package
The sanitizeIPFilename helper in listener/p2.go used '.' → '-' and
':' → '-', but acme/ip_issuer.go:sanitizeIPFilename uses '.' → '_'
and ':' → '_'.
For IP 85.149.212.214:
- p2.go generated: ip-85-149-212-214-cert.pem  (WRONG)
- acme generated:  ip-85_149_212_214-cert.pem  (CORRECT)
The mismatch meant os.Stat never found the IP cert file, so
resolveCertificatePair fell through to the autocert DirCache path
(which also failed because the domain IS an IP, not a hostname),
ultimately returning the confusing error:
panel SSL uses ACME with domain "85.149.212.214". The autocert
cache PEM was not found at /var/lib/3m-ui/acme/85.149.212.214 ...
Fix: change p2.go's sanitizeIPFilename to match acme/ip_issuer.go
exactly (dots and colons → underscore, not hyphen).
fix(ssl): sanitizeIPFilename must use _ not - to match acme package
The sanitizeIPFilename helper in listener/p2.go used '.' → '-' and
':' → '-', but acme/ip_issuer.go:sanitizeIPFilename uses '.' → '_'
and ':' → '_'.
For IP 85.149.212.214:
- p2.go generated: ip-85-149-212-214-cert.pem  (WRONG)
- acme generated:  ip-85_149_212_214-cert.pem  (CORRECT)
The mismatch meant os.Stat never found the IP cert file, so
resolveCertificatePair fell through to the autocert DirCache path
(which also failed because the domain IS an IP, not a hostname),
ultimately returning the confusing error:
panel SSL uses ACME with domain "85.149.212.214". The autocert
cache PEM was not found at /var/lib/3m-ui/acme/85.149.212.214 ...
Fix: change p2.go's sanitizeIPFilename to match acme/ip_issuer.go
exactly (dots and colons → underscore, not hyphen).
2026-09-25 14:55:24 +00:00
Z User 76c83cbf8e fix(ssl): extract PEM from autocert DirCache for domain ACME
The previous fix only handled IP certificates (acmez). Domain ACME
(autocert.DirCache) was left as 'paste PEM or use /etc/letsencrypt/'
because I incorrectly assumed the cache was in Go binary format.
In reality, autocert's DirCache stores the certificate as concatenated
PEM blocks (PRIVATE KEY + CERTIFICATE) in a file named <cacheDir>/<domain>
(no extension). This is the format autocert's cacheGet() reads via
pem.Decode.
Fix: when FromPanelSSL=true and both manual cert_file/key_file and IP
cert PEM files are absent, try reading <cacheDir>/<domain>. If it
contains both 'PRIVATE KEY' and 'CERTIFICATE' PEM blocks, split them:
- Private key: from '-----BEGIN' to the first '-----END ...\n'
- Certificate: from '-----BEGIN CERTIFICATE-----' to end
If the cache file doesn't exist (cert not yet issued — common on a
fresh install where the panel hasn't been visited over HTTPS yet),
return a clear actionable error:
'Visit the panel over HTTPS once to trigger cert issuance, then retry.'
Tests: 23/23 packages PASS.
fix(ssl): extract PEM from autocert DirCache for domain ACME
The previous fix only handled IP certificates (acmez). Domain ACME
(autocert.DirCache) was left as 'paste PEM or use /etc/letsencrypt/'
because I incorrectly assumed the cache was in Go binary format.
In reality, autocert's DirCache stores the certificate as concatenated
PEM blocks (PRIVATE KEY + CERTIFICATE) in a file named <cacheDir>/<domain>
(no extension). This is the format autocert's cacheGet() reads via
pem.Decode.
Fix: when FromPanelSSL=true and both manual cert_file/key_file and IP
cert PEM files are absent, try reading <cacheDir>/<domain>. If it
contains both 'PRIVATE KEY' and 'CERTIFICATE' PEM blocks, split them:
- Private key: from '-----BEGIN' to the first '-----END ...\n'
- Certificate: from '-----BEGIN CERTIFICATE-----' to end
If the cache file doesn't exist (cert not yet issued — common on a
fresh install where the panel hasn't been visited over HTTPS yet),
return a clear actionable error:
'Visit the panel over HTTPS once to trigger cert issuance, then retry.'
Tests: 23/23 packages PASS.
fix(ssl): extract PEM from autocert DirCache for domain ACME
The previous fix only handled IP certificates (acmez). Domain ACME
(autocert.DirCache) was left as 'paste PEM or use /etc/letsencrypt/'
because I incorrectly assumed the cache was in Go binary format.
In reality, autocert's DirCache stores the certificate as concatenated
PEM blocks (PRIVATE KEY + CERTIFICATE) in a file named <cacheDir>/<domain>
(no extension). This is the format autocert's cacheGet() reads via
pem.Decode.
Fix: when FromPanelSSL=true and both manual cert_file/key_file and IP
cert PEM files are absent, try reading <cacheDir>/<domain>. If it
contains both 'PRIVATE KEY' and 'CERTIFICATE' PEM blocks, split them:
- Private key: from '-----BEGIN' to the first '-----END ...\n'
- Certificate: from '-----BEGIN CERTIFICATE-----' to end
If the cache file doesn't exist (cert not yet issued — common on a
fresh install where the panel hasn't been visited over HTTPS yet),
return a clear actionable error:
'Visit the panel over HTTPS once to trigger cert issuance, then retry.'
Tests: 23/23 packages PASS.
fix(ssl): extract PEM from autocert DirCache for domain ACME
The previous fix only handled IP certificates (acmez). Domain ACME
(autocert.DirCache) was left as 'paste PEM or use /etc/letsencrypt/'
because I incorrectly assumed the cache was in Go binary format.
In reality, autocert's DirCache stores the certificate as concatenated
PEM blocks (PRIVATE KEY + CERTIFICATE) in a file named <cacheDir>/<domain>
(no extension). This is the format autocert's cacheGet() reads via
pem.Decode.
Fix: when FromPanelSSL=true and both manual cert_file/key_file and IP
cert PEM files are absent, try reading <cacheDir>/<domain>. If it
contains both 'PRIVATE KEY' and 'CERTIFICATE' PEM blocks, split them:
- Private key: from '-----BEGIN' to the first '-----END ...\n'
- Certificate: from '-----BEGIN CERTIFICATE-----' to end
If the cache file doesn't exist (cert not yet issued — common on a
fresh install where the panel hasn't been visited over HTTPS yet),
return a clear actionable error:
'Visit the panel over HTTPS once to trigger cert issuance, then retry.'
Tests: 23/23 packages PASS.
fix(ssl): extract PEM from autocert DirCache for domain ACME
The previous fix only handled IP certificates (acmez). Domain ACME
(autocert.DirCache) was left as 'paste PEM or use /etc/letsencrypt/'
because I incorrectly assumed the cache was in Go binary format.
In reality, autocert's DirCache stores the certificate as concatenated
PEM blocks (PRIVATE KEY + CERTIFICATE) in a file named <cacheDir>/<domain>
(no extension). This is the format autocert's cacheGet() reads via
pem.Decode.
Fix: when FromPanelSSL=true and both manual cert_file/key_file and IP
cert PEM files are absent, try reading <cacheDir>/<domain>. If it
contains both 'PRIVATE KEY' and 'CERTIFICATE' PEM blocks, split them:
- Private key: from '-----BEGIN' to the first '-----END ...\n'
- Certificate: from '-----BEGIN CERTIFICATE-----' to end
If the cache file doesn't exist (cert not yet issued — common on a
fresh install where the panel hasn't been visited over HTTPS yet),
return a clear actionable error:
'Visit the panel over HTTPS once to trigger cert issuance, then retry.'
Tests: 23/23 packages PASS.
fix(ssl): extract PEM from autocert DirCache for domain ACME
The previous fix only handled IP certificates (acmez). Domain ACME
(autocert.DirCache) was left as 'paste PEM or use /etc/letsencrypt/'
because I incorrectly assumed the cache was in Go binary format.
In reality, autocert's DirCache stores the certificate as concatenated
PEM blocks (PRIVATE KEY + CERTIFICATE) in a file named <cacheDir>/<domain>
(no extension). This is the format autocert's cacheGet() reads via
pem.Decode.
Fix: when FromPanelSSL=true and both manual cert_file/key_file and IP
cert PEM files are absent, try reading <cacheDir>/<domain>. If it
contains both 'PRIVATE KEY' and 'CERTIFICATE' PEM blocks, split them:
- Private key: from '-----BEGIN' to the first '-----END ...\n'
- Certificate: from '-----BEGIN CERTIFICATE-----' to end
If the cache file doesn't exist (cert not yet issued — common on a
fresh install where the panel hasn't been visited over HTTPS yet),
return a clear actionable error:
'Visit the panel over HTTPS once to trigger cert issuance, then retry.'
Tests: 23/23 packages PASS.
fix(ssl): extract PEM from autocert DirCache for domain ACME
The previous fix only handled IP certificates (acmez). Domain ACME
(autocert.DirCache) was left as 'paste PEM or use /etc/letsencrypt/'
because I incorrectly assumed the cache was in Go binary format.
In reality, autocert's DirCache stores the certificate as concatenated
PEM blocks (PRIVATE KEY + CERTIFICATE) in a file named <cacheDir>/<domain>
(no extension). This is the format autocert's cacheGet() reads via
pem.Decode.
Fix: when FromPanelSSL=true and both manual cert_file/key_file and IP
cert PEM files are absent, try reading <cacheDir>/<domain>. If it
contains both 'PRIVATE KEY' and 'CERTIFICATE' PEM blocks, split them:
- Private key: from '-----BEGIN' to the first '-----END ...\n'
- Certificate: from '-----BEGIN CERTIFICATE-----' to end
If the cache file doesn't exist (cert not yet issued — common on a
fresh install where the panel hasn't been visited over HTTPS yet),
return a clear actionable error:
'Visit the panel over HTTPS once to trigger cert issuance, then retry.'
Tests: 23/23 packages PASS.
fix(ssl): extract PEM from autocert DirCache for domain ACME
The previous fix only handled IP certificates (acmez). Domain ACME
(autocert.DirCache) was left as 'paste PEM or use /etc/letsencrypt/'
because I incorrectly assumed the cache was in Go binary format.
In reality, autocert's DirCache stores the certificate as concatenated
PEM blocks (PRIVATE KEY + CERTIFICATE) in a file named <cacheDir>/<domain>
(no extension). This is the format autocert's cacheGet() reads via
pem.Decode.
Fix: when FromPanelSSL=true and both manual cert_file/key_file and IP
cert PEM files are absent, try reading <cacheDir>/<domain>. If it
contains both 'PRIVATE KEY' and 'CERTIFICATE' PEM blocks, split them:
- Private key: from '-----BEGIN' to the first '-----END ...\n'
- Certificate: from '-----BEGIN CERTIFICATE-----' to end
If the cache file doesn't exist (cert not yet issued — common on a
fresh install where the panel hasn't been visited over HTTPS yet),
return a clear actionable error:
'Visit the panel over HTTPS once to trigger cert issuance, then retry.'
Tests: 23/23 packages PASS.
fix(ssl): extract PEM from autocert DirCache for domain ACME
The previous fix only handled IP certificates (acmez). Domain ACME
(autocert.DirCache) was left as 'paste PEM or use /etc/letsencrypt/'
because I incorrectly assumed the cache was in Go binary format.
In reality, autocert's DirCache stores the certificate as concatenated
PEM blocks (PRIVATE KEY + CERTIFICATE) in a file named <cacheDir>/<domain>
(no extension). This is the format autocert's cacheGet() reads via
pem.Decode.
Fix: when FromPanelSSL=true and both manual cert_file/key_file and IP
cert PEM files are absent, try reading <cacheDir>/<domain>. If it
contains both 'PRIVATE KEY' and 'CERTIFICATE' PEM blocks, split them:
- Private key: from '-----BEGIN' to the first '-----END ...\n'
- Certificate: from '-----BEGIN CERTIFICATE-----' to end
If the cache file doesn't exist (cert not yet issued — common on a
fresh install where the panel hasn't been visited over HTTPS yet),
return a clear actionable error:
'Visit the panel over HTTPS once to trigger cert issuance, then retry.'
Tests: 23/23 packages PASS.
fix(ssl): extract PEM from autocert DirCache for domain ACME
The previous fix only handled IP certificates (acmez). Domain ACME
(autocert.DirCache) was left as 'paste PEM or use /etc/letsencrypt/'
because I incorrectly assumed the cache was in Go binary format.
In reality, autocert's DirCache stores the certificate as concatenated
PEM blocks (PRIVATE KEY + CERTIFICATE) in a file named <cacheDir>/<domain>
(no extension). This is the format autocert's cacheGet() reads via
pem.Decode.
Fix: when FromPanelSSL=true and both manual cert_file/key_file and IP
cert PEM files are absent, try reading <cacheDir>/<domain>. If it
contains both 'PRIVATE KEY' and 'CERTIFICATE' PEM blocks, split them:
- Private key: from '-----BEGIN' to the first '-----END ...\n'
- Certificate: from '-----BEGIN CERTIFICATE-----' to end
If the cache file doesn't exist (cert not yet issued — common on a
fresh install where the panel hasn't been visited over HTTPS yet),
return a clear actionable error:
'Visit the panel over HTTPS once to trigger cert issuance, then retry.'
Tests: 23/23 packages PASS.
fix(ssl): extract PEM from autocert DirCache for domain ACME
The previous fix only handled IP certificates (acmez). Domain ACME
(autocert.DirCache) was left as 'paste PEM or use /etc/letsencrypt/'
because I incorrectly assumed the cache was in Go binary format.
In reality, autocert's DirCache stores the certificate as concatenated
PEM blocks (PRIVATE KEY + CERTIFICATE) in a file named <cacheDir>/<domain>
(no extension). This is the format autocert's cacheGet() reads via
pem.Decode.
Fix: when FromPanelSSL=true and both manual cert_file/key_file and IP
cert PEM files are absent, try reading <cacheDir>/<domain>. If it
contains both 'PRIVATE KEY' and 'CERTIFICATE' PEM blocks, split them:
- Private key: from '-----BEGIN' to the first '-----END ...\n'
- Certificate: from '-----BEGIN CERTIFICATE-----' to end
If the cache file doesn't exist (cert not yet issued — common on a
fresh install where the panel hasn't been visited over HTTPS yet),
return a clear actionable error:
'Visit the panel over HTTPS once to trigger cert issuance, then retry.'
Tests: 23/23 packages PASS.
fix(ssl): extract PEM from autocert DirCache for domain ACME
The previous fix only handled IP certificates (acmez). Domain ACME
(autocert.DirCache) was left as 'paste PEM or use /etc/letsencrypt/'
because I incorrectly assumed the cache was in Go binary format.
In reality, autocert's DirCache stores the certificate as concatenated
PEM blocks (PRIVATE KEY + CERTIFICATE) in a file named <cacheDir>/<domain>
(no extension). This is the format autocert's cacheGet() reads via
pem.Decode.
Fix: when FromPanelSSL=true and both manual cert_file/key_file and IP
cert PEM files are absent, try reading <cacheDir>/<domain>. If it
contains both 'PRIVATE KEY' and 'CERTIFICATE' PEM blocks, split them:
- Private key: from '-----BEGIN' to the first '-----END ...\n'
- Certificate: from '-----BEGIN CERTIFICATE-----' to end
If the cache file doesn't exist (cert not yet issued — common on a
fresh install where the panel hasn't been visited over HTTPS yet),
return a clear actionable error:
'Visit the panel over HTTPS once to trigger cert issuance, then retry.'
Tests: 23/23 packages PASS.
2026-09-25 14:08:58 +00:00
Z User 5dee84b598 docs(api): update OpenAPI with channel switching fields
Update /system/update-info and /system/update endpoint docs to reflect
the stable ↔ pre-release channel switching feature (commit 78111ef):
- /system/update-info: now documents current_channel, target_channel,
latest_stable, latest_pre fields. Description explains both channels
are queried simultaneously.
- /system/update: now documents the requestBody with channel field
(stable | pre), and the response includes the channel field.
Description explains how channel parameter maps to update.sh env.
Applied to both openapi.yaml files (internal + public).
docs(api): update OpenAPI with channel switching fields
Update /system/update-info and /system/update endpoint docs to reflect
the stable ↔ pre-release channel switching feature (commit 78111ef):
- /system/update-info: now documents current_channel, target_channel,
latest_stable, latest_pre fields. Description explains both channels
are queried simultaneously.
- /system/update: now documents the requestBody with channel field
(stable | pre), and the response includes the channel field.
Description explains how channel parameter maps to update.sh env.
Applied to both openapi.yaml files (internal + public).
docs(api): update OpenAPI with channel switching fields
Update /system/update-info and /system/update endpoint docs to reflect
the stable ↔ pre-release channel switching feature (commit 78111ef):
- /system/update-info: now documents current_channel, target_channel,
latest_stable, latest_pre fields. Description explains both channels
are queried simultaneously.
- /system/update: now documents the requestBody with channel field
(stable | pre), and the response includes the channel field.
Description explains how channel parameter maps to update.sh env.
Applied to both openapi.yaml files (internal + public).
docs(api): update OpenAPI with channel switching fields
Update /system/update-info and /system/update endpoint docs to reflect
the stable ↔ pre-release channel switching feature (commit 78111ef):
- /system/update-info: now documents current_channel, target_channel,
latest_stable, latest_pre fields. Description explains both channels
are queried simultaneously.
- /system/update: now documents the requestBody with channel field
(stable | pre), and the response includes the channel field.
Description explains how channel parameter maps to update.sh env.
Applied to both openapi.yaml files (internal + public).
docs(api): update OpenAPI with channel switching fields
Update /system/update-info and /system/update endpoint docs to reflect
the stable ↔ pre-release channel switching feature (commit 78111ef):
- /system/update-info: now documents current_channel, target_channel,
latest_stable, latest_pre fields. Description explains both channels
are queried simultaneously.
- /system/update: now documents the requestBody with channel field
(stable | pre), and the response includes the channel field.
Description explains how channel parameter maps to update.sh env.
Applied to both openapi.yaml files (internal + public).
docs(api): update OpenAPI with channel switching fields
Update /system/update-info and /system/update endpoint docs to reflect
the stable ↔ pre-release channel switching feature (commit 78111ef):
- /system/update-info: now documents current_channel, target_channel,
latest_stable, latest_pre fields. Description explains both channels
are queried simultaneously.
- /system/update: now documents the requestBody with channel field
(stable | pre), and the response includes the channel field.
Description explains how channel parameter maps to update.sh env.
Applied to both openapi.yaml files (internal + public).
docs(api): update OpenAPI with channel switching fields
Update /system/update-info and /system/update endpoint docs to reflect
the stable ↔ pre-release channel switching feature (commit 78111ef):
- /system/update-info: now documents current_channel, target_channel,
latest_stable, latest_pre fields. Description explains both channels
are queried simultaneously.
- /system/update: now documents the requestBody with channel field
(stable | pre), and the response includes the channel field.
Description explains how channel parameter maps to update.sh env.
Applied to both openapi.yaml files (internal + public).
docs(api): update OpenAPI with channel switching fields
Update /system/update-info and /system/update endpoint docs to reflect
the stable ↔ pre-release channel switching feature (commit 78111ef):
- /system/update-info: now documents current_channel, target_channel,
latest_stable, latest_pre fields. Description explains both channels
are queried simultaneously.
- /system/update: now documents the requestBody with channel field
(stable | pre), and the response includes the channel field.
Description explains how channel parameter maps to update.sh env.
Applied to both openapi.yaml files (internal + public).
docs(api): update OpenAPI with channel switching fields
Update /system/update-info and /system/update endpoint docs to reflect
the stable ↔ pre-release channel switching feature (commit 78111ef):
- /system/update-info: now documents current_channel, target_channel,
latest_stable, latest_pre fields. Description explains both channels
are queried simultaneously.
- /system/update: now documents the requestBody with channel field
(stable | pre), and the response includes the channel field.
Description explains how channel parameter maps to update.sh env.
Applied to both openapi.yaml files (internal + public).
docs(api): update OpenAPI with channel switching fields
Update /system/update-info and /system/update endpoint docs to reflect
the stable ↔ pre-release channel switching feature (commit 78111ef):
- /system/update-info: now documents current_channel, target_channel,
latest_stable, latest_pre fields. Description explains both channels
are queried simultaneously.
- /system/update: now documents the requestBody with channel field
(stable | pre), and the response includes the channel field.
Description explains how channel parameter maps to update.sh env.
Applied to both openapi.yaml files (internal + public).
docs(api): update OpenAPI with channel switching fields
Update /system/update-info and /system/update endpoint docs to reflect
the stable ↔ pre-release channel switching feature (commit 78111ef):
- /system/update-info: now documents current_channel, target_channel,
latest_stable, latest_pre fields. Description explains both channels
are queried simultaneously.
- /system/update: now documents the requestBody with channel field
(stable | pre), and the response includes the channel field.
Description explains how channel parameter maps to update.sh env.
Applied to both openapi.yaml files (internal + public).
docs(api): update OpenAPI with channel switching fields
Update /system/update-info and /system/update endpoint docs to reflect
the stable ↔ pre-release channel switching feature (commit 78111ef):
- /system/update-info: now documents current_channel, target_channel,
latest_stable, latest_pre fields. Description explains both channels
are queried simultaneously.
- /system/update: now documents the requestBody with channel field
(stable | pre), and the response includes the channel field.
Description explains how channel parameter maps to update.sh env.
Applied to both openapi.yaml files (internal + public).
2026-09-25 13:41:24 +00:00
Z User c954fb9085 feat(update): stable ↔ pre-release channel switching from web UI
Adds the ability to switch between stable and pre-release channels
directly from the Settings → System ops → Panel update card — no SSH
required.
Backend:
- GET /system/update-info: now queries BOTH GitHub releases/latest
(stable) AND releases/tags/pre simultaneously. Returns:
current_channel ('stable' | 'pre' — detected from CHANNEL file)
target_channel (the one you're NOT on — for switch prompt)
latest_stable (latest stable tag, e.g. v1.3.0)
latest_pre (latest pre tag, e.g. pre)
latest_version (target channel's latest — for update_available)
Channel detection reads the CHANNEL file alongside the binary
(written by install.sh). Also checks version string for -rc/-pre
suffixes as fallback.
- POST /system/update: accepts { channel: 'stable' | 'pre' } in the
request body. When channel='pre', passes THREE_M_UI_CHANNEL=pre env
+ 'pre' positional arg to update.sh. When channel='stable', passes
THREE_M_UI_CHANNEL=stable. Response includes the selected channel +
a descriptive message ('Switching to pre-release channel...').
Frontend:
- Update card now shows current channel badge (cyan 'Stable' or
purple 'Pre-release') next to the version tag.
- Segmented control to switch between Stable and Pre-release channels.
Each option shows the latest version tag for that channel inline
(e.g. 'Stable v1.3.0' / 'Pre-release pre').
- Switching the Segmented recomputes latest_version +
update_available for the newly-selected target channel.
- 'Update now' button passes the selected channel to POST /system/update.
Button tooltip reflects the action: 'Switch to pre-release channel' or
'Switch to stable channel'.
- Release notes reflect the target channel's GitHub release body.
i18n: 3 new keys (stable, preRelease, channelSwitch) in all 18 locales.
zh-CN: 稳定版 / 预发布版 / 切换渠道
Pre-release URL: https://github.com/kazeyukiro/3m-ui/releases/tag/pre
Tests: 23/23 backend PASS, frontend build + lint + 16/16 unit tests PASS.
feat(update): stable ↔ pre-release channel switching from web UI
Adds the ability to switch between stable and pre-release channels
directly from the Settings → System ops → Panel update card — no SSH
required.
Backend:
- GET /system/update-info: now queries BOTH GitHub releases/latest
(stable) AND releases/tags/pre simultaneously. Returns:
current_channel ('stable' | 'pre' — detected from CHANNEL file)
target_channel (the one you're NOT on — for switch prompt)
latest_stable (latest stable tag, e.g. v1.3.0)
latest_pre (latest pre tag, e.g. pre)
latest_version (target channel's latest — for update_available)
Channel detection reads the CHANNEL file alongside the binary
(written by install.sh). Also checks version string for -rc/-pre
suffixes as fallback.
- POST /system/update: accepts { channel: 'stable' | 'pre' } in the
request body. When channel='pre', passes THREE_M_UI_CHANNEL=pre env
+ 'pre' positional arg to update.sh. When channel='stable', passes
THREE_M_UI_CHANNEL=stable. Response includes the selected channel +
a descriptive message ('Switching to pre-release channel...').
Frontend:
- Update card now shows current channel badge (cyan 'Stable' or
purple 'Pre-release') next to the version tag.
- Segmented control to switch between Stable and Pre-release channels.
Each option shows the latest version tag for that channel inline
(e.g. 'Stable v1.3.0' / 'Pre-release pre').
- Switching the Segmented recomputes latest_version +
update_available for the newly-selected target channel.
- 'Update now' button passes the selected channel to POST /system/update.
Button tooltip reflects the action: 'Switch to pre-release channel' or
'Switch to stable channel'.
- Release notes reflect the target channel's GitHub release body.
i18n: 3 new keys (stable, preRelease, channelSwitch) in all 18 locales.
zh-CN: 稳定版 / 预发布版 / 切换渠道
Pre-release URL: https://github.com/kazeyukiro/3m-ui/releases/tag/pre
Tests: 23/23 backend PASS, frontend build + lint + 16/16 unit tests PASS.
feat(update): stable ↔ pre-release channel switching from web UI
Adds the ability to switch between stable and pre-release channels
directly from the Settings → System ops → Panel update card — no SSH
required.
Backend:
- GET /system/update-info: now queries BOTH GitHub releases/latest
(stable) AND releases/tags/pre simultaneously. Returns:
current_channel ('stable' | 'pre' — detected from CHANNEL file)
target_channel (the one you're NOT on — for switch prompt)
latest_stable (latest stable tag, e.g. v1.3.0)
latest_pre (latest pre tag, e.g. pre)
latest_version (target channel's latest — for update_available)
Channel detection reads the CHANNEL file alongside the binary
(written by install.sh). Also checks version string for -rc/-pre
suffixes as fallback.
- POST /system/update: accepts { channel: 'stable' | 'pre' } in the
request body. When channel='pre', passes THREE_M_UI_CHANNEL=pre env
+ 'pre' positional arg to update.sh. When channel='stable', passes
THREE_M_UI_CHANNEL=stable. Response includes the selected channel +
a descriptive message ('Switching to pre-release channel...').
Frontend:
- Update card now shows current channel badge (cyan 'Stable' or
purple 'Pre-release') next to the version tag.
- Segmented control to switch between Stable and Pre-release channels.
Each option shows the latest version tag for that channel inline
(e.g. 'Stable v1.3.0' / 'Pre-release pre').
- Switching the Segmented recomputes latest_version +
update_available for the newly-selected target channel.
- 'Update now' button passes the selected channel to POST /system/update.
Button tooltip reflects the action: 'Switch to pre-release channel' or
'Switch to stable channel'.
- Release notes reflect the target channel's GitHub release body.
i18n: 3 new keys (stable, preRelease, channelSwitch) in all 18 locales.
zh-CN: 稳定版 / 预发布版 / 切换渠道
Pre-release URL: https://github.com/kazeyukiro/3m-ui/releases/tag/pre
Tests: 23/23 backend PASS, frontend build + lint + 16/16 unit tests PASS.
feat(update): stable ↔ pre-release channel switching from web UI
Adds the ability to switch between stable and pre-release channels
directly from the Settings → System ops → Panel update card — no SSH
required.
Backend:
- GET /system/update-info: now queries BOTH GitHub releases/latest
(stable) AND releases/tags/pre simultaneously. Returns:
current_channel ('stable' | 'pre' — detected from CHANNEL file)
target_channel (the one you're NOT on — for switch prompt)
latest_stable (latest stable tag, e.g. v1.3.0)
latest_pre (latest pre tag, e.g. pre)
latest_version (target channel's latest — for update_available)
Channel detection reads the CHANNEL file alongside the binary
(written by install.sh). Also checks version string for -rc/-pre
suffixes as fallback.
- POST /system/update: accepts { channel: 'stable' | 'pre' } in the
request body. When channel='pre', passes THREE_M_UI_CHANNEL=pre env
+ 'pre' positional arg to update.sh. When channel='stable', passes
THREE_M_UI_CHANNEL=stable. Response includes the selected channel +
a descriptive message ('Switching to pre-release channel...').
Frontend:
- Update card now shows current channel badge (cyan 'Stable' or
purple 'Pre-release') next to the version tag.
- Segmented control to switch between Stable and Pre-release channels.
Each option shows the latest version tag for that channel inline
(e.g. 'Stable v1.3.0' / 'Pre-release pre').
- Switching the Segmented recomputes latest_version +
update_available for the newly-selected target channel.
- 'Update now' button passes the selected channel to POST /system/update.
Button tooltip reflects the action: 'Switch to pre-release channel' or
'Switch to stable channel'.
- Release notes reflect the target channel's GitHub release body.
i18n: 3 new keys (stable, preRelease, channelSwitch) in all 18 locales.
zh-CN: 稳定版 / 预发布版 / 切换渠道
Pre-release URL: https://github.com/kazeyukiro/3m-ui/releases/tag/pre
Tests: 23/23 backend PASS, frontend build + lint + 16/16 unit tests PASS.
feat(update): stable ↔ pre-release channel switching from web UI
Adds the ability to switch between stable and pre-release channels
directly from the Settings → System ops → Panel update card — no SSH
required.
Backend:
- GET /system/update-info: now queries BOTH GitHub releases/latest
(stable) AND releases/tags/pre simultaneously. Returns:
current_channel ('stable' | 'pre' — detected from CHANNEL file)
target_channel (the one you're NOT on — for switch prompt)
latest_stable (latest stable tag, e.g. v1.3.0)
latest_pre (latest pre tag, e.g. pre)
latest_version (target channel's latest — for update_available)
Channel detection reads the CHANNEL file alongside the binary
(written by install.sh). Also checks version string for -rc/-pre
suffixes as fallback.
- POST /system/update: accepts { channel: 'stable' | 'pre' } in the
request body. When channel='pre', passes THREE_M_UI_CHANNEL=pre env
+ 'pre' positional arg to update.sh. When channel='stable', passes
THREE_M_UI_CHANNEL=stable. Response includes the selected channel +
a descriptive message ('Switching to pre-release channel...').
Frontend:
- Update card now shows current channel badge (cyan 'Stable' or
purple 'Pre-release') next to the version tag.
- Segmented control to switch between Stable and Pre-release channels.
Each option shows the latest version tag for that channel inline
(e.g. 'Stable v1.3.0' / 'Pre-release pre').
- Switching the Segmented recomputes latest_version +
update_available for the newly-selected target channel.
- 'Update now' button passes the selected channel to POST /system/update.
Button tooltip reflects the action: 'Switch to pre-release channel' or
'Switch to stable channel'.
- Release notes reflect the target channel's GitHub release body.
i18n: 3 new keys (stable, preRelease, channelSwitch) in all 18 locales.
zh-CN: 稳定版 / 预发布版 / 切换渠道
Pre-release URL: https://github.com/kazeyukiro/3m-ui/releases/tag/pre
Tests: 23/23 backend PASS, frontend build + lint + 16/16 unit tests PASS.
feat(update): stable ↔ pre-release channel switching from web UI
Adds the ability to switch between stable and pre-release channels
directly from the Settings → System ops → Panel update card — no SSH
required.
Backend:
- GET /system/update-info: now queries BOTH GitHub releases/latest
(stable) AND releases/tags/pre simultaneously. Returns:
current_channel ('stable' | 'pre' — detected from CHANNEL file)
target_channel (the one you're NOT on — for switch prompt)
latest_stable (latest stable tag, e.g. v1.3.0)
latest_pre (latest pre tag, e.g. pre)
latest_version (target channel's latest — for update_available)
Channel detection reads the CHANNEL file alongside the binary
(written by install.sh). Also checks version string for -rc/-pre
suffixes as fallback.
- POST /system/update: accepts { channel: 'stable' | 'pre' } in the
request body. When channel='pre', passes THREE_M_UI_CHANNEL=pre env
+ 'pre' positional arg to update.sh. When channel='stable', passes
THREE_M_UI_CHANNEL=stable. Response includes the selected channel +
a descriptive message ('Switching to pre-release channel...').
Frontend:
- Update card now shows current channel badge (cyan 'Stable' or
purple 'Pre-release') next to the version tag.
- Segmented control to switch between Stable and Pre-release channels.
Each option shows the latest version tag for that channel inline
(e.g. 'Stable v1.3.0' / 'Pre-release pre').
- Switching the Segmented recomputes latest_version +
update_available for the newly-selected target channel.
- 'Update now' button passes the selected channel to POST /system/update.
Button tooltip reflects the action: 'Switch to pre-release channel' or
'Switch to stable channel'.
- Release notes reflect the target channel's GitHub release body.
i18n: 3 new keys (stable, preRelease, channelSwitch) in all 18 locales.
zh-CN: 稳定版 / 预发布版 / 切换渠道
Pre-release URL: https://github.com/kazeyukiro/3m-ui/releases/tag/pre
Tests: 23/23 backend PASS, frontend build + lint + 16/16 unit tests PASS.
feat(update): stable ↔ pre-release channel switching from web UI
Adds the ability to switch between stable and pre-release channels
directly from the Settings → System ops → Panel update card — no SSH
required.
Backend:
- GET /system/update-info: now queries BOTH GitHub releases/latest
(stable) AND releases/tags/pre simultaneously. Returns:
current_channel ('stable' | 'pre' — detected from CHANNEL file)
target_channel (the one you're NOT on — for switch prompt)
latest_stable (latest stable tag, e.g. v1.3.0)
latest_pre (latest pre tag, e.g. pre)
latest_version (target channel's latest — for update_available)
Channel detection reads the CHANNEL file alongside the binary
(written by install.sh). Also checks version string for -rc/-pre
suffixes as fallback.
- POST /system/update: accepts { channel: 'stable' | 'pre' } in the
request body. When channel='pre', passes THREE_M_UI_CHANNEL=pre env
+ 'pre' positional arg to update.sh. When channel='stable', passes
THREE_M_UI_CHANNEL=stable. Response includes the selected channel +
a descriptive message ('Switching to pre-release channel...').
Frontend:
- Update card now shows current channel badge (cyan 'Stable' or
purple 'Pre-release') next to the version tag.
- Segmented control to switch between Stable and Pre-release channels.
Each option shows the latest version tag for that channel inline
(e.g. 'Stable v1.3.0' / 'Pre-release pre').
- Switching the Segmented recomputes latest_version +
update_available for the newly-selected target channel.
- 'Update now' button passes the selected channel to POST /system/update.
Button tooltip reflects the action: 'Switch to pre-release channel' or
'Switch to stable channel'.
- Release notes reflect the target channel's GitHub release body.
i18n: 3 new keys (stable, preRelease, channelSwitch) in all 18 locales.
zh-CN: 稳定版 / 预发布版 / 切换渠道
Pre-release URL: https://github.com/kazeyukiro/3m-ui/releases/tag/pre
Tests: 23/23 backend PASS, frontend build + lint + 16/16 unit tests PASS.
feat(update): stable ↔ pre-release channel switching from web UI
Adds the ability to switch between stable and pre-release channels
directly from the Settings → System ops → Panel update card — no SSH
required.
Backend:
- GET /system/update-info: now queries BOTH GitHub releases/latest
(stable) AND releases/tags/pre simultaneously. Returns:
current_channel ('stable' | 'pre' — detected from CHANNEL file)
target_channel (the one you're NOT on — for switch prompt)
latest_stable (latest stable tag, e.g. v1.3.0)
latest_pre (latest pre tag, e.g. pre)
latest_version (target channel's latest — for update_available)
Channel detection reads the CHANNEL file alongside the binary
(written by install.sh). Also checks version string for -rc/-pre
suffixes as fallback.
- POST /system/update: accepts { channel: 'stable' | 'pre' } in the
request body. When channel='pre', passes THREE_M_UI_CHANNEL=pre env
+ 'pre' positional arg to update.sh. When channel='stable', passes
THREE_M_UI_CHANNEL=stable. Response includes the selected channel +
a descriptive message ('Switching to pre-release channel...').
Frontend:
- Update card now shows current channel badge (cyan 'Stable' or
purple 'Pre-release') next to the version tag.
- Segmented control to switch between Stable and Pre-release channels.
Each option shows the latest version tag for that channel inline
(e.g. 'Stable v1.3.0' / 'Pre-release pre').
- Switching the Segmented recomputes latest_version +
update_available for the newly-selected target channel.
- 'Update now' button passes the selected channel to POST /system/update.
Button tooltip reflects the action: 'Switch to pre-release channel' or
'Switch to stable channel'.
- Release notes reflect the target channel's GitHub release body.
i18n: 3 new keys (stable, preRelease, channelSwitch) in all 18 locales.
zh-CN: 稳定版 / 预发布版 / 切换渠道
Pre-release URL: https://github.com/kazeyukiro/3m-ui/releases/tag/pre
Tests: 23/23 backend PASS, frontend build + lint + 16/16 unit tests PASS.
feat(update): stable ↔ pre-release channel switching from web UI
Adds the ability to switch between stable and pre-release channels
directly from the Settings → System ops → Panel update card — no SSH
required.
Backend:
- GET /system/update-info: now queries BOTH GitHub releases/latest
(stable) AND releases/tags/pre simultaneously. Returns:
current_channel ('stable' | 'pre' — detected from CHANNEL file)
target_channel (the one you're NOT on — for switch prompt)
latest_stable (latest stable tag, e.g. v1.3.0)
latest_pre (latest pre tag, e.g. pre)
latest_version (target channel's latest — for update_available)
Channel detection reads the CHANNEL file alongside the binary
(written by install.sh). Also checks version string for -rc/-pre
suffixes as fallback.
- POST /system/update: accepts { channel: 'stable' | 'pre' } in the
request body. When channel='pre', passes THREE_M_UI_CHANNEL=pre env
+ 'pre' positional arg to update.sh. When channel='stable', passes
THREE_M_UI_CHANNEL=stable. Response includes the selected channel +
a descriptive message ('Switching to pre-release channel...').
Frontend:
- Update card now shows current channel badge (cyan 'Stable' or
purple 'Pre-release') next to the version tag.
- Segmented control to switch between Stable and Pre-release channels.
Each option shows the latest version tag for that channel inline
(e.g. 'Stable v1.3.0' / 'Pre-release pre').
- Switching the Segmented recomputes latest_version +
update_available for the newly-selected target channel.
- 'Update now' button passes the selected channel to POST /system/update.
Button tooltip reflects the action: 'Switch to pre-release channel' or
'Switch to stable channel'.
- Release notes reflect the target channel's GitHub release body.
i18n: 3 new keys (stable, preRelease, channelSwitch) in all 18 locales.
zh-CN: 稳定版 / 预发布版 / 切换渠道
Pre-release URL: https://github.com/kazeyukiro/3m-ui/releases/tag/pre
Tests: 23/23 backend PASS, frontend build + lint + 16/16 unit tests PASS.
feat(update): stable ↔ pre-release channel switching from web UI
Adds the ability to switch between stable and pre-release channels
directly from the Settings → System ops → Panel update card — no SSH
required.
Backend:
- GET /system/update-info: now queries BOTH GitHub releases/latest
(stable) AND releases/tags/pre simultaneously. Returns:
current_channel ('stable' | 'pre' — detected from CHANNEL file)
target_channel (the one you're NOT on — for switch prompt)
latest_stable (latest stable tag, e.g. v1.3.0)
latest_pre (latest pre tag, e.g. pre)
latest_version (target channel's latest — for update_available)
Channel detection reads the CHANNEL file alongside the binary
(written by install.sh). Also checks version string for -rc/-pre
suffixes as fallback.
- POST /system/update: accepts { channel: 'stable' | 'pre' } in the
request body. When channel='pre', passes THREE_M_UI_CHANNEL=pre env
+ 'pre' positional arg to update.sh. When channel='stable', passes
THREE_M_UI_CHANNEL=stable. Response includes the selected channel +
a descriptive message ('Switching to pre-release channel...').
Frontend:
- Update card now shows current channel badge (cyan 'Stable' or
purple 'Pre-release') next to the version tag.
- Segmented control to switch between Stable and Pre-release channels.
Each option shows the latest version tag for that channel inline
(e.g. 'Stable v1.3.0' / 'Pre-release pre').
- Switching the Segmented recomputes latest_version +
update_available for the newly-selected target channel.
- 'Update now' button passes the selected channel to POST /system/update.
Button tooltip reflects the action: 'Switch to pre-release channel' or
'Switch to stable channel'.
- Release notes reflect the target channel's GitHub release body.
i18n: 3 new keys (stable, preRelease, channelSwitch) in all 18 locales.
zh-CN: 稳定版 / 预发布版 / 切换渠道
Pre-release URL: https://github.com/kazeyukiro/3m-ui/releases/tag/pre
Tests: 23/23 backend PASS, frontend build + lint + 16/16 unit tests PASS.
feat(update): stable ↔ pre-release channel switching from web UI
Adds the ability to switch between stable and pre-release channels
directly from the Settings → System ops → Panel update card — no SSH
required.
Backend:
- GET /system/update-info: now queries BOTH GitHub releases/latest
(stable) AND releases/tags/pre simultaneously. Returns:
current_channel ('stable' | 'pre' — detected from CHANNEL file)
target_channel (the one you're NOT on — for switch prompt)
latest_stable (latest stable tag, e.g. v1.3.0)
latest_pre (latest pre tag, e.g. pre)
latest_version (target channel's latest — for update_available)
Channel detection reads the CHANNEL file alongside the binary
(written by install.sh). Also checks version string for -rc/-pre
suffixes as fallback.
- POST /system/update: accepts { channel: 'stable' | 'pre' } in the
request body. When channel='pre', passes THREE_M_UI_CHANNEL=pre env
+ 'pre' positional arg to update.sh. When channel='stable', passes
THREE_M_UI_CHANNEL=stable. Response includes the selected channel +
a descriptive message ('Switching to pre-release channel...').
Frontend:
- Update card now shows current channel badge (cyan 'Stable' or
purple 'Pre-release') next to the version tag.
- Segmented control to switch between Stable and Pre-release channels.
Each option shows the latest version tag for that channel inline
(e.g. 'Stable v1.3.0' / 'Pre-release pre').
- Switching the Segmented recomputes latest_version +
update_available for the newly-selected target channel.
- 'Update now' button passes the selected channel to POST /system/update.
Button tooltip reflects the action: 'Switch to pre-release channel' or
'Switch to stable channel'.
- Release notes reflect the target channel's GitHub release body.
i18n: 3 new keys (stable, preRelease, channelSwitch) in all 18 locales.
zh-CN: 稳定版 / 预发布版 / 切换渠道
Pre-release URL: https://github.com/kazeyukiro/3m-ui/releases/tag/pre
Tests: 23/23 backend PASS, frontend build + lint + 16/16 unit tests PASS.
feat(update): stable ↔ pre-release channel switching from web UI
Adds the ability to switch between stable and pre-release channels
directly from the Settings → System ops → Panel update card — no SSH
required.
Backend:
- GET /system/update-info: now queries BOTH GitHub releases/latest
(stable) AND releases/tags/pre simultaneously. Returns:
current_channel ('stable' | 'pre' — detected from CHANNEL file)
target_channel (the one you're NOT on — for switch prompt)
latest_stable (latest stable tag, e.g. v1.3.0)
latest_pre (latest pre tag, e.g. pre)
latest_version (target channel's latest — for update_available)
Channel detection reads the CHANNEL file alongside the binary
(written by install.sh). Also checks version string for -rc/-pre
suffixes as fallback.
- POST /system/update: accepts { channel: 'stable' | 'pre' } in the
request body. When channel='pre', passes THREE_M_UI_CHANNEL=pre env
+ 'pre' positional arg to update.sh. When channel='stable', passes
THREE_M_UI_CHANNEL=stable. Response includes the selected channel +
a descriptive message ('Switching to pre-release channel...').
Frontend:
- Update card now shows current channel badge (cyan 'Stable' or
purple 'Pre-release') next to the version tag.
- Segmented control to switch between Stable and Pre-release channels.
Each option shows the latest version tag for that channel inline
(e.g. 'Stable v1.3.0' / 'Pre-release pre').
- Switching the Segmented recomputes latest_version +
update_available for the newly-selected target channel.
- 'Update now' button passes the selected channel to POST /system/update.
Button tooltip reflects the action: 'Switch to pre-release channel' or
'Switch to stable channel'.
- Release notes reflect the target channel's GitHub release body.
i18n: 3 new keys (stable, preRelease, channelSwitch) in all 18 locales.
zh-CN: 稳定版 / 预发布版 / 切换渠道
Pre-release URL: https://github.com/kazeyukiro/3m-ui/releases/tag/pre
Tests: 23/23 backend PASS, frontend build + lint + 16/16 unit tests PASS.
2026-09-25 13:21:19 +00:00
Z User f5b2ade27d fix(ssl): fallback to ACME cache PEM when panel uses IP cert
When 'Apply cert to nodes' is used with 'Use panel SSL files' enabled,
resolveCertificatePair tried to read st.CertFile/st.KeyFile from the
panel SSL settings. But if the panel uses ACME (not manual cert files),
those fields are empty — returning 'panel SSL has no manual
cert_file/key_file; paste PEM or set panel SSL files first'.
Fix: when CertFile/KeyFile are empty, check if the panel is using an IP
certificate (acmez IP issuer). IP certs are stored as standard PEM files
in the ACME cache dir:
<cacheDir>/ip-<sanitized-ip>-cert.pem
<cacheDir>/ip-<sanitized-ip>-key.pem
If these files exist, use them as the cert/key source. If they don't
exist (e.g. domain ACME which uses autocert.DirCache binary format, or
IP cert not yet issued), return a clearer error message that:
1. Explains the panel uses ACME (no manual cert_file)
2. For domain ACME: tells user to paste PEM or use /etc/letsencrypt paths
3. For IP certs: tells user the cache PEM should exist but doesn't
Added sanitizeIPFilename() helper in p2.go (mirrors acme/ip_issuer.go).
Tests: 23/23 packages PASS.
fix(ssl): fallback to ACME cache PEM when panel uses IP cert
When 'Apply cert to nodes' is used with 'Use panel SSL files' enabled,
resolveCertificatePair tried to read st.CertFile/st.KeyFile from the
panel SSL settings. But if the panel uses ACME (not manual cert files),
those fields are empty — returning 'panel SSL has no manual
cert_file/key_file; paste PEM or set panel SSL files first'.
Fix: when CertFile/KeyFile are empty, check if the panel is using an IP
certificate (acmez IP issuer). IP certs are stored as standard PEM files
in the ACME cache dir:
<cacheDir>/ip-<sanitized-ip>-cert.pem
<cacheDir>/ip-<sanitized-ip>-key.pem
If these files exist, use them as the cert/key source. If they don't
exist (e.g. domain ACME which uses autocert.DirCache binary format, or
IP cert not yet issued), return a clearer error message that:
1. Explains the panel uses ACME (no manual cert_file)
2. For domain ACME: tells user to paste PEM or use /etc/letsencrypt paths
3. For IP certs: tells user the cache PEM should exist but doesn't
Added sanitizeIPFilename() helper in p2.go (mirrors acme/ip_issuer.go).
Tests: 23/23 packages PASS.
fix(ssl): fallback to ACME cache PEM when panel uses IP cert
When 'Apply cert to nodes' is used with 'Use panel SSL files' enabled,
resolveCertificatePair tried to read st.CertFile/st.KeyFile from the
panel SSL settings. But if the panel uses ACME (not manual cert files),
those fields are empty — returning 'panel SSL has no manual
cert_file/key_file; paste PEM or set panel SSL files first'.
Fix: when CertFile/KeyFile are empty, check if the panel is using an IP
certificate (acmez IP issuer). IP certs are stored as standard PEM files
in the ACME cache dir:
<cacheDir>/ip-<sanitized-ip>-cert.pem
<cacheDir>/ip-<sanitized-ip>-key.pem
If these files exist, use them as the cert/key source. If they don't
exist (e.g. domain ACME which uses autocert.DirCache binary format, or
IP cert not yet issued), return a clearer error message that:
1. Explains the panel uses ACME (no manual cert_file)
2. For domain ACME: tells user to paste PEM or use /etc/letsencrypt paths
3. For IP certs: tells user the cache PEM should exist but doesn't
Added sanitizeIPFilename() helper in p2.go (mirrors acme/ip_issuer.go).
Tests: 23/23 packages PASS.
fix(ssl): fallback to ACME cache PEM when panel uses IP cert
When 'Apply cert to nodes' is used with 'Use panel SSL files' enabled,
resolveCertificatePair tried to read st.CertFile/st.KeyFile from the
panel SSL settings. But if the panel uses ACME (not manual cert files),
those fields are empty — returning 'panel SSL has no manual
cert_file/key_file; paste PEM or set panel SSL files first'.
Fix: when CertFile/KeyFile are empty, check if the panel is using an IP
certificate (acmez IP issuer). IP certs are stored as standard PEM files
in the ACME cache dir:
<cacheDir>/ip-<sanitized-ip>-cert.pem
<cacheDir>/ip-<sanitized-ip>-key.pem
If these files exist, use them as the cert/key source. If they don't
exist (e.g. domain ACME which uses autocert.DirCache binary format, or
IP cert not yet issued), return a clearer error message that:
1. Explains the panel uses ACME (no manual cert_file)
2. For domain ACME: tells user to paste PEM or use /etc/letsencrypt paths
3. For IP certs: tells user the cache PEM should exist but doesn't
Added sanitizeIPFilename() helper in p2.go (mirrors acme/ip_issuer.go).
Tests: 23/23 packages PASS.
fix(ssl): fallback to ACME cache PEM when panel uses IP cert
When 'Apply cert to nodes' is used with 'Use panel SSL files' enabled,
resolveCertificatePair tried to read st.CertFile/st.KeyFile from the
panel SSL settings. But if the panel uses ACME (not manual cert files),
those fields are empty — returning 'panel SSL has no manual
cert_file/key_file; paste PEM or set panel SSL files first'.
Fix: when CertFile/KeyFile are empty, check if the panel is using an IP
certificate (acmez IP issuer). IP certs are stored as standard PEM files
in the ACME cache dir:
<cacheDir>/ip-<sanitized-ip>-cert.pem
<cacheDir>/ip-<sanitized-ip>-key.pem
If these files exist, use them as the cert/key source. If they don't
exist (e.g. domain ACME which uses autocert.DirCache binary format, or
IP cert not yet issued), return a clearer error message that:
1. Explains the panel uses ACME (no manual cert_file)
2. For domain ACME: tells user to paste PEM or use /etc/letsencrypt paths
3. For IP certs: tells user the cache PEM should exist but doesn't
Added sanitizeIPFilename() helper in p2.go (mirrors acme/ip_issuer.go).
Tests: 23/23 packages PASS.
fix(ssl): fallback to ACME cache PEM when panel uses IP cert
When 'Apply cert to nodes' is used with 'Use panel SSL files' enabled,
resolveCertificatePair tried to read st.CertFile/st.KeyFile from the
panel SSL settings. But if the panel uses ACME (not manual cert files),
those fields are empty — returning 'panel SSL has no manual
cert_file/key_file; paste PEM or set panel SSL files first'.
Fix: when CertFile/KeyFile are empty, check if the panel is using an IP
certificate (acmez IP issuer). IP certs are stored as standard PEM files
in the ACME cache dir:
<cacheDir>/ip-<sanitized-ip>-cert.pem
<cacheDir>/ip-<sanitized-ip>-key.pem
If these files exist, use them as the cert/key source. If they don't
exist (e.g. domain ACME which uses autocert.DirCache binary format, or
IP cert not yet issued), return a clearer error message that:
1. Explains the panel uses ACME (no manual cert_file)
2. For domain ACME: tells user to paste PEM or use /etc/letsencrypt paths
3. For IP certs: tells user the cache PEM should exist but doesn't
Added sanitizeIPFilename() helper in p2.go (mirrors acme/ip_issuer.go).
Tests: 23/23 packages PASS.
fix(ssl): fallback to ACME cache PEM when panel uses IP cert
When 'Apply cert to nodes' is used with 'Use panel SSL files' enabled,
resolveCertificatePair tried to read st.CertFile/st.KeyFile from the
panel SSL settings. But if the panel uses ACME (not manual cert files),
those fields are empty — returning 'panel SSL has no manual
cert_file/key_file; paste PEM or set panel SSL files first'.
Fix: when CertFile/KeyFile are empty, check if the panel is using an IP
certificate (acmez IP issuer). IP certs are stored as standard PEM files
in the ACME cache dir:
<cacheDir>/ip-<sanitized-ip>-cert.pem
<cacheDir>/ip-<sanitized-ip>-key.pem
If these files exist, use them as the cert/key source. If they don't
exist (e.g. domain ACME which uses autocert.DirCache binary format, or
IP cert not yet issued), return a clearer error message that:
1. Explains the panel uses ACME (no manual cert_file)
2. For domain ACME: tells user to paste PEM or use /etc/letsencrypt paths
3. For IP certs: tells user the cache PEM should exist but doesn't
Added sanitizeIPFilename() helper in p2.go (mirrors acme/ip_issuer.go).
Tests: 23/23 packages PASS.
fix(ssl): fallback to ACME cache PEM when panel uses IP cert
When 'Apply cert to nodes' is used with 'Use panel SSL files' enabled,
resolveCertificatePair tried to read st.CertFile/st.KeyFile from the
panel SSL settings. But if the panel uses ACME (not manual cert files),
those fields are empty — returning 'panel SSL has no manual
cert_file/key_file; paste PEM or set panel SSL files first'.
Fix: when CertFile/KeyFile are empty, check if the panel is using an IP
certificate (acmez IP issuer). IP certs are stored as standard PEM files
in the ACME cache dir:
<cacheDir>/ip-<sanitized-ip>-cert.pem
<cacheDir>/ip-<sanitized-ip>-key.pem
If these files exist, use them as the cert/key source. If they don't
exist (e.g. domain ACME which uses autocert.DirCache binary format, or
IP cert not yet issued), return a clearer error message that:
1. Explains the panel uses ACME (no manual cert_file)
2. For domain ACME: tells user to paste PEM or use /etc/letsencrypt paths
3. For IP certs: tells user the cache PEM should exist but doesn't
Added sanitizeIPFilename() helper in p2.go (mirrors acme/ip_issuer.go).
Tests: 23/23 packages PASS.
fix(ssl): fallback to ACME cache PEM when panel uses IP cert
When 'Apply cert to nodes' is used with 'Use panel SSL files' enabled,
resolveCertificatePair tried to read st.CertFile/st.KeyFile from the
panel SSL settings. But if the panel uses ACME (not manual cert files),
those fields are empty — returning 'panel SSL has no manual
cert_file/key_file; paste PEM or set panel SSL files first'.
Fix: when CertFile/KeyFile are empty, check if the panel is using an IP
certificate (acmez IP issuer). IP certs are stored as standard PEM files
in the ACME cache dir:
<cacheDir>/ip-<sanitized-ip>-cert.pem
<cacheDir>/ip-<sanitized-ip>-key.pem
If these files exist, use them as the cert/key source. If they don't
exist (e.g. domain ACME which uses autocert.DirCache binary format, or
IP cert not yet issued), return a clearer error message that:
1. Explains the panel uses ACME (no manual cert_file)
2. For domain ACME: tells user to paste PEM or use /etc/letsencrypt paths
3. For IP certs: tells user the cache PEM should exist but doesn't
Added sanitizeIPFilename() helper in p2.go (mirrors acme/ip_issuer.go).
Tests: 23/23 packages PASS.
fix(ssl): fallback to ACME cache PEM when panel uses IP cert
When 'Apply cert to nodes' is used with 'Use panel SSL files' enabled,
resolveCertificatePair tried to read st.CertFile/st.KeyFile from the
panel SSL settings. But if the panel uses ACME (not manual cert files),
those fields are empty — returning 'panel SSL has no manual
cert_file/key_file; paste PEM or set panel SSL files first'.
Fix: when CertFile/KeyFile are empty, check if the panel is using an IP
certificate (acmez IP issuer). IP certs are stored as standard PEM files
in the ACME cache dir:
<cacheDir>/ip-<sanitized-ip>-cert.pem
<cacheDir>/ip-<sanitized-ip>-key.pem
If these files exist, use them as the cert/key source. If they don't
exist (e.g. domain ACME which uses autocert.DirCache binary format, or
IP cert not yet issued), return a clearer error message that:
1. Explains the panel uses ACME (no manual cert_file)
2. For domain ACME: tells user to paste PEM or use /etc/letsencrypt paths
3. For IP certs: tells user the cache PEM should exist but doesn't
Added sanitizeIPFilename() helper in p2.go (mirrors acme/ip_issuer.go).
Tests: 23/23 packages PASS.
fix(ssl): fallback to ACME cache PEM when panel uses IP cert
When 'Apply cert to nodes' is used with 'Use panel SSL files' enabled,
resolveCertificatePair tried to read st.CertFile/st.KeyFile from the
panel SSL settings. But if the panel uses ACME (not manual cert files),
those fields are empty — returning 'panel SSL has no manual
cert_file/key_file; paste PEM or set panel SSL files first'.
Fix: when CertFile/KeyFile are empty, check if the panel is using an IP
certificate (acmez IP issuer). IP certs are stored as standard PEM files
in the ACME cache dir:
<cacheDir>/ip-<sanitized-ip>-cert.pem
<cacheDir>/ip-<sanitized-ip>-key.pem
If these files exist, use them as the cert/key source. If they don't
exist (e.g. domain ACME which uses autocert.DirCache binary format, or
IP cert not yet issued), return a clearer error message that:
1. Explains the panel uses ACME (no manual cert_file)
2. For domain ACME: tells user to paste PEM or use /etc/letsencrypt paths
3. For IP certs: tells user the cache PEM should exist but doesn't
Added sanitizeIPFilename() helper in p2.go (mirrors acme/ip_issuer.go).
Tests: 23/23 packages PASS.
fix(ssl): fallback to ACME cache PEM when panel uses IP cert
When 'Apply cert to nodes' is used with 'Use panel SSL files' enabled,
resolveCertificatePair tried to read st.CertFile/st.KeyFile from the
panel SSL settings. But if the panel uses ACME (not manual cert files),
those fields are empty — returning 'panel SSL has no manual
cert_file/key_file; paste PEM or set panel SSL files first'.
Fix: when CertFile/KeyFile are empty, check if the panel is using an IP
certificate (acmez IP issuer). IP certs are stored as standard PEM files
in the ACME cache dir:
<cacheDir>/ip-<sanitized-ip>-cert.pem
<cacheDir>/ip-<sanitized-ip>-key.pem
If these files exist, use them as the cert/key source. If they don't
exist (e.g. domain ACME which uses autocert.DirCache binary format, or
IP cert not yet issued), return a clearer error message that:
1. Explains the panel uses ACME (no manual cert_file)
2. For domain ACME: tells user to paste PEM or use /etc/letsencrypt paths
3. For IP certs: tells user the cache PEM should exist but doesn't
Added sanitizeIPFilename() helper in p2.go (mirrors acme/ip_issuer.go).
Tests: 23/23 packages PASS.
2026-09-25 12:14:48 +00:00
Z User 77b16aaef2 docs(api): add /system/restart, /system/update-info, /system/update to OpenAPI
Documents the 3 new system endpoints added in commit 496b536:
- POST /system/restart: os.Exit(0) → systemd restart
- GET /system/update-info: GitHub latest release check
- POST /system/update: triggers update.sh in background
Each endpoint includes security (BearerAuth), request/response schemas,
content-type, and example responses. Added to both:
- backend/internal/docs/openapi.yaml (embedded copy)
- backend/docs/openapi.yaml (public copy)
docs(api): add /system/restart, /system/update-info, /system/update to OpenAPI
Documents the 3 new system endpoints added in commit 496b536:
- POST /system/restart: os.Exit(0) → systemd restart
- GET /system/update-info: GitHub latest release check
- POST /system/update: triggers update.sh in background
Each endpoint includes security (BearerAuth), request/response schemas,
content-type, and example responses. Added to both:
- backend/internal/docs/openapi.yaml (embedded copy)
- backend/docs/openapi.yaml (public copy)
docs(api): add /system/restart, /system/update-info, /system/update to OpenAPI
Documents the 3 new system endpoints added in commit 496b536:
- POST /system/restart: os.Exit(0) → systemd restart
- GET /system/update-info: GitHub latest release check
- POST /system/update: triggers update.sh in background
Each endpoint includes security (BearerAuth), request/response schemas,
content-type, and example responses. Added to both:
- backend/internal/docs/openapi.yaml (embedded copy)
- backend/docs/openapi.yaml (public copy)
docs(api): add /system/restart, /system/update-info, /system/update to OpenAPI
Documents the 3 new system endpoints added in commit 496b536:
- POST /system/restart: os.Exit(0) → systemd restart
- GET /system/update-info: GitHub latest release check
- POST /system/update: triggers update.sh in background
Each endpoint includes security (BearerAuth), request/response schemas,
content-type, and example responses. Added to both:
- backend/internal/docs/openapi.yaml (embedded copy)
- backend/docs/openapi.yaml (public copy)
docs(api): add /system/restart, /system/update-info, /system/update to OpenAPI
Documents the 3 new system endpoints added in commit 496b536:
- POST /system/restart: os.Exit(0) → systemd restart
- GET /system/update-info: GitHub latest release check
- POST /system/update: triggers update.sh in background
Each endpoint includes security (BearerAuth), request/response schemas,
content-type, and example responses. Added to both:
- backend/internal/docs/openapi.yaml (embedded copy)
- backend/docs/openapi.yaml (public copy)
docs(api): add /system/restart, /system/update-info, /system/update to OpenAPI
Documents the 3 new system endpoints added in commit 496b536:
- POST /system/restart: os.Exit(0) → systemd restart
- GET /system/update-info: GitHub latest release check
- POST /system/update: triggers update.sh in background
Each endpoint includes security (BearerAuth), request/response schemas,
content-type, and example responses. Added to both:
- backend/internal/docs/openapi.yaml (embedded copy)
- backend/docs/openapi.yaml (public copy)
docs(api): add /system/restart, /system/update-info, /system/update to OpenAPI
Documents the 3 new system endpoints added in commit 496b536:
- POST /system/restart: os.Exit(0) → systemd restart
- GET /system/update-info: GitHub latest release check
- POST /system/update: triggers update.sh in background
Each endpoint includes security (BearerAuth), request/response schemas,
content-type, and example responses. Added to both:
- backend/internal/docs/openapi.yaml (embedded copy)
- backend/docs/openapi.yaml (public copy)
docs(api): add /system/restart, /system/update-info, /system/update to OpenAPI
Documents the 3 new system endpoints added in commit 496b536:
- POST /system/restart: os.Exit(0) → systemd restart
- GET /system/update-info: GitHub latest release check
- POST /system/update: triggers update.sh in background
Each endpoint includes security (BearerAuth), request/response schemas,
content-type, and example responses. Added to both:
- backend/internal/docs/openapi.yaml (embedded copy)
- backend/docs/openapi.yaml (public copy)
docs(api): add /system/restart, /system/update-info, /system/update to OpenAPI
Documents the 3 new system endpoints added in commit 496b536:
- POST /system/restart: os.Exit(0) → systemd restart
- GET /system/update-info: GitHub latest release check
- POST /system/update: triggers update.sh in background
Each endpoint includes security (BearerAuth), request/response schemas,
content-type, and example responses. Added to both:
- backend/internal/docs/openapi.yaml (embedded copy)
- backend/docs/openapi.yaml (public copy)
docs(api): add /system/restart, /system/update-info, /system/update to OpenAPI
Documents the 3 new system endpoints added in commit 496b536:
- POST /system/restart: os.Exit(0) → systemd restart
- GET /system/update-info: GitHub latest release check
- POST /system/update: triggers update.sh in background
Each endpoint includes security (BearerAuth), request/response schemas,
content-type, and example responses. Added to both:
- backend/internal/docs/openapi.yaml (embedded copy)
- backend/docs/openapi.yaml (public copy)
docs(api): add /system/restart, /system/update-info, /system/update to OpenAPI
Documents the 3 new system endpoints added in commit 496b536:
- POST /system/restart: os.Exit(0) → systemd restart
- GET /system/update-info: GitHub latest release check
- POST /system/update: triggers update.sh in background
Each endpoint includes security (BearerAuth), request/response schemas,
content-type, and example responses. Added to both:
- backend/internal/docs/openapi.yaml (embedded copy)
- backend/docs/openapi.yaml (public copy)
docs(api): add /system/restart, /system/update-info, /system/update to OpenAPI
Documents the 3 new system endpoints added in commit 496b536:
- POST /system/restart: os.Exit(0) → systemd restart
- GET /system/update-info: GitHub latest release check
- POST /system/update: triggers update.sh in background
Each endpoint includes security (BearerAuth), request/response schemas,
content-type, and example responses. Added to both:
- backend/internal/docs/openapi.yaml (embedded copy)
- backend/docs/openapi.yaml (public copy)
2026-09-25 12:09:27 +00:00
Z User 6e0c498423 feat(settings): add restart + update panel + field hints
Backend:
- POST /system/restart: os.Exit(0) so systemd Restart=always reboots
the panel. Same pattern as RestoreDatabase.
- GET /system/update-info: queries GitHub releases API for latest
version, compares to buildinfo.Version, returns update_available flag.
- POST /system/update: triggers update.sh in background. Panel restarts
automatically when update completes.
Frontend:
- New 'System ops' section in Settings sidebar (between 'Traffic reset'
and 'About').
- SystemOpsCard component with 2 cards:
1. Restart panel: Modal.confirm → POST /system/restart → poll
/api/v1/health → auto-reload page. Button is danger-styled.
2. Panel update: auto-checks latest version on mount. Shows current vs
latest tags + 'Update available' / 'Up to date' badge. Collapsible
release notes. 'Check for updates' + 'Update now' buttons. Update
polls health with 120s timeout (update takes ~60s).
- 12 Form.Item fields across access/telegram/subscription sections now
have tooltip hints (public_host, public_port, sni, client_fingerprint,
bot_token, cpu_warn_pct, traffic_warn_pct, expiry_warn_hours,
proxy_url, api_server, sub_title, sub_support_url).
i18n:
- 23 new keys (navOps, restartPanel, restartPanelHint, restartNow,
restartConfirmTitle, restartConfirmHint, restarting, panelUpdate,
updateHint, checkUpdate, updateNow, updateConfirmTitle,
updateConfirmHint, updating, updateTimeout, updateAvailable,
upToDate, upToDateHint, updateCheckFailed, currentVersion,
latestVersion, releaseNotes, unknown) added to all 18 locales.
- 12 field hint keys (publicHostHint, publicPortHint, accessSniHint,
clientFingerprintHint, botTokenHint, cpuWarnPctHint, trafficWarnPctHint,
expiryWarnHoursHint, tgProxyHint, tgApiServerHint, subTitleHint,
subSupportUrlHint) added to all 18 locales.
Icons:
- IconSettingsOps (Wrench from lucide-react) for sidebar nav.
- IconUpdate (CloudDownload from lucide-react) for update button.
Responsive + theme:
- Uses antd theme.useToken() tokens for all colors (text.secondary, etc).
- Mobile: buttons are full-width (block={isMobile}).
- Dark mode: all text uses type='secondary' or token-based styles.
- Release notes pre uses var(--ant-color-fill-quaternary) background.
Tests: backend 23/23 PASS, frontend build PASS, lint 0 errors,
16/16 unit tests PASS.
feat(settings): add restart + update panel + field hints
Backend:
- POST /system/restart: os.Exit(0) so systemd Restart=always reboots
the panel. Same pattern as RestoreDatabase.
- GET /system/update-info: queries GitHub releases API for latest
version, compares to buildinfo.Version, returns update_available flag.
- POST /system/update: triggers update.sh in background. Panel restarts
automatically when update completes.
Frontend:
- New 'System ops' section in Settings sidebar (between 'Traffic reset'
and 'About').
- SystemOpsCard component with 2 cards:
1. Restart panel: Modal.confirm → POST /system/restart → poll
/api/v1/health → auto-reload page. Button is danger-styled.
2. Panel update: auto-checks latest version on mount. Shows current vs
latest tags + 'Update available' / 'Up to date' badge. Collapsible
release notes. 'Check for updates' + 'Update now' buttons. Update
polls health with 120s timeout (update takes ~60s).
- 12 Form.Item fields across access/telegram/subscription sections now
have tooltip hints (public_host, public_port, sni, client_fingerprint,
bot_token, cpu_warn_pct, traffic_warn_pct, expiry_warn_hours,
proxy_url, api_server, sub_title, sub_support_url).
i18n:
- 23 new keys (navOps, restartPanel, restartPanelHint, restartNow,
restartConfirmTitle, restartConfirmHint, restarting, panelUpdate,
updateHint, checkUpdate, updateNow, updateConfirmTitle,
updateConfirmHint, updating, updateTimeout, updateAvailable,
upToDate, upToDateHint, updateCheckFailed, currentVersion,
latestVersion, releaseNotes, unknown) added to all 18 locales.
- 12 field hint keys (publicHostHint, publicPortHint, accessSniHint,
clientFingerprintHint, botTokenHint, cpuWarnPctHint, trafficWarnPctHint,
expiryWarnHoursHint, tgProxyHint, tgApiServerHint, subTitleHint,
subSupportUrlHint) added to all 18 locales.
Icons:
- IconSettingsOps (Wrench from lucide-react) for sidebar nav.
- IconUpdate (CloudDownload from lucide-react) for update button.
Responsive + theme:
- Uses antd theme.useToken() tokens for all colors (text.secondary, etc).
- Mobile: buttons are full-width (block={isMobile}).
- Dark mode: all text uses type='secondary' or token-based styles.
- Release notes pre uses var(--ant-color-fill-quaternary) background.
Tests: backend 23/23 PASS, frontend build PASS, lint 0 errors,
16/16 unit tests PASS.
feat(settings): add restart + update panel + field hints
Backend:
- POST /system/restart: os.Exit(0) so systemd Restart=always reboots
the panel. Same pattern as RestoreDatabase.
- GET /system/update-info: queries GitHub releases API for latest
version, compares to buildinfo.Version, returns update_available flag.
- POST /system/update: triggers update.sh in background. Panel restarts
automatically when update completes.
Frontend:
- New 'System ops' section in Settings sidebar (between 'Traffic reset'
and 'About').
- SystemOpsCard component with 2 cards:
1. Restart panel: Modal.confirm → POST /system/restart → poll
/api/v1/health → auto-reload page. Button is danger-styled.
2. Panel update: auto-checks latest version on mount. Shows current vs
latest tags + 'Update available' / 'Up to date' badge. Collapsible
release notes. 'Check for updates' + 'Update now' buttons. Update
polls health with 120s timeout (update takes ~60s).
- 12 Form.Item fields across access/telegram/subscription sections now
have tooltip hints (public_host, public_port, sni, client_fingerprint,
bot_token, cpu_warn_pct, traffic_warn_pct, expiry_warn_hours,
proxy_url, api_server, sub_title, sub_support_url).
i18n:
- 23 new keys (navOps, restartPanel, restartPanelHint, restartNow,
restartConfirmTitle, restartConfirmHint, restarting, panelUpdate,
updateHint, checkUpdate, updateNow, updateConfirmTitle,
updateConfirmHint, updating, updateTimeout, updateAvailable,
upToDate, upToDateHint, updateCheckFailed, currentVersion,
latestVersion, releaseNotes, unknown) added to all 18 locales.
- 12 field hint keys (publicHostHint, publicPortHint, accessSniHint,
clientFingerprintHint, botTokenHint, cpuWarnPctHint, trafficWarnPctHint,
expiryWarnHoursHint, tgProxyHint, tgApiServerHint, subTitleHint,
subSupportUrlHint) added to all 18 locales.
Icons:
- IconSettingsOps (Wrench from lucide-react) for sidebar nav.
- IconUpdate (CloudDownload from lucide-react) for update button.
Responsive + theme:
- Uses antd theme.useToken() tokens for all colors (text.secondary, etc).
- Mobile: buttons are full-width (block={isMobile}).
- Dark mode: all text uses type='secondary' or token-based styles.
- Release notes pre uses var(--ant-color-fill-quaternary) background.
Tests: backend 23/23 PASS, frontend build PASS, lint 0 errors,
16/16 unit tests PASS.
feat(settings): add restart + update panel + field hints
Backend:
- POST /system/restart: os.Exit(0) so systemd Restart=always reboots
the panel. Same pattern as RestoreDatabase.
- GET /system/update-info: queries GitHub releases API for latest
version, compares to buildinfo.Version, returns update_available flag.
- POST /system/update: triggers update.sh in background. Panel restarts
automatically when update completes.
Frontend:
- New 'System ops' section in Settings sidebar (between 'Traffic reset'
and 'About').
- SystemOpsCard component with 2 cards:
1. Restart panel: Modal.confirm → POST /system/restart → poll
/api/v1/health → auto-reload page. Button is danger-styled.
2. Panel update: auto-checks latest version on mount. Shows current vs
latest tags + 'Update available' / 'Up to date' badge. Collapsible
release notes. 'Check for updates' + 'Update now' buttons. Update
polls health with 120s timeout (update takes ~60s).
- 12 Form.Item fields across access/telegram/subscription sections now
have tooltip hints (public_host, public_port, sni, client_fingerprint,
bot_token, cpu_warn_pct, traffic_warn_pct, expiry_warn_hours,
proxy_url, api_server, sub_title, sub_support_url).
i18n:
- 23 new keys (navOps, restartPanel, restartPanelHint, restartNow,
restartConfirmTitle, restartConfirmHint, restarting, panelUpdate,
updateHint, checkUpdate, updateNow, updateConfirmTitle,
updateConfirmHint, updating, updateTimeout, updateAvailable,
upToDate, upToDateHint, updateCheckFailed, currentVersion,
latestVersion, releaseNotes, unknown) added to all 18 locales.
- 12 field hint keys (publicHostHint, publicPortHint, accessSniHint,
clientFingerprintHint, botTokenHint, cpuWarnPctHint, trafficWarnPctHint,
expiryWarnHoursHint, tgProxyHint, tgApiServerHint, subTitleHint,
subSupportUrlHint) added to all 18 locales.
Icons:
- IconSettingsOps (Wrench from lucide-react) for sidebar nav.
- IconUpdate (CloudDownload from lucide-react) for update button.
Responsive + theme:
- Uses antd theme.useToken() tokens for all colors (text.secondary, etc).
- Mobile: buttons are full-width (block={isMobile}).
- Dark mode: all text uses type='secondary' or token-based styles.
- Release notes pre uses var(--ant-color-fill-quaternary) background.
Tests: backend 23/23 PASS, frontend build PASS, lint 0 errors,
16/16 unit tests PASS.
feat(settings): add restart + update panel + field hints
Backend:
- POST /system/restart: os.Exit(0) so systemd Restart=always reboots
the panel. Same pattern as RestoreDatabase.
- GET /system/update-info: queries GitHub releases API for latest
version, compares to buildinfo.Version, returns update_available flag.
- POST /system/update: triggers update.sh in background. Panel restarts
automatically when update completes.
Frontend:
- New 'System ops' section in Settings sidebar (between 'Traffic reset'
and 'About').
- SystemOpsCard component with 2 cards:
1. Restart panel: Modal.confirm → POST /system/restart → poll
/api/v1/health → auto-reload page. Button is danger-styled.
2. Panel update: auto-checks latest version on mount. Shows current vs
latest tags + 'Update available' / 'Up to date' badge. Collapsible
release notes. 'Check for updates' + 'Update now' buttons. Update
polls health with 120s timeout (update takes ~60s).
- 12 Form.Item fields across access/telegram/subscription sections now
have tooltip hints (public_host, public_port, sni, client_fingerprint,
bot_token, cpu_warn_pct, traffic_warn_pct, expiry_warn_hours,
proxy_url, api_server, sub_title, sub_support_url).
i18n:
- 23 new keys (navOps, restartPanel, restartPanelHint, restartNow,
restartConfirmTitle, restartConfirmHint, restarting, panelUpdate,
updateHint, checkUpdate, updateNow, updateConfirmTitle,
updateConfirmHint, updating, updateTimeout, updateAvailable,
upToDate, upToDateHint, updateCheckFailed, currentVersion,
latestVersion, releaseNotes, unknown) added to all 18 locales.
- 12 field hint keys (publicHostHint, publicPortHint, accessSniHint,
clientFingerprintHint, botTokenHint, cpuWarnPctHint, trafficWarnPctHint,
expiryWarnHoursHint, tgProxyHint, tgApiServerHint, subTitleHint,
subSupportUrlHint) added to all 18 locales.
Icons:
- IconSettingsOps (Wrench from lucide-react) for sidebar nav.
- IconUpdate (CloudDownload from lucide-react) for update button.
Responsive + theme:
- Uses antd theme.useToken() tokens for all colors (text.secondary, etc).
- Mobile: buttons are full-width (block={isMobile}).
- Dark mode: all text uses type='secondary' or token-based styles.
- Release notes pre uses var(--ant-color-fill-quaternary) background.
Tests: backend 23/23 PASS, frontend build PASS, lint 0 errors,
16/16 unit tests PASS.
feat(settings): add restart + update panel + field hints
Backend:
- POST /system/restart: os.Exit(0) so systemd Restart=always reboots
the panel. Same pattern as RestoreDatabase.
- GET /system/update-info: queries GitHub releases API for latest
version, compares to buildinfo.Version, returns update_available flag.
- POST /system/update: triggers update.sh in background. Panel restarts
automatically when update completes.
Frontend:
- New 'System ops' section in Settings sidebar (between 'Traffic reset'
and 'About').
- SystemOpsCard component with 2 cards:
1. Restart panel: Modal.confirm → POST /system/restart → poll
/api/v1/health → auto-reload page. Button is danger-styled.
2. Panel update: auto-checks latest version on mount. Shows current vs
latest tags + 'Update available' / 'Up to date' badge. Collapsible
release notes. 'Check for updates' + 'Update now' buttons. Update
polls health with 120s timeout (update takes ~60s).
- 12 Form.Item fields across access/telegram/subscription sections now
have tooltip hints (public_host, public_port, sni, client_fingerprint,
bot_token, cpu_warn_pct, traffic_warn_pct, expiry_warn_hours,
proxy_url, api_server, sub_title, sub_support_url).
i18n:
- 23 new keys (navOps, restartPanel, restartPanelHint, restartNow,
restartConfirmTitle, restartConfirmHint, restarting, panelUpdate,
updateHint, checkUpdate, updateNow, updateConfirmTitle,
updateConfirmHint, updating, updateTimeout, updateAvailable,
upToDate, upToDateHint, updateCheckFailed, currentVersion,
latestVersion, releaseNotes, unknown) added to all 18 locales.
- 12 field hint keys (publicHostHint, publicPortHint, accessSniHint,
clientFingerprintHint, botTokenHint, cpuWarnPctHint, trafficWarnPctHint,
expiryWarnHoursHint, tgProxyHint, tgApiServerHint, subTitleHint,
subSupportUrlHint) added to all 18 locales.
Icons:
- IconSettingsOps (Wrench from lucide-react) for sidebar nav.
- IconUpdate (CloudDownload from lucide-react) for update button.
Responsive + theme:
- Uses antd theme.useToken() tokens for all colors (text.secondary, etc).
- Mobile: buttons are full-width (block={isMobile}).
- Dark mode: all text uses type='secondary' or token-based styles.
- Release notes pre uses var(--ant-color-fill-quaternary) background.
Tests: backend 23/23 PASS, frontend build PASS, lint 0 errors,
16/16 unit tests PASS.
feat(settings): add restart + update panel + field hints
Backend:
- POST /system/restart: os.Exit(0) so systemd Restart=always reboots
the panel. Same pattern as RestoreDatabase.
- GET /system/update-info: queries GitHub releases API for latest
version, compares to buildinfo.Version, returns update_available flag.
- POST /system/update: triggers update.sh in background. Panel restarts
automatically when update completes.
Frontend:
- New 'System ops' section in Settings sidebar (between 'Traffic reset'
and 'About').
- SystemOpsCard component with 2 cards:
1. Restart panel: Modal.confirm → POST /system/restart → poll
/api/v1/health → auto-reload page. Button is danger-styled.
2. Panel update: auto-checks latest version on mount. Shows current vs
latest tags + 'Update available' / 'Up to date' badge. Collapsible
release notes. 'Check for updates' + 'Update now' buttons. Update
polls health with 120s timeout (update takes ~60s).
- 12 Form.Item fields across access/telegram/subscription sections now
have tooltip hints (public_host, public_port, sni, client_fingerprint,
bot_token, cpu_warn_pct, traffic_warn_pct, expiry_warn_hours,
proxy_url, api_server, sub_title, sub_support_url).
i18n:
- 23 new keys (navOps, restartPanel, restartPanelHint, restartNow,
restartConfirmTitle, restartConfirmHint, restarting, panelUpdate,
updateHint, checkUpdate, updateNow, updateConfirmTitle,
updateConfirmHint, updating, updateTimeout, updateAvailable,
upToDate, upToDateHint, updateCheckFailed, currentVersion,
latestVersion, releaseNotes, unknown) added to all 18 locales.
- 12 field hint keys (publicHostHint, publicPortHint, accessSniHint,
clientFingerprintHint, botTokenHint, cpuWarnPctHint, trafficWarnPctHint,
expiryWarnHoursHint, tgProxyHint, tgApiServerHint, subTitleHint,
subSupportUrlHint) added to all 18 locales.
Icons:
- IconSettingsOps (Wrench from lucide-react) for sidebar nav.
- IconUpdate (CloudDownload from lucide-react) for update button.
Responsive + theme:
- Uses antd theme.useToken() tokens for all colors (text.secondary, etc).
- Mobile: buttons are full-width (block={isMobile}).
- Dark mode: all text uses type='secondary' or token-based styles.
- Release notes pre uses var(--ant-color-fill-quaternary) background.
Tests: backend 23/23 PASS, frontend build PASS, lint 0 errors,
16/16 unit tests PASS.
feat(settings): add restart + update panel + field hints
Backend:
- POST /system/restart: os.Exit(0) so systemd Restart=always reboots
the panel. Same pattern as RestoreDatabase.
- GET /system/update-info: queries GitHub releases API for latest
version, compares to buildinfo.Version, returns update_available flag.
- POST /system/update: triggers update.sh in background. Panel restarts
automatically when update completes.
Frontend:
- New 'System ops' section in Settings sidebar (between 'Traffic reset'
and 'About').
- SystemOpsCard component with 2 cards:
1. Restart panel: Modal.confirm → POST /system/restart → poll
/api/v1/health → auto-reload page. Button is danger-styled.
2. Panel update: auto-checks latest version on mount. Shows current vs
latest tags + 'Update available' / 'Up to date' badge. Collapsible
release notes. 'Check for updates' + 'Update now' buttons. Update
polls health with 120s timeout (update takes ~60s).
- 12 Form.Item fields across access/telegram/subscription sections now
have tooltip hints (public_host, public_port, sni, client_fingerprint,
bot_token, cpu_warn_pct, traffic_warn_pct, expiry_warn_hours,
proxy_url, api_server, sub_title, sub_support_url).
i18n:
- 23 new keys (navOps, restartPanel, restartPanelHint, restartNow,
restartConfirmTitle, restartConfirmHint, restarting, panelUpdate,
updateHint, checkUpdate, updateNow, updateConfirmTitle,
updateConfirmHint, updating, updateTimeout, updateAvailable,
upToDate, upToDateHint, updateCheckFailed, currentVersion,
latestVersion, releaseNotes, unknown) added to all 18 locales.
- 12 field hint keys (publicHostHint, publicPortHint, accessSniHint,
clientFingerprintHint, botTokenHint, cpuWarnPctHint, trafficWarnPctHint,
expiryWarnHoursHint, tgProxyHint, tgApiServerHint, subTitleHint,
subSupportUrlHint) added to all 18 locales.
Icons:
- IconSettingsOps (Wrench from lucide-react) for sidebar nav.
- IconUpdate (CloudDownload from lucide-react) for update button.
Responsive + theme:
- Uses antd theme.useToken() tokens for all colors (text.secondary, etc).
- Mobile: buttons are full-width (block={isMobile}).
- Dark mode: all text uses type='secondary' or token-based styles.
- Release notes pre uses var(--ant-color-fill-quaternary) background.
Tests: backend 23/23 PASS, frontend build PASS, lint 0 errors,
16/16 unit tests PASS.
feat(settings): add restart + update panel + field hints
Backend:
- POST /system/restart: os.Exit(0) so systemd Restart=always reboots
the panel. Same pattern as RestoreDatabase.
- GET /system/update-info: queries GitHub releases API for latest
version, compares to buildinfo.Version, returns update_available flag.
- POST /system/update: triggers update.sh in background. Panel restarts
automatically when update completes.
Frontend:
- New 'System ops' section in Settings sidebar (between 'Traffic reset'
and 'About').
- SystemOpsCard component with 2 cards:
1. Restart panel: Modal.confirm → POST /system/restart → poll
/api/v1/health → auto-reload page. Button is danger-styled.
2. Panel update: auto-checks latest version on mount. Shows current vs
latest tags + 'Update available' / 'Up to date' badge. Collapsible
release notes. 'Check for updates' + 'Update now' buttons. Update
polls health with 120s timeout (update takes ~60s).
- 12 Form.Item fields across access/telegram/subscription sections now
have tooltip hints (public_host, public_port, sni, client_fingerprint,
bot_token, cpu_warn_pct, traffic_warn_pct, expiry_warn_hours,
proxy_url, api_server, sub_title, sub_support_url).
i18n:
- 23 new keys (navOps, restartPanel, restartPanelHint, restartNow,
restartConfirmTitle, restartConfirmHint, restarting, panelUpdate,
updateHint, checkUpdate, updateNow, updateConfirmTitle,
updateConfirmHint, updating, updateTimeout, updateAvailable,
upToDate, upToDateHint, updateCheckFailed, currentVersion,
latestVersion, releaseNotes, unknown) added to all 18 locales.
- 12 field hint keys (publicHostHint, publicPortHint, accessSniHint,
clientFingerprintHint, botTokenHint, cpuWarnPctHint, trafficWarnPctHint,
expiryWarnHoursHint, tgProxyHint, tgApiServerHint, subTitleHint,
subSupportUrlHint) added to all 18 locales.
Icons:
- IconSettingsOps (Wrench from lucide-react) for sidebar nav.
- IconUpdate (CloudDownload from lucide-react) for update button.
Responsive + theme:
- Uses antd theme.useToken() tokens for all colors (text.secondary, etc).
- Mobile: buttons are full-width (block={isMobile}).
- Dark mode: all text uses type='secondary' or token-based styles.
- Release notes pre uses var(--ant-color-fill-quaternary) background.
Tests: backend 23/23 PASS, frontend build PASS, lint 0 errors,
16/16 unit tests PASS.
feat(settings): add restart + update panel + field hints
Backend:
- POST /system/restart: os.Exit(0) so systemd Restart=always reboots
the panel. Same pattern as RestoreDatabase.
- GET /system/update-info: queries GitHub releases API for latest
version, compares to buildinfo.Version, returns update_available flag.
- POST /system/update: triggers update.sh in background. Panel restarts
automatically when update completes.
Frontend:
- New 'System ops' section in Settings sidebar (between 'Traffic reset'
and 'About').
- SystemOpsCard component with 2 cards:
1. Restart panel: Modal.confirm → POST /system/restart → poll
/api/v1/health → auto-reload page. Button is danger-styled.
2. Panel update: auto-checks latest version on mount. Shows current vs
latest tags + 'Update available' / 'Up to date' badge. Collapsible
release notes. 'Check for updates' + 'Update now' buttons. Update
polls health with 120s timeout (update takes ~60s).
- 12 Form.Item fields across access/telegram/subscription sections now
have tooltip hints (public_host, public_port, sni, client_fingerprint,
bot_token, cpu_warn_pct, traffic_warn_pct, expiry_warn_hours,
proxy_url, api_server, sub_title, sub_support_url).
i18n:
- 23 new keys (navOps, restartPanel, restartPanelHint, restartNow,
restartConfirmTitle, restartConfirmHint, restarting, panelUpdate,
updateHint, checkUpdate, updateNow, updateConfirmTitle,
updateConfirmHint, updating, updateTimeout, updateAvailable,
upToDate, upToDateHint, updateCheckFailed, currentVersion,
latestVersion, releaseNotes, unknown) added to all 18 locales.
- 12 field hint keys (publicHostHint, publicPortHint, accessSniHint,
clientFingerprintHint, botTokenHint, cpuWarnPctHint, trafficWarnPctHint,
expiryWarnHoursHint, tgProxyHint, tgApiServerHint, subTitleHint,
subSupportUrlHint) added to all 18 locales.
Icons:
- IconSettingsOps (Wrench from lucide-react) for sidebar nav.
- IconUpdate (CloudDownload from lucide-react) for update button.
Responsive + theme:
- Uses antd theme.useToken() tokens for all colors (text.secondary, etc).
- Mobile: buttons are full-width (block={isMobile}).
- Dark mode: all text uses type='secondary' or token-based styles.
- Release notes pre uses var(--ant-color-fill-quaternary) background.
Tests: backend 23/23 PASS, frontend build PASS, lint 0 errors,
16/16 unit tests PASS.
feat(settings): add restart + update panel + field hints
Backend:
- POST /system/restart: os.Exit(0) so systemd Restart=always reboots
the panel. Same pattern as RestoreDatabase.
- GET /system/update-info: queries GitHub releases API for latest
version, compares to buildinfo.Version, returns update_available flag.
- POST /system/update: triggers update.sh in background. Panel restarts
automatically when update completes.
Frontend:
- New 'System ops' section in Settings sidebar (between 'Traffic reset'
and 'About').
- SystemOpsCard component with 2 cards:
1. Restart panel: Modal.confirm → POST /system/restart → poll
/api/v1/health → auto-reload page. Button is danger-styled.
2. Panel update: auto-checks latest version on mount. Shows current vs
latest tags + 'Update available' / 'Up to date' badge. Collapsible
release notes. 'Check for updates' + 'Update now' buttons. Update
polls health with 120s timeout (update takes ~60s).
- 12 Form.Item fields across access/telegram/subscription sections now
have tooltip hints (public_host, public_port, sni, client_fingerprint,
bot_token, cpu_warn_pct, traffic_warn_pct, expiry_warn_hours,
proxy_url, api_server, sub_title, sub_support_url).
i18n:
- 23 new keys (navOps, restartPanel, restartPanelHint, restartNow,
restartConfirmTitle, restartConfirmHint, restarting, panelUpdate,
updateHint, checkUpdate, updateNow, updateConfirmTitle,
updateConfirmHint, updating, updateTimeout, updateAvailable,
upToDate, upToDateHint, updateCheckFailed, currentVersion,
latestVersion, releaseNotes, unknown) added to all 18 locales.
- 12 field hint keys (publicHostHint, publicPortHint, accessSniHint,
clientFingerprintHint, botTokenHint, cpuWarnPctHint, trafficWarnPctHint,
expiryWarnHoursHint, tgProxyHint, tgApiServerHint, subTitleHint,
subSupportUrlHint) added to all 18 locales.
Icons:
- IconSettingsOps (Wrench from lucide-react) for sidebar nav.
- IconUpdate (CloudDownload from lucide-react) for update button.
Responsive + theme:
- Uses antd theme.useToken() tokens for all colors (text.secondary, etc).
- Mobile: buttons are full-width (block={isMobile}).
- Dark mode: all text uses type='secondary' or token-based styles.
- Release notes pre uses var(--ant-color-fill-quaternary) background.
Tests: backend 23/23 PASS, frontend build PASS, lint 0 errors,
16/16 unit tests PASS.
feat(settings): add restart + update panel + field hints
Backend:
- POST /system/restart: os.Exit(0) so systemd Restart=always reboots
the panel. Same pattern as RestoreDatabase.
- GET /system/update-info: queries GitHub releases API for latest
version, compares to buildinfo.Version, returns update_available flag.
- POST /system/update: triggers update.sh in background. Panel restarts
automatically when update completes.
Frontend:
- New 'System ops' section in Settings sidebar (between 'Traffic reset'
and 'About').
- SystemOpsCard component with 2 cards:
1. Restart panel: Modal.confirm → POST /system/restart → poll
/api/v1/health → auto-reload page. Button is danger-styled.
2. Panel update: auto-checks latest version on mount. Shows current vs
latest tags + 'Update available' / 'Up to date' badge. Collapsible
release notes. 'Check for updates' + 'Update now' buttons. Update
polls health with 120s timeout (update takes ~60s).
- 12 Form.Item fields across access/telegram/subscription sections now
have tooltip hints (public_host, public_port, sni, client_fingerprint,
bot_token, cpu_warn_pct, traffic_warn_pct, expiry_warn_hours,
proxy_url, api_server, sub_title, sub_support_url).
i18n:
- 23 new keys (navOps, restartPanel, restartPanelHint, restartNow,
restartConfirmTitle, restartConfirmHint, restarting, panelUpdate,
updateHint, checkUpdate, updateNow, updateConfirmTitle,
updateConfirmHint, updating, updateTimeout, updateAvailable,
upToDate, upToDateHint, updateCheckFailed, currentVersion,
latestVersion, releaseNotes, unknown) added to all 18 locales.
- 12 field hint keys (publicHostHint, publicPortHint, accessSniHint,
clientFingerprintHint, botTokenHint, cpuWarnPctHint, trafficWarnPctHint,
expiryWarnHoursHint, tgProxyHint, tgApiServerHint, subTitleHint,
subSupportUrlHint) added to all 18 locales.
Icons:
- IconSettingsOps (Wrench from lucide-react) for sidebar nav.
- IconUpdate (CloudDownload from lucide-react) for update button.
Responsive + theme:
- Uses antd theme.useToken() tokens for all colors (text.secondary, etc).
- Mobile: buttons are full-width (block={isMobile}).
- Dark mode: all text uses type='secondary' or token-based styles.
- Release notes pre uses var(--ant-color-fill-quaternary) background.
Tests: backend 23/23 PASS, frontend build PASS, lint 0 errors,
16/16 unit tests PASS.
2026-09-25 11:38:12 +00:00
Z User 62b3d810c5 fix(ci): FormData guard for Node test environment
The previous commit (57122a3) added 'config.data instanceof FormData'
to the axios interceptor to clear Content-Type for multipart uploads.
In Node.js test environment (node --test), FormData is not defined
globally (it's a browser global), so 'instanceof FormData' throws
ReferenceError, breaking 2 tests in apiTimeout.test.mjs:
Error: FormData is not defined
at client.ts:83
at apiTimeout.test.mjs:30
Fix: guard with typeof check so the instanceof only runs in browsers
that actually have FormData defined.
typeof FormData !== 'undefined' && config.data instanceof FormData
Frontend unit tests: 16/16 PASS (was 14 pass / 2 fail).
fix(ci): FormData guard for Node test environment
The previous commit (57122a3) added 'config.data instanceof FormData'
to the axios interceptor to clear Content-Type for multipart uploads.
In Node.js test environment (node --test), FormData is not defined
globally (it's a browser global), so 'instanceof FormData' throws
ReferenceError, breaking 2 tests in apiTimeout.test.mjs:
Error: FormData is not defined
at client.ts:83
at apiTimeout.test.mjs:30
Fix: guard with typeof check so the instanceof only runs in browsers
that actually have FormData defined.
typeof FormData !== 'undefined' && config.data instanceof FormData
Frontend unit tests: 16/16 PASS (was 14 pass / 2 fail).
fix(ci): FormData guard for Node test environment
The previous commit (57122a3) added 'config.data instanceof FormData'
to the axios interceptor to clear Content-Type for multipart uploads.
In Node.js test environment (node --test), FormData is not defined
globally (it's a browser global), so 'instanceof FormData' throws
ReferenceError, breaking 2 tests in apiTimeout.test.mjs:
Error: FormData is not defined
at client.ts:83
at apiTimeout.test.mjs:30
Fix: guard with typeof check so the instanceof only runs in browsers
that actually have FormData defined.
typeof FormData !== 'undefined' && config.data instanceof FormData
Frontend unit tests: 16/16 PASS (was 14 pass / 2 fail).
fix(ci): FormData guard for Node test environment
The previous commit (57122a3) added 'config.data instanceof FormData'
to the axios interceptor to clear Content-Type for multipart uploads.
In Node.js test environment (node --test), FormData is not defined
globally (it's a browser global), so 'instanceof FormData' throws
ReferenceError, breaking 2 tests in apiTimeout.test.mjs:
Error: FormData is not defined
at client.ts:83
at apiTimeout.test.mjs:30
Fix: guard with typeof check so the instanceof only runs in browsers
that actually have FormData defined.
typeof FormData !== 'undefined' && config.data instanceof FormData
Frontend unit tests: 16/16 PASS (was 14 pass / 2 fail).
fix(ci): FormData guard for Node test environment
The previous commit (57122a3) added 'config.data instanceof FormData'
to the axios interceptor to clear Content-Type for multipart uploads.
In Node.js test environment (node --test), FormData is not defined
globally (it's a browser global), so 'instanceof FormData' throws
ReferenceError, breaking 2 tests in apiTimeout.test.mjs:
Error: FormData is not defined
at client.ts:83
at apiTimeout.test.mjs:30
Fix: guard with typeof check so the instanceof only runs in browsers
that actually have FormData defined.
typeof FormData !== 'undefined' && config.data instanceof FormData
Frontend unit tests: 16/16 PASS (was 14 pass / 2 fail).
fix(ci): FormData guard for Node test environment
The previous commit (57122a3) added 'config.data instanceof FormData'
to the axios interceptor to clear Content-Type for multipart uploads.
In Node.js test environment (node --test), FormData is not defined
globally (it's a browser global), so 'instanceof FormData' throws
ReferenceError, breaking 2 tests in apiTimeout.test.mjs:
Error: FormData is not defined
at client.ts:83
at apiTimeout.test.mjs:30
Fix: guard with typeof check so the instanceof only runs in browsers
that actually have FormData defined.
typeof FormData !== 'undefined' && config.data instanceof FormData
Frontend unit tests: 16/16 PASS (was 14 pass / 2 fail).
fix(ci): FormData guard for Node test environment
The previous commit (57122a3) added 'config.data instanceof FormData'
to the axios interceptor to clear Content-Type for multipart uploads.
In Node.js test environment (node --test), FormData is not defined
globally (it's a browser global), so 'instanceof FormData' throws
ReferenceError, breaking 2 tests in apiTimeout.test.mjs:
Error: FormData is not defined
at client.ts:83
at apiTimeout.test.mjs:30
Fix: guard with typeof check so the instanceof only runs in browsers
that actually have FormData defined.
typeof FormData !== 'undefined' && config.data instanceof FormData
Frontend unit tests: 16/16 PASS (was 14 pass / 2 fail).
fix(ci): FormData guard for Node test environment
The previous commit (57122a3) added 'config.data instanceof FormData'
to the axios interceptor to clear Content-Type for multipart uploads.
In Node.js test environment (node --test), FormData is not defined
globally (it's a browser global), so 'instanceof FormData' throws
ReferenceError, breaking 2 tests in apiTimeout.test.mjs:
Error: FormData is not defined
at client.ts:83
at apiTimeout.test.mjs:30
Fix: guard with typeof check so the instanceof only runs in browsers
that actually have FormData defined.
typeof FormData !== 'undefined' && config.data instanceof FormData
Frontend unit tests: 16/16 PASS (was 14 pass / 2 fail).
fix(ci): FormData guard for Node test environment
The previous commit (57122a3) added 'config.data instanceof FormData'
to the axios interceptor to clear Content-Type for multipart uploads.
In Node.js test environment (node --test), FormData is not defined
globally (it's a browser global), so 'instanceof FormData' throws
ReferenceError, breaking 2 tests in apiTimeout.test.mjs:
Error: FormData is not defined
at client.ts:83
at apiTimeout.test.mjs:30
Fix: guard with typeof check so the instanceof only runs in browsers
that actually have FormData defined.
typeof FormData !== 'undefined' && config.data instanceof FormData
Frontend unit tests: 16/16 PASS (was 14 pass / 2 fail).
fix(ci): FormData guard for Node test environment
The previous commit (57122a3) added 'config.data instanceof FormData'
to the axios interceptor to clear Content-Type for multipart uploads.
In Node.js test environment (node --test), FormData is not defined
globally (it's a browser global), so 'instanceof FormData' throws
ReferenceError, breaking 2 tests in apiTimeout.test.mjs:
Error: FormData is not defined
at client.ts:83
at apiTimeout.test.mjs:30
Fix: guard with typeof check so the instanceof only runs in browsers
that actually have FormData defined.
typeof FormData !== 'undefined' && config.data instanceof FormData
Frontend unit tests: 16/16 PASS (was 14 pass / 2 fail).
fix(ci): FormData guard for Node test environment
The previous commit (57122a3) added 'config.data instanceof FormData'
to the axios interceptor to clear Content-Type for multipart uploads.
In Node.js test environment (node --test), FormData is not defined
globally (it's a browser global), so 'instanceof FormData' throws
ReferenceError, breaking 2 tests in apiTimeout.test.mjs:
Error: FormData is not defined
at client.ts:83
at apiTimeout.test.mjs:30
Fix: guard with typeof check so the instanceof only runs in browsers
that actually have FormData defined.
typeof FormData !== 'undefined' && config.data instanceof FormData
Frontend unit tests: 16/16 PASS (was 14 pass / 2 fail).
fix(ci): FormData guard for Node test environment
The previous commit (57122a3) added 'config.data instanceof FormData'
to the axios interceptor to clear Content-Type for multipart uploads.
In Node.js test environment (node --test), FormData is not defined
globally (it's a browser global), so 'instanceof FormData' throws
ReferenceError, breaking 2 tests in apiTimeout.test.mjs:
Error: FormData is not defined
at client.ts:83
at apiTimeout.test.mjs:30
Fix: guard with typeof check so the instanceof only runs in browsers
that actually have FormData defined.
typeof FormData !== 'undefined' && config.data instanceof FormData
Frontend unit tests: 16/16 PASS (was 14 pass / 2 fail).
2026-09-25 09:08:19 +00:00
Z User 38342e3470 fix(backup): client errors return 400 (not 500) for invalid uploads
RestoreDatabase handler returned 500 for every RestoreDatabase error,
including client-side issues like 'not a valid SQLite database' or
'extract from zip' failures. These are 400-class (bad request) — the
operator should fix the file, not the server.
Now maps known client errors to 400:
- 'not a valid SQLite database' (magic header mismatch)
- 'does not contain a database' (empty zip)
- 'extract from zip' (corrupt zip)
- 'empty or does not contain' (empty upload)
- 'database path is empty' (misconfigured server)
Real server errors (disk full, permission denied) still return 500.
Verified end-to-end:
- invalid file → HTTP 400 'not a valid SQLite database: missing magic header'
- valid zip    → HTTP 200 + os.Exit (panel restarts via systemd)
- raw .db      → HTTP 200 + os.Exit
- cleanup keep=3,older_than_days=1 on 5 backups all 2 days old → keeps 3
(B8 AND semantics — newest 'keep' always protected)
- 8 unit tests PASS, full suite 23/23 PASS
fix(backup): client errors return 400 (not 500) for invalid uploads
RestoreDatabase handler returned 500 for every RestoreDatabase error,
including client-side issues like 'not a valid SQLite database' or
'extract from zip' failures. These are 400-class (bad request) — the
operator should fix the file, not the server.
Now maps known client errors to 400:
- 'not a valid SQLite database' (magic header mismatch)
- 'does not contain a database' (empty zip)
- 'extract from zip' (corrupt zip)
- 'empty or does not contain' (empty upload)
- 'database path is empty' (misconfigured server)
Real server errors (disk full, permission denied) still return 500.
Verified end-to-end:
- invalid file → HTTP 400 'not a valid SQLite database: missing magic header'
- valid zip    → HTTP 200 + os.Exit (panel restarts via systemd)
- raw .db      → HTTP 200 + os.Exit
- cleanup keep=3,older_than_days=1 on 5 backups all 2 days old → keeps 3
(B8 AND semantics — newest 'keep' always protected)
- 8 unit tests PASS, full suite 23/23 PASS
fix(backup): client errors return 400 (not 500) for invalid uploads
RestoreDatabase handler returned 500 for every RestoreDatabase error,
including client-side issues like 'not a valid SQLite database' or
'extract from zip' failures. These are 400-class (bad request) — the
operator should fix the file, not the server.
Now maps known client errors to 400:
- 'not a valid SQLite database' (magic header mismatch)
- 'does not contain a database' (empty zip)
- 'extract from zip' (corrupt zip)
- 'empty or does not contain' (empty upload)
- 'database path is empty' (misconfigured server)
Real server errors (disk full, permission denied) still return 500.
Verified end-to-end:
- invalid file → HTTP 400 'not a valid SQLite database: missing magic header'
- valid zip    → HTTP 200 + os.Exit (panel restarts via systemd)
- raw .db      → HTTP 200 + os.Exit
- cleanup keep=3,older_than_days=1 on 5 backups all 2 days old → keeps 3
(B8 AND semantics — newest 'keep' always protected)
- 8 unit tests PASS, full suite 23/23 PASS
fix(backup): client errors return 400 (not 500) for invalid uploads
RestoreDatabase handler returned 500 for every RestoreDatabase error,
including client-side issues like 'not a valid SQLite database' or
'extract from zip' failures. These are 400-class (bad request) — the
operator should fix the file, not the server.
Now maps known client errors to 400:
- 'not a valid SQLite database' (magic header mismatch)
- 'does not contain a database' (empty zip)
- 'extract from zip' (corrupt zip)
- 'empty or does not contain' (empty upload)
- 'database path is empty' (misconfigured server)
Real server errors (disk full, permission denied) still return 500.
Verified end-to-end:
- invalid file → HTTP 400 'not a valid SQLite database: missing magic header'
- valid zip    → HTTP 200 + os.Exit (panel restarts via systemd)
- raw .db      → HTTP 200 + os.Exit
- cleanup keep=3,older_than_days=1 on 5 backups all 2 days old → keeps 3
(B8 AND semantics — newest 'keep' always protected)
- 8 unit tests PASS, full suite 23/23 PASS
fix(backup): client errors return 400 (not 500) for invalid uploads
RestoreDatabase handler returned 500 for every RestoreDatabase error,
including client-side issues like 'not a valid SQLite database' or
'extract from zip' failures. These are 400-class (bad request) — the
operator should fix the file, not the server.
Now maps known client errors to 400:
- 'not a valid SQLite database' (magic header mismatch)
- 'does not contain a database' (empty zip)
- 'extract from zip' (corrupt zip)
- 'empty or does not contain' (empty upload)
- 'database path is empty' (misconfigured server)
Real server errors (disk full, permission denied) still return 500.
Verified end-to-end:
- invalid file → HTTP 400 'not a valid SQLite database: missing magic header'
- valid zip    → HTTP 200 + os.Exit (panel restarts via systemd)
- raw .db      → HTTP 200 + os.Exit
- cleanup keep=3,older_than_days=1 on 5 backups all 2 days old → keeps 3
(B8 AND semantics — newest 'keep' always protected)
- 8 unit tests PASS, full suite 23/23 PASS
fix(backup): client errors return 400 (not 500) for invalid uploads
RestoreDatabase handler returned 500 for every RestoreDatabase error,
including client-side issues like 'not a valid SQLite database' or
'extract from zip' failures. These are 400-class (bad request) — the
operator should fix the file, not the server.
Now maps known client errors to 400:
- 'not a valid SQLite database' (magic header mismatch)
- 'does not contain a database' (empty zip)
- 'extract from zip' (corrupt zip)
- 'empty or does not contain' (empty upload)
- 'database path is empty' (misconfigured server)
Real server errors (disk full, permission denied) still return 500.
Verified end-to-end:
- invalid file → HTTP 400 'not a valid SQLite database: missing magic header'
- valid zip    → HTTP 200 + os.Exit (panel restarts via systemd)
- raw .db      → HTTP 200 + os.Exit
- cleanup keep=3,older_than_days=1 on 5 backups all 2 days old → keeps 3
(B8 AND semantics — newest 'keep' always protected)
- 8 unit tests PASS, full suite 23/23 PASS
fix(backup): client errors return 400 (not 500) for invalid uploads
RestoreDatabase handler returned 500 for every RestoreDatabase error,
including client-side issues like 'not a valid SQLite database' or
'extract from zip' failures. These are 400-class (bad request) — the
operator should fix the file, not the server.
Now maps known client errors to 400:
- 'not a valid SQLite database' (magic header mismatch)
- 'does not contain a database' (empty zip)
- 'extract from zip' (corrupt zip)
- 'empty or does not contain' (empty upload)
- 'database path is empty' (misconfigured server)
Real server errors (disk full, permission denied) still return 500.
Verified end-to-end:
- invalid file → HTTP 400 'not a valid SQLite database: missing magic header'
- valid zip    → HTTP 200 + os.Exit (panel restarts via systemd)
- raw .db      → HTTP 200 + os.Exit
- cleanup keep=3,older_than_days=1 on 5 backups all 2 days old → keeps 3
(B8 AND semantics — newest 'keep' always protected)
- 8 unit tests PASS, full suite 23/23 PASS
fix(backup): client errors return 400 (not 500) for invalid uploads
RestoreDatabase handler returned 500 for every RestoreDatabase error,
including client-side issues like 'not a valid SQLite database' or
'extract from zip' failures. These are 400-class (bad request) — the
operator should fix the file, not the server.
Now maps known client errors to 400:
- 'not a valid SQLite database' (magic header mismatch)
- 'does not contain a database' (empty zip)
- 'extract from zip' (corrupt zip)
- 'empty or does not contain' (empty upload)
- 'database path is empty' (misconfigured server)
Real server errors (disk full, permission denied) still return 500.
Verified end-to-end:
- invalid file → HTTP 400 'not a valid SQLite database: missing magic header'
- valid zip    → HTTP 200 + os.Exit (panel restarts via systemd)
- raw .db      → HTTP 200 + os.Exit
- cleanup keep=3,older_than_days=1 on 5 backups all 2 days old → keeps 3
(B8 AND semantics — newest 'keep' always protected)
- 8 unit tests PASS, full suite 23/23 PASS
fix(backup): client errors return 400 (not 500) for invalid uploads
RestoreDatabase handler returned 500 for every RestoreDatabase error,
including client-side issues like 'not a valid SQLite database' or
'extract from zip' failures. These are 400-class (bad request) — the
operator should fix the file, not the server.
Now maps known client errors to 400:
- 'not a valid SQLite database' (magic header mismatch)
- 'does not contain a database' (empty zip)
- 'extract from zip' (corrupt zip)
- 'empty or does not contain' (empty upload)
- 'database path is empty' (misconfigured server)
Real server errors (disk full, permission denied) still return 500.
Verified end-to-end:
- invalid file → HTTP 400 'not a valid SQLite database: missing magic header'
- valid zip    → HTTP 200 + os.Exit (panel restarts via systemd)
- raw .db      → HTTP 200 + os.Exit
- cleanup keep=3,older_than_days=1 on 5 backups all 2 days old → keeps 3
(B8 AND semantics — newest 'keep' always protected)
- 8 unit tests PASS, full suite 23/23 PASS
fix(backup): client errors return 400 (not 500) for invalid uploads
RestoreDatabase handler returned 500 for every RestoreDatabase error,
including client-side issues like 'not a valid SQLite database' or
'extract from zip' failures. These are 400-class (bad request) — the
operator should fix the file, not the server.
Now maps known client errors to 400:
- 'not a valid SQLite database' (magic header mismatch)
- 'does not contain a database' (empty zip)
- 'extract from zip' (corrupt zip)
- 'empty or does not contain' (empty upload)
- 'database path is empty' (misconfigured server)
Real server errors (disk full, permission denied) still return 500.
Verified end-to-end:
- invalid file → HTTP 400 'not a valid SQLite database: missing magic header'
- valid zip    → HTTP 200 + os.Exit (panel restarts via systemd)
- raw .db      → HTTP 200 + os.Exit
- cleanup keep=3,older_than_days=1 on 5 backups all 2 days old → keeps 3
(B8 AND semantics — newest 'keep' always protected)
- 8 unit tests PASS, full suite 23/23 PASS
fix(backup): client errors return 400 (not 500) for invalid uploads
RestoreDatabase handler returned 500 for every RestoreDatabase error,
including client-side issues like 'not a valid SQLite database' or
'extract from zip' failures. These are 400-class (bad request) — the
operator should fix the file, not the server.
Now maps known client errors to 400:
- 'not a valid SQLite database' (magic header mismatch)
- 'does not contain a database' (empty zip)
- 'extract from zip' (corrupt zip)
- 'empty or does not contain' (empty upload)
- 'database path is empty' (misconfigured server)
Real server errors (disk full, permission denied) still return 500.
Verified end-to-end:
- invalid file → HTTP 400 'not a valid SQLite database: missing magic header'
- valid zip    → HTTP 200 + os.Exit (panel restarts via systemd)
- raw .db      → HTTP 200 + os.Exit
- cleanup keep=3,older_than_days=1 on 5 backups all 2 days old → keeps 3
(B8 AND semantics — newest 'keep' always protected)
- 8 unit tests PASS, full suite 23/23 PASS
fix(backup): client errors return 400 (not 500) for invalid uploads
RestoreDatabase handler returned 500 for every RestoreDatabase error,
including client-side issues like 'not a valid SQLite database' or
'extract from zip' failures. These are 400-class (bad request) — the
operator should fix the file, not the server.
Now maps known client errors to 400:
- 'not a valid SQLite database' (magic header mismatch)
- 'does not contain a database' (empty zip)
- 'extract from zip' (corrupt zip)
- 'empty or does not contain' (empty upload)
- 'database path is empty' (misconfigured server)
Real server errors (disk full, permission denied) still return 500.
Verified end-to-end:
- invalid file → HTTP 400 'not a valid SQLite database: missing magic header'
- valid zip    → HTTP 200 + os.Exit (panel restarts via systemd)
- raw .db      → HTTP 200 + os.Exit
- cleanup keep=3,older_than_days=1 on 5 backups all 2 days old → keeps 3
(B8 AND semantics — newest 'keep' always protected)
- 8 unit tests PASS, full suite 23/23 PASS
2026-09-25 07:49:51 +00:00
Z User 1ace570144 fix(backup): clear Content-Type for FormData uploads so multipart works
The axios client sets a global 'Content-Type: application/json' header.
For FormData uploads (restoreDatabase), this header must be removed so
the browser can set 'multipart/form-data; boundary=...' itself.
Without this fix, the panel's restore endpoint returned:
request Content-Type isn't multipart/form-data
Fix: in the request interceptor, detect config.data instanceof FormData
and delete the Content-Type header (both 'Content-Type' and 'contentType'
keys to cover AxiosHeader quirks). The browser then sets the proper
multipart Content-Type with boundary automatically.
fix(backup): clear Content-Type for FormData uploads so multipart works
The axios client sets a global 'Content-Type: application/json' header.
For FormData uploads (restoreDatabase), this header must be removed so
the browser can set 'multipart/form-data; boundary=...' itself.
Without this fix, the panel's restore endpoint returned:
request Content-Type isn't multipart/form-data
Fix: in the request interceptor, detect config.data instanceof FormData
and delete the Content-Type header (both 'Content-Type' and 'contentType'
keys to cover AxiosHeader quirks). The browser then sets the proper
multipart Content-Type with boundary automatically.
fix(backup): clear Content-Type for FormData uploads so multipart works
The axios client sets a global 'Content-Type: application/json' header.
For FormData uploads (restoreDatabase), this header must be removed so
the browser can set 'multipart/form-data; boundary=...' itself.
Without this fix, the panel's restore endpoint returned:
request Content-Type isn't multipart/form-data
Fix: in the request interceptor, detect config.data instanceof FormData
and delete the Content-Type header (both 'Content-Type' and 'contentType'
keys to cover AxiosHeader quirks). The browser then sets the proper
multipart Content-Type with boundary automatically.
fix(backup): clear Content-Type for FormData uploads so multipart works
The axios client sets a global 'Content-Type: application/json' header.
For FormData uploads (restoreDatabase), this header must be removed so
the browser can set 'multipart/form-data; boundary=...' itself.
Without this fix, the panel's restore endpoint returned:
request Content-Type isn't multipart/form-data
Fix: in the request interceptor, detect config.data instanceof FormData
and delete the Content-Type header (both 'Content-Type' and 'contentType'
keys to cover AxiosHeader quirks). The browser then sets the proper
multipart Content-Type with boundary automatically.
fix(backup): clear Content-Type for FormData uploads so multipart works
The axios client sets a global 'Content-Type: application/json' header.
For FormData uploads (restoreDatabase), this header must be removed so
the browser can set 'multipart/form-data; boundary=...' itself.
Without this fix, the panel's restore endpoint returned:
request Content-Type isn't multipart/form-data
Fix: in the request interceptor, detect config.data instanceof FormData
and delete the Content-Type header (both 'Content-Type' and 'contentType'
keys to cover AxiosHeader quirks). The browser then sets the proper
multipart Content-Type with boundary automatically.
fix(backup): clear Content-Type for FormData uploads so multipart works
The axios client sets a global 'Content-Type: application/json' header.
For FormData uploads (restoreDatabase), this header must be removed so
the browser can set 'multipart/form-data; boundary=...' itself.
Without this fix, the panel's restore endpoint returned:
request Content-Type isn't multipart/form-data
Fix: in the request interceptor, detect config.data instanceof FormData
and delete the Content-Type header (both 'Content-Type' and 'contentType'
keys to cover AxiosHeader quirks). The browser then sets the proper
multipart Content-Type with boundary automatically.
fix(backup): clear Content-Type for FormData uploads so multipart works
The axios client sets a global 'Content-Type: application/json' header.
For FormData uploads (restoreDatabase), this header must be removed so
the browser can set 'multipart/form-data; boundary=...' itself.
Without this fix, the panel's restore endpoint returned:
request Content-Type isn't multipart/form-data
Fix: in the request interceptor, detect config.data instanceof FormData
and delete the Content-Type header (both 'Content-Type' and 'contentType'
keys to cover AxiosHeader quirks). The browser then sets the proper
multipart Content-Type with boundary automatically.
fix(backup): clear Content-Type for FormData uploads so multipart works
The axios client sets a global 'Content-Type: application/json' header.
For FormData uploads (restoreDatabase), this header must be removed so
the browser can set 'multipart/form-data; boundary=...' itself.
Without this fix, the panel's restore endpoint returned:
request Content-Type isn't multipart/form-data
Fix: in the request interceptor, detect config.data instanceof FormData
and delete the Content-Type header (both 'Content-Type' and 'contentType'
keys to cover AxiosHeader quirks). The browser then sets the proper
multipart Content-Type with boundary automatically.
fix(backup): clear Content-Type for FormData uploads so multipart works
The axios client sets a global 'Content-Type: application/json' header.
For FormData uploads (restoreDatabase), this header must be removed so
the browser can set 'multipart/form-data; boundary=...' itself.
Without this fix, the panel's restore endpoint returned:
request Content-Type isn't multipart/form-data
Fix: in the request interceptor, detect config.data instanceof FormData
and delete the Content-Type header (both 'Content-Type' and 'contentType'
keys to cover AxiosHeader quirks). The browser then sets the proper
multipart Content-Type with boundary automatically.
fix(backup): clear Content-Type for FormData uploads so multipart works
The axios client sets a global 'Content-Type: application/json' header.
For FormData uploads (restoreDatabase), this header must be removed so
the browser can set 'multipart/form-data; boundary=...' itself.
Without this fix, the panel's restore endpoint returned:
request Content-Type isn't multipart/form-data
Fix: in the request interceptor, detect config.data instanceof FormData
and delete the Content-Type header (both 'Content-Type' and 'contentType'
keys to cover AxiosHeader quirks). The browser then sets the proper
multipart Content-Type with boundary automatically.
fix(backup): clear Content-Type for FormData uploads so multipart works
The axios client sets a global 'Content-Type: application/json' header.
For FormData uploads (restoreDatabase), this header must be removed so
the browser can set 'multipart/form-data; boundary=...' itself.
Without this fix, the panel's restore endpoint returned:
request Content-Type isn't multipart/form-data
Fix: in the request interceptor, detect config.data instanceof FormData
and delete the Content-Type header (both 'Content-Type' and 'contentType'
keys to cover AxiosHeader quirks). The browser then sets the proper
multipart Content-Type with boundary automatically.
fix(backup): clear Content-Type for FormData uploads so multipart works
The axios client sets a global 'Content-Type: application/json' header.
For FormData uploads (restoreDatabase), this header must be removed so
the browser can set 'multipart/form-data; boundary=...' itself.
Without this fix, the panel's restore endpoint returned:
request Content-Type isn't multipart/form-data
Fix: in the request interceptor, detect config.data instanceof FormData
and delete the Content-Type header (both 'Content-Type' and 'contentType'
keys to cover AxiosHeader quirks). The browser then sets the proper
multipart Content-Type with boundary automatically.
2026-09-25 07:41:01 +00:00
Z User 5bda47befc fix(backup): surface real FormFile error + raise MultipartMemory cap
The restore endpoint returned a generic 'multipart field database is
required and must be <= 128 MiB' for EVERY c.FormFile failure — masking
the actual cause. Operators hitting this error couldn't tell whether:
- they forgot to select a file
- the upload exceeded 128 MiB
- the multipart boundary was missing (browser/axios issue)
- the upload was interrupted mid-stream
- gin's default 32 MiB MultipartMemory cap spilled to /tmp (and
/tmp was full or read-only, causing a silent parse failure)
Fixes:
1. Surface the actual err.Error() to the operator, with friendly
rewrites for known error classes ('request body too large' →
'upload exceeds 128 MiB limit', 'missing boundary' → 'invalid
upload', 'EOF' → 'upload was empty or interrupted').
2. Log the raw error to journalctl so it's always diagnosable.
3. Call c.Request.ParseMultipartForm(maxRestoreDatabaseBytes) before
c.FormFile to raise gin's MultipartMemory cap from the 32 MiB
default to 128 MiB (matching the request body cap). This keeps
100 MiB backups in RAM instead of spilling to /tmp, which fixes
the failure mode where /tmp is full or read-only.
fix(backup): surface real FormFile error + raise MultipartMemory cap
The restore endpoint returned a generic 'multipart field database is
required and must be <= 128 MiB' for EVERY c.FormFile failure — masking
the actual cause. Operators hitting this error couldn't tell whether:
- they forgot to select a file
- the upload exceeded 128 MiB
- the multipart boundary was missing (browser/axios issue)
- the upload was interrupted mid-stream
- gin's default 32 MiB MultipartMemory cap spilled to /tmp (and
/tmp was full or read-only, causing a silent parse failure)
Fixes:
1. Surface the actual err.Error() to the operator, with friendly
rewrites for known error classes ('request body too large' →
'upload exceeds 128 MiB limit', 'missing boundary' → 'invalid
upload', 'EOF' → 'upload was empty or interrupted').
2. Log the raw error to journalctl so it's always diagnosable.
3. Call c.Request.ParseMultipartForm(maxRestoreDatabaseBytes) before
c.FormFile to raise gin's MultipartMemory cap from the 32 MiB
default to 128 MiB (matching the request body cap). This keeps
100 MiB backups in RAM instead of spilling to /tmp, which fixes
the failure mode where /tmp is full or read-only.
fix(backup): surface real FormFile error + raise MultipartMemory cap
The restore endpoint returned a generic 'multipart field database is
required and must be <= 128 MiB' for EVERY c.FormFile failure — masking
the actual cause. Operators hitting this error couldn't tell whether:
- they forgot to select a file
- the upload exceeded 128 MiB
- the multipart boundary was missing (browser/axios issue)
- the upload was interrupted mid-stream
- gin's default 32 MiB MultipartMemory cap spilled to /tmp (and
/tmp was full or read-only, causing a silent parse failure)
Fixes:
1. Surface the actual err.Error() to the operator, with friendly
rewrites for known error classes ('request body too large' →
'upload exceeds 128 MiB limit', 'missing boundary' → 'invalid
upload', 'EOF' → 'upload was empty or interrupted').
2. Log the raw error to journalctl so it's always diagnosable.
3. Call c.Request.ParseMultipartForm(maxRestoreDatabaseBytes) before
c.FormFile to raise gin's MultipartMemory cap from the 32 MiB
default to 128 MiB (matching the request body cap). This keeps
100 MiB backups in RAM instead of spilling to /tmp, which fixes
the failure mode where /tmp is full or read-only.
fix(backup): surface real FormFile error + raise MultipartMemory cap
The restore endpoint returned a generic 'multipart field database is
required and must be <= 128 MiB' for EVERY c.FormFile failure — masking
the actual cause. Operators hitting this error couldn't tell whether:
- they forgot to select a file
- the upload exceeded 128 MiB
- the multipart boundary was missing (browser/axios issue)
- the upload was interrupted mid-stream
- gin's default 32 MiB MultipartMemory cap spilled to /tmp (and
/tmp was full or read-only, causing a silent parse failure)
Fixes:
1. Surface the actual err.Error() to the operator, with friendly
rewrites for known error classes ('request body too large' →
'upload exceeds 128 MiB limit', 'missing boundary' → 'invalid
upload', 'EOF' → 'upload was empty or interrupted').
2. Log the raw error to journalctl so it's always diagnosable.
3. Call c.Request.ParseMultipartForm(maxRestoreDatabaseBytes) before
c.FormFile to raise gin's MultipartMemory cap from the 32 MiB
default to 128 MiB (matching the request body cap). This keeps
100 MiB backups in RAM instead of spilling to /tmp, which fixes
the failure mode where /tmp is full or read-only.
fix(backup): surface real FormFile error + raise MultipartMemory cap
The restore endpoint returned a generic 'multipart field database is
required and must be <= 128 MiB' for EVERY c.FormFile failure — masking
the actual cause. Operators hitting this error couldn't tell whether:
- they forgot to select a file
- the upload exceeded 128 MiB
- the multipart boundary was missing (browser/axios issue)
- the upload was interrupted mid-stream
- gin's default 32 MiB MultipartMemory cap spilled to /tmp (and
/tmp was full or read-only, causing a silent parse failure)
Fixes:
1. Surface the actual err.Error() to the operator, with friendly
rewrites for known error classes ('request body too large' →
'upload exceeds 128 MiB limit', 'missing boundary' → 'invalid
upload', 'EOF' → 'upload was empty or interrupted').
2. Log the raw error to journalctl so it's always diagnosable.
3. Call c.Request.ParseMultipartForm(maxRestoreDatabaseBytes) before
c.FormFile to raise gin's MultipartMemory cap from the 32 MiB
default to 128 MiB (matching the request body cap). This keeps
100 MiB backups in RAM instead of spilling to /tmp, which fixes
the failure mode where /tmp is full or read-only.
fix(backup): surface real FormFile error + raise MultipartMemory cap
The restore endpoint returned a generic 'multipart field database is
required and must be <= 128 MiB' for EVERY c.FormFile failure — masking
the actual cause. Operators hitting this error couldn't tell whether:
- they forgot to select a file
- the upload exceeded 128 MiB
- the multipart boundary was missing (browser/axios issue)
- the upload was interrupted mid-stream
- gin's default 32 MiB MultipartMemory cap spilled to /tmp (and
/tmp was full or read-only, causing a silent parse failure)
Fixes:
1. Surface the actual err.Error() to the operator, with friendly
rewrites for known error classes ('request body too large' →
'upload exceeds 128 MiB limit', 'missing boundary' → 'invalid
upload', 'EOF' → 'upload was empty or interrupted').
2. Log the raw error to journalctl so it's always diagnosable.
3. Call c.Request.ParseMultipartForm(maxRestoreDatabaseBytes) before
c.FormFile to raise gin's MultipartMemory cap from the 32 MiB
default to 128 MiB (matching the request body cap). This keeps
100 MiB backups in RAM instead of spilling to /tmp, which fixes
the failure mode where /tmp is full or read-only.
fix(backup): surface real FormFile error + raise MultipartMemory cap
The restore endpoint returned a generic 'multipart field database is
required and must be <= 128 MiB' for EVERY c.FormFile failure — masking
the actual cause. Operators hitting this error couldn't tell whether:
- they forgot to select a file
- the upload exceeded 128 MiB
- the multipart boundary was missing (browser/axios issue)
- the upload was interrupted mid-stream
- gin's default 32 MiB MultipartMemory cap spilled to /tmp (and
/tmp was full or read-only, causing a silent parse failure)
Fixes:
1. Surface the actual err.Error() to the operator, with friendly
rewrites for known error classes ('request body too large' →
'upload exceeds 128 MiB limit', 'missing boundary' → 'invalid
upload', 'EOF' → 'upload was empty or interrupted').
2. Log the raw error to journalctl so it's always diagnosable.
3. Call c.Request.ParseMultipartForm(maxRestoreDatabaseBytes) before
c.FormFile to raise gin's MultipartMemory cap from the 32 MiB
default to 128 MiB (matching the request body cap). This keeps
100 MiB backups in RAM instead of spilling to /tmp, which fixes
the failure mode where /tmp is full or read-only.
fix(backup): surface real FormFile error + raise MultipartMemory cap
The restore endpoint returned a generic 'multipart field database is
required and must be <= 128 MiB' for EVERY c.FormFile failure — masking
the actual cause. Operators hitting this error couldn't tell whether:
- they forgot to select a file
- the upload exceeded 128 MiB
- the multipart boundary was missing (browser/axios issue)
- the upload was interrupted mid-stream
- gin's default 32 MiB MultipartMemory cap spilled to /tmp (and
/tmp was full or read-only, causing a silent parse failure)
Fixes:
1. Surface the actual err.Error() to the operator, with friendly
rewrites for known error classes ('request body too large' →
'upload exceeds 128 MiB limit', 'missing boundary' → 'invalid
upload', 'EOF' → 'upload was empty or interrupted').
2. Log the raw error to journalctl so it's always diagnosable.
3. Call c.Request.ParseMultipartForm(maxRestoreDatabaseBytes) before
c.FormFile to raise gin's MultipartMemory cap from the 32 MiB
default to 128 MiB (matching the request body cap). This keeps
100 MiB backups in RAM instead of spilling to /tmp, which fixes
the failure mode where /tmp is full or read-only.
fix(backup): surface real FormFile error + raise MultipartMemory cap
The restore endpoint returned a generic 'multipart field database is
required and must be <= 128 MiB' for EVERY c.FormFile failure — masking
the actual cause. Operators hitting this error couldn't tell whether:
- they forgot to select a file
- the upload exceeded 128 MiB
- the multipart boundary was missing (browser/axios issue)
- the upload was interrupted mid-stream
- gin's default 32 MiB MultipartMemory cap spilled to /tmp (and
/tmp was full or read-only, causing a silent parse failure)
Fixes:
1. Surface the actual err.Error() to the operator, with friendly
rewrites for known error classes ('request body too large' →
'upload exceeds 128 MiB limit', 'missing boundary' → 'invalid
upload', 'EOF' → 'upload was empty or interrupted').
2. Log the raw error to journalctl so it's always diagnosable.
3. Call c.Request.ParseMultipartForm(maxRestoreDatabaseBytes) before
c.FormFile to raise gin's MultipartMemory cap from the 32 MiB
default to 128 MiB (matching the request body cap). This keeps
100 MiB backups in RAM instead of spilling to /tmp, which fixes
the failure mode where /tmp is full or read-only.
fix(backup): surface real FormFile error + raise MultipartMemory cap
The restore endpoint returned a generic 'multipart field database is
required and must be <= 128 MiB' for EVERY c.FormFile failure — masking
the actual cause. Operators hitting this error couldn't tell whether:
- they forgot to select a file
- the upload exceeded 128 MiB
- the multipart boundary was missing (browser/axios issue)
- the upload was interrupted mid-stream
- gin's default 32 MiB MultipartMemory cap spilled to /tmp (and
/tmp was full or read-only, causing a silent parse failure)
Fixes:
1. Surface the actual err.Error() to the operator, with friendly
rewrites for known error classes ('request body too large' →
'upload exceeds 128 MiB limit', 'missing boundary' → 'invalid
upload', 'EOF' → 'upload was empty or interrupted').
2. Log the raw error to journalctl so it's always diagnosable.
3. Call c.Request.ParseMultipartForm(maxRestoreDatabaseBytes) before
c.FormFile to raise gin's MultipartMemory cap from the 32 MiB
default to 128 MiB (matching the request body cap). This keeps
100 MiB backups in RAM instead of spilling to /tmp, which fixes
the failure mode where /tmp is full or read-only.
fix(backup): surface real FormFile error + raise MultipartMemory cap
The restore endpoint returned a generic 'multipart field database is
required and must be <= 128 MiB' for EVERY c.FormFile failure — masking
the actual cause. Operators hitting this error couldn't tell whether:
- they forgot to select a file
- the upload exceeded 128 MiB
- the multipart boundary was missing (browser/axios issue)
- the upload was interrupted mid-stream
- gin's default 32 MiB MultipartMemory cap spilled to /tmp (and
/tmp was full or read-only, causing a silent parse failure)
Fixes:
1. Surface the actual err.Error() to the operator, with friendly
rewrites for known error classes ('request body too large' →
'upload exceeds 128 MiB limit', 'missing boundary' → 'invalid
upload', 'EOF' → 'upload was empty or interrupted').
2. Log the raw error to journalctl so it's always diagnosable.
3. Call c.Request.ParseMultipartForm(maxRestoreDatabaseBytes) before
c.FormFile to raise gin's MultipartMemory cap from the 32 MiB
default to 128 MiB (matching the request body cap). This keeps
100 MiB backups in RAM instead of spilling to /tmp, which fixes
the failure mode where /tmp is full or read-only.
fix(backup): surface real FormFile error + raise MultipartMemory cap
The restore endpoint returned a generic 'multipart field database is
required and must be <= 128 MiB' for EVERY c.FormFile failure — masking
the actual cause. Operators hitting this error couldn't tell whether:
- they forgot to select a file
- the upload exceeded 128 MiB
- the multipart boundary was missing (browser/axios issue)
- the upload was interrupted mid-stream
- gin's default 32 MiB MultipartMemory cap spilled to /tmp (and
/tmp was full or read-only, causing a silent parse failure)
Fixes:
1. Surface the actual err.Error() to the operator, with friendly
rewrites for known error classes ('request body too large' →
'upload exceeds 128 MiB limit', 'missing boundary' → 'invalid
upload', 'EOF' → 'upload was empty or interrupted').
2. Log the raw error to journalctl so it's always diagnosable.
3. Call c.Request.ParseMultipartForm(maxRestoreDatabaseBytes) before
c.FormFile to raise gin's MultipartMemory cap from the 32 MiB
default to 128 MiB (matching the request body cap). This keeps
100 MiB backups in RAM instead of spilling to /tmp, which fixes
the failure mode where /tmp is full or read-only.
2026-09-25 07:24:37 +00:00
Z User 5de2f6c1eb fix(backup): close critical backup/restore bugs
Audit found 5 HIGH-severity backup bugs. Fixed:
B1 (HIGH — silent data loss): RestoreDatabase handler returned 200 OK
but the panel's GORM pool kept writing to the old (unlinked) SQLite
inode. Every write between restore and a manual 'systemctl restart'
was silently lost. Fix: handler now os.Exit(0)s 500ms after responding
so systemd's Restart=always reboots the panel with the new DB. The
frontend polls /api/v1/health and auto-reloads the page when the
panel is back (no more dismissable 'remember to restart' modal).
B4 (HIGH): RestoreDatabase didn't delete stale -journal / -wal / -shm
siblings. SQLite could roll back the just-restored DB to a stale
transaction state on next open. Fix: remove all three suffixes
(ignore not-exist errors) after the rename.
B5 (HIGH): No SQLite magic-byte validation — any file (txt, corrupt
db, etc.) was accepted as a 'database' and silently overwrote the
live DB, then the panel boot-looped on the bad file. Fix: validate
the 16-byte 'SQLite format 3\0' header before rename; return 400
on mismatch.
B8 (HIGH — data loss): cleanupLocalBackups used OR semantics for
keep+age. With keep=3 + older_than_days=7 and all 3 backups older
than 7 days, ALL 3 were deleted — operator lost every snapshot.
Fix: change to AND semantics. The newest 'keep' entries ALWAYS
survive even if they're older than the age threshold. Age only
applies to entries beyond the keep window.
C2 (HIGH): Telegram sendBackup had no size guard. DB > 50 MB fails
the Bot API sendDocument limit with HTTP 413, and the scheduled
backup silently stopped working forever (error only in journalctl).
Fix: check buf.Len() > 50 MB before sending; if exceeded, send a
text notice to all chats instead ('DB too large to attach: N MB;
download via the web UI').
Also fixed:
- B6/B7: mihomo config restore was best-effort (errors silently
discarded). Now returns proper errors + a MihomoSkipped flag when
the zip contained mihomo-config.yaml but the panel has no
mihomo.config path configured.
- B13: RestoreDatabase MkdirAll mode 0o750 → 0o700 (matches
database.go, DB contains password hashes + secrets).
- B10: frontend restore shows 'Restarting panel…' modal with
auto-reload indicator; surfaces mihomo_config_skipped message.
Tests: 8 new unit tests in backup_test.go covering:
- RestoreDatabase rejects non-SQLite / empty uploads
- RestoreDatabase accepts raw SQLite + removes -wal/-shm/-journal
- RestoreDatabase restores mihomo config from zip
- RestoreDatabase reports MihomoSkipped when path empty
- cleanupLocalBackups keep+age AND semantics (newest N always safe)
- cleanupLocalBackups keep-only / age-only paths
Full suite: 23/23 backend packages PASS, frontend build + lint clean.
Not addressed in this commit (follow-ups):
- B2/C4: WriteZip still reads live DB without VACUUM INTO / Online
Backup API (inconsistent snapshot risk). Needs sqlite driver work.
- B3/C1: backup zip is unencrypted and contains bot_token + 2FA
secrets. Needs passphrase encryption or sanitized dump.
- A1: shell snapshot has no sha256 checksum verification on restore.
- B11: backup-meta.txt missing panel/schema version.
fix(backup): close critical backup/restore bugs
Audit found 5 HIGH-severity backup bugs. Fixed:
B1 (HIGH — silent data loss): RestoreDatabase handler returned 200 OK
but the panel's GORM pool kept writing to the old (unlinked) SQLite
inode. Every write between restore and a manual 'systemctl restart'
was silently lost. Fix: handler now os.Exit(0)s 500ms after responding
so systemd's Restart=always reboots the panel with the new DB. The
frontend polls /api/v1/health and auto-reloads the page when the
panel is back (no more dismissable 'remember to restart' modal).
B4 (HIGH): RestoreDatabase didn't delete stale -journal / -wal / -shm
siblings. SQLite could roll back the just-restored DB to a stale
transaction state on next open. Fix: remove all three suffixes
(ignore not-exist errors) after the rename.
B5 (HIGH): No SQLite magic-byte validation — any file (txt, corrupt
db, etc.) was accepted as a 'database' and silently overwrote the
live DB, then the panel boot-looped on the bad file. Fix: validate
the 16-byte 'SQLite format 3\0' header before rename; return 400
on mismatch.
B8 (HIGH — data loss): cleanupLocalBackups used OR semantics for
keep+age. With keep=3 + older_than_days=7 and all 3 backups older
than 7 days, ALL 3 were deleted — operator lost every snapshot.
Fix: change to AND semantics. The newest 'keep' entries ALWAYS
survive even if they're older than the age threshold. Age only
applies to entries beyond the keep window.
C2 (HIGH): Telegram sendBackup had no size guard. DB > 50 MB fails
the Bot API sendDocument limit with HTTP 413, and the scheduled
backup silently stopped working forever (error only in journalctl).
Fix: check buf.Len() > 50 MB before sending; if exceeded, send a
text notice to all chats instead ('DB too large to attach: N MB;
download via the web UI').
Also fixed:
- B6/B7: mihomo config restore was best-effort (errors silently
discarded). Now returns proper errors + a MihomoSkipped flag when
the zip contained mihomo-config.yaml but the panel has no
mihomo.config path configured.
- B13: RestoreDatabase MkdirAll mode 0o750 → 0o700 (matches
database.go, DB contains password hashes + secrets).
- B10: frontend restore shows 'Restarting panel…' modal with
auto-reload indicator; surfaces mihomo_config_skipped message.
Tests: 8 new unit tests in backup_test.go covering:
- RestoreDatabase rejects non-SQLite / empty uploads
- RestoreDatabase accepts raw SQLite + removes -wal/-shm/-journal
- RestoreDatabase restores mihomo config from zip
- RestoreDatabase reports MihomoSkipped when path empty
- cleanupLocalBackups keep+age AND semantics (newest N always safe)
- cleanupLocalBackups keep-only / age-only paths
Full suite: 23/23 backend packages PASS, frontend build + lint clean.
Not addressed in this commit (follow-ups):
- B2/C4: WriteZip still reads live DB without VACUUM INTO / Online
Backup API (inconsistent snapshot risk). Needs sqlite driver work.
- B3/C1: backup zip is unencrypted and contains bot_token + 2FA
secrets. Needs passphrase encryption or sanitized dump.
- A1: shell snapshot has no sha256 checksum verification on restore.
- B11: backup-meta.txt missing panel/schema version.
fix(backup): close critical backup/restore bugs
Audit found 5 HIGH-severity backup bugs. Fixed:
B1 (HIGH — silent data loss): RestoreDatabase handler returned 200 OK
but the panel's GORM pool kept writing to the old (unlinked) SQLite
inode. Every write between restore and a manual 'systemctl restart'
was silently lost. Fix: handler now os.Exit(0)s 500ms after responding
so systemd's Restart=always reboots the panel with the new DB. The
frontend polls /api/v1/health and auto-reloads the page when the
panel is back (no more dismissable 'remember to restart' modal).
B4 (HIGH): RestoreDatabase didn't delete stale -journal / -wal / -shm
siblings. SQLite could roll back the just-restored DB to a stale
transaction state on next open. Fix: remove all three suffixes
(ignore not-exist errors) after the rename.
B5 (HIGH): No SQLite magic-byte validation — any file (txt, corrupt
db, etc.) was accepted as a 'database' and silently overwrote the
live DB, then the panel boot-looped on the bad file. Fix: validate
the 16-byte 'SQLite format 3\0' header before rename; return 400
on mismatch.
B8 (HIGH — data loss): cleanupLocalBackups used OR semantics for
keep+age. With keep=3 + older_than_days=7 and all 3 backups older
than 7 days, ALL 3 were deleted — operator lost every snapshot.
Fix: change to AND semantics. The newest 'keep' entries ALWAYS
survive even if they're older than the age threshold. Age only
applies to entries beyond the keep window.
C2 (HIGH): Telegram sendBackup had no size guard. DB > 50 MB fails
the Bot API sendDocument limit with HTTP 413, and the scheduled
backup silently stopped working forever (error only in journalctl).
Fix: check buf.Len() > 50 MB before sending; if exceeded, send a
text notice to all chats instead ('DB too large to attach: N MB;
download via the web UI').
Also fixed:
- B6/B7: mihomo config restore was best-effort (errors silently
discarded). Now returns proper errors + a MihomoSkipped flag when
the zip contained mihomo-config.yaml but the panel has no
mihomo.config path configured.
- B13: RestoreDatabase MkdirAll mode 0o750 → 0o700 (matches
database.go, DB contains password hashes + secrets).
- B10: frontend restore shows 'Restarting panel…' modal with
auto-reload indicator; surfaces mihomo_config_skipped message.
Tests: 8 new unit tests in backup_test.go covering:
- RestoreDatabase rejects non-SQLite / empty uploads
- RestoreDatabase accepts raw SQLite + removes -wal/-shm/-journal
- RestoreDatabase restores mihomo config from zip
- RestoreDatabase reports MihomoSkipped when path empty
- cleanupLocalBackups keep+age AND semantics (newest N always safe)
- cleanupLocalBackups keep-only / age-only paths
Full suite: 23/23 backend packages PASS, frontend build + lint clean.
Not addressed in this commit (follow-ups):
- B2/C4: WriteZip still reads live DB without VACUUM INTO / Online
Backup API (inconsistent snapshot risk). Needs sqlite driver work.
- B3/C1: backup zip is unencrypted and contains bot_token + 2FA
secrets. Needs passphrase encryption or sanitized dump.
- A1: shell snapshot has no sha256 checksum verification on restore.
- B11: backup-meta.txt missing panel/schema version.
fix(backup): close critical backup/restore bugs
Audit found 5 HIGH-severity backup bugs. Fixed:
B1 (HIGH — silent data loss): RestoreDatabase handler returned 200 OK
but the panel's GORM pool kept writing to the old (unlinked) SQLite
inode. Every write between restore and a manual 'systemctl restart'
was silently lost. Fix: handler now os.Exit(0)s 500ms after responding
so systemd's Restart=always reboots the panel with the new DB. The
frontend polls /api/v1/health and auto-reloads the page when the
panel is back (no more dismissable 'remember to restart' modal).
B4 (HIGH): RestoreDatabase didn't delete stale -journal / -wal / -shm
siblings. SQLite could roll back the just-restored DB to a stale
transaction state on next open. Fix: remove all three suffixes
(ignore not-exist errors) after the rename.
B5 (HIGH): No SQLite magic-byte validation — any file (txt, corrupt
db, etc.) was accepted as a 'database' and silently overwrote the
live DB, then the panel boot-looped on the bad file. Fix: validate
the 16-byte 'SQLite format 3\0' header before rename; return 400
on mismatch.
B8 (HIGH — data loss): cleanupLocalBackups used OR semantics for
keep+age. With keep=3 + older_than_days=7 and all 3 backups older
than 7 days, ALL 3 were deleted — operator lost every snapshot.
Fix: change to AND semantics. The newest 'keep' entries ALWAYS
survive even if they're older than the age threshold. Age only
applies to entries beyond the keep window.
C2 (HIGH): Telegram sendBackup had no size guard. DB > 50 MB fails
the Bot API sendDocument limit with HTTP 413, and the scheduled
backup silently stopped working forever (error only in journalctl).
Fix: check buf.Len() > 50 MB before sending; if exceeded, send a
text notice to all chats instead ('DB too large to attach: N MB;
download via the web UI').
Also fixed:
- B6/B7: mihomo config restore was best-effort (errors silently
discarded). Now returns proper errors + a MihomoSkipped flag when
the zip contained mihomo-config.yaml but the panel has no
mihomo.config path configured.
- B13: RestoreDatabase MkdirAll mode 0o750 → 0o700 (matches
database.go, DB contains password hashes + secrets).
- B10: frontend restore shows 'Restarting panel…' modal with
auto-reload indicator; surfaces mihomo_config_skipped message.
Tests: 8 new unit tests in backup_test.go covering:
- RestoreDatabase rejects non-SQLite / empty uploads
- RestoreDatabase accepts raw SQLite + removes -wal/-shm/-journal
- RestoreDatabase restores mihomo config from zip
- RestoreDatabase reports MihomoSkipped when path empty
- cleanupLocalBackups keep+age AND semantics (newest N always safe)
- cleanupLocalBackups keep-only / age-only paths
Full suite: 23/23 backend packages PASS, frontend build + lint clean.
Not addressed in this commit (follow-ups):
- B2/C4: WriteZip still reads live DB without VACUUM INTO / Online
Backup API (inconsistent snapshot risk). Needs sqlite driver work.
- B3/C1: backup zip is unencrypted and contains bot_token + 2FA
secrets. Needs passphrase encryption or sanitized dump.
- A1: shell snapshot has no sha256 checksum verification on restore.
- B11: backup-meta.txt missing panel/schema version.
fix(backup): close critical backup/restore bugs
Audit found 5 HIGH-severity backup bugs. Fixed:
B1 (HIGH — silent data loss): RestoreDatabase handler returned 200 OK
but the panel's GORM pool kept writing to the old (unlinked) SQLite
inode. Every write between restore and a manual 'systemctl restart'
was silently lost. Fix: handler now os.Exit(0)s 500ms after responding
so systemd's Restart=always reboots the panel with the new DB. The
frontend polls /api/v1/health and auto-reloads the page when the
panel is back (no more dismissable 'remember to restart' modal).
B4 (HIGH): RestoreDatabase didn't delete stale -journal / -wal / -shm
siblings. SQLite could roll back the just-restored DB to a stale
transaction state on next open. Fix: remove all three suffixes
(ignore not-exist errors) after the rename.
B5 (HIGH): No SQLite magic-byte validation — any file (txt, corrupt
db, etc.) was accepted as a 'database' and silently overwrote the
live DB, then the panel boot-looped on the bad file. Fix: validate
the 16-byte 'SQLite format 3\0' header before rename; return 400
on mismatch.
B8 (HIGH — data loss): cleanupLocalBackups used OR semantics for
keep+age. With keep=3 + older_than_days=7 and all 3 backups older
than 7 days, ALL 3 were deleted — operator lost every snapshot.
Fix: change to AND semantics. The newest 'keep' entries ALWAYS
survive even if they're older than the age threshold. Age only
applies to entries beyond the keep window.
C2 (HIGH): Telegram sendBackup had no size guard. DB > 50 MB fails
the Bot API sendDocument limit with HTTP 413, and the scheduled
backup silently stopped working forever (error only in journalctl).
Fix: check buf.Len() > 50 MB before sending; if exceeded, send a
text notice to all chats instead ('DB too large to attach: N MB;
download via the web UI').
Also fixed:
- B6/B7: mihomo config restore was best-effort (errors silently
discarded). Now returns proper errors + a MihomoSkipped flag when
the zip contained mihomo-config.yaml but the panel has no
mihomo.config path configured.
- B13: RestoreDatabase MkdirAll mode 0o750 → 0o700 (matches
database.go, DB contains password hashes + secrets).
- B10: frontend restore shows 'Restarting panel…' modal with
auto-reload indicator; surfaces mihomo_config_skipped message.
Tests: 8 new unit tests in backup_test.go covering:
- RestoreDatabase rejects non-SQLite / empty uploads
- RestoreDatabase accepts raw SQLite + removes -wal/-shm/-journal
- RestoreDatabase restores mihomo config from zip
- RestoreDatabase reports MihomoSkipped when path empty
- cleanupLocalBackups keep+age AND semantics (newest N always safe)
- cleanupLocalBackups keep-only / age-only paths
Full suite: 23/23 backend packages PASS, frontend build + lint clean.
Not addressed in this commit (follow-ups):
- B2/C4: WriteZip still reads live DB without VACUUM INTO / Online
Backup API (inconsistent snapshot risk). Needs sqlite driver work.
- B3/C1: backup zip is unencrypted and contains bot_token + 2FA
secrets. Needs passphrase encryption or sanitized dump.
- A1: shell snapshot has no sha256 checksum verification on restore.
- B11: backup-meta.txt missing panel/schema version.
fix(backup): close critical backup/restore bugs
Audit found 5 HIGH-severity backup bugs. Fixed:
B1 (HIGH — silent data loss): RestoreDatabase handler returned 200 OK
but the panel's GORM pool kept writing to the old (unlinked) SQLite
inode. Every write between restore and a manual 'systemctl restart'
was silently lost. Fix: handler now os.Exit(0)s 500ms after responding
so systemd's Restart=always reboots the panel with the new DB. The
frontend polls /api/v1/health and auto-reloads the page when the
panel is back (no more dismissable 'remember to restart' modal).
B4 (HIGH): RestoreDatabase didn't delete stale -journal / -wal / -shm
siblings. SQLite could roll back the just-restored DB to a stale
transaction state on next open. Fix: remove all three suffixes
(ignore not-exist errors) after the rename.
B5 (HIGH): No SQLite magic-byte validation — any file (txt, corrupt
db, etc.) was accepted as a 'database' and silently overwrote the
live DB, then the panel boot-looped on the bad file. Fix: validate
the 16-byte 'SQLite format 3\0' header before rename; return 400
on mismatch.
B8 (HIGH — data loss): cleanupLocalBackups used OR semantics for
keep+age. With keep=3 + older_than_days=7 and all 3 backups older
than 7 days, ALL 3 were deleted — operator lost every snapshot.
Fix: change to AND semantics. The newest 'keep' entries ALWAYS
survive even if they're older than the age threshold. Age only
applies to entries beyond the keep window.
C2 (HIGH): Telegram sendBackup had no size guard. DB > 50 MB fails
the Bot API sendDocument limit with HTTP 413, and the scheduled
backup silently stopped working forever (error only in journalctl).
Fix: check buf.Len() > 50 MB before sending; if exceeded, send a
text notice to all chats instead ('DB too large to attach: N MB;
download via the web UI').
Also fixed:
- B6/B7: mihomo config restore was best-effort (errors silently
discarded). Now returns proper errors + a MihomoSkipped flag when
the zip contained mihomo-config.yaml but the panel has no
mihomo.config path configured.
- B13: RestoreDatabase MkdirAll mode 0o750 → 0o700 (matches
database.go, DB contains password hashes + secrets).
- B10: frontend restore shows 'Restarting panel…' modal with
auto-reload indicator; surfaces mihomo_config_skipped message.
Tests: 8 new unit tests in backup_test.go covering:
- RestoreDatabase rejects non-SQLite / empty uploads
- RestoreDatabase accepts raw SQLite + removes -wal/-shm/-journal
- RestoreDatabase restores mihomo config from zip
- RestoreDatabase reports MihomoSkipped when path empty
- cleanupLocalBackups keep+age AND semantics (newest N always safe)
- cleanupLocalBackups keep-only / age-only paths
Full suite: 23/23 backend packages PASS, frontend build + lint clean.
Not addressed in this commit (follow-ups):
- B2/C4: WriteZip still reads live DB without VACUUM INTO / Online
Backup API (inconsistent snapshot risk). Needs sqlite driver work.
- B3/C1: backup zip is unencrypted and contains bot_token + 2FA
secrets. Needs passphrase encryption or sanitized dump.
- A1: shell snapshot has no sha256 checksum verification on restore.
- B11: backup-meta.txt missing panel/schema version.
fix(backup): close critical backup/restore bugs
Audit found 5 HIGH-severity backup bugs. Fixed:
B1 (HIGH — silent data loss): RestoreDatabase handler returned 200 OK
but the panel's GORM pool kept writing to the old (unlinked) SQLite
inode. Every write between restore and a manual 'systemctl restart'
was silently lost. Fix: handler now os.Exit(0)s 500ms after responding
so systemd's Restart=always reboots the panel with the new DB. The
frontend polls /api/v1/health and auto-reloads the page when the
panel is back (no more dismissable 'remember to restart' modal).
B4 (HIGH): RestoreDatabase didn't delete stale -journal / -wal / -shm
siblings. SQLite could roll back the just-restored DB to a stale
transaction state on next open. Fix: remove all three suffixes
(ignore not-exist errors) after the rename.
B5 (HIGH): No SQLite magic-byte validation — any file (txt, corrupt
db, etc.) was accepted as a 'database' and silently overwrote the
live DB, then the panel boot-looped on the bad file. Fix: validate
the 16-byte 'SQLite format 3\0' header before rename; return 400
on mismatch.
B8 (HIGH — data loss): cleanupLocalBackups used OR semantics for
keep+age. With keep=3 + older_than_days=7 and all 3 backups older
than 7 days, ALL 3 were deleted — operator lost every snapshot.
Fix: change to AND semantics. The newest 'keep' entries ALWAYS
survive even if they're older than the age threshold. Age only
applies to entries beyond the keep window.
C2 (HIGH): Telegram sendBackup had no size guard. DB > 50 MB fails
the Bot API sendDocument limit with HTTP 413, and the scheduled
backup silently stopped working forever (error only in journalctl).
Fix: check buf.Len() > 50 MB before sending; if exceeded, send a
text notice to all chats instead ('DB too large to attach: N MB;
download via the web UI').
Also fixed:
- B6/B7: mihomo config restore was best-effort (errors silently
discarded). Now returns proper errors + a MihomoSkipped flag when
the zip contained mihomo-config.yaml but the panel has no
mihomo.config path configured.
- B13: RestoreDatabase MkdirAll mode 0o750 → 0o700 (matches
database.go, DB contains password hashes + secrets).
- B10: frontend restore shows 'Restarting panel…' modal with
auto-reload indicator; surfaces mihomo_config_skipped message.
Tests: 8 new unit tests in backup_test.go covering:
- RestoreDatabase rejects non-SQLite / empty uploads
- RestoreDatabase accepts raw SQLite + removes -wal/-shm/-journal
- RestoreDatabase restores mihomo config from zip
- RestoreDatabase reports MihomoSkipped when path empty
- cleanupLocalBackups keep+age AND semantics (newest N always safe)
- cleanupLocalBackups keep-only / age-only paths
Full suite: 23/23 backend packages PASS, frontend build + lint clean.
Not addressed in this commit (follow-ups):
- B2/C4: WriteZip still reads live DB without VACUUM INTO / Online
Backup API (inconsistent snapshot risk). Needs sqlite driver work.
- B3/C1: backup zip is unencrypted and contains bot_token + 2FA
secrets. Needs passphrase encryption or sanitized dump.
- A1: shell snapshot has no sha256 checksum verification on restore.
- B11: backup-meta.txt missing panel/schema version.
fix(backup): close critical backup/restore bugs
Audit found 5 HIGH-severity backup bugs. Fixed:
B1 (HIGH — silent data loss): RestoreDatabase handler returned 200 OK
but the panel's GORM pool kept writing to the old (unlinked) SQLite
inode. Every write between restore and a manual 'systemctl restart'
was silently lost. Fix: handler now os.Exit(0)s 500ms after responding
so systemd's Restart=always reboots the panel with the new DB. The
frontend polls /api/v1/health and auto-reloads the page when the
panel is back (no more dismissable 'remember to restart' modal).
B4 (HIGH): RestoreDatabase didn't delete stale -journal / -wal / -shm
siblings. SQLite could roll back the just-restored DB to a stale
transaction state on next open. Fix: remove all three suffixes
(ignore not-exist errors) after the rename.
B5 (HIGH): No SQLite magic-byte validation — any file (txt, corrupt
db, etc.) was accepted as a 'database' and silently overwrote the
live DB, then the panel boot-looped on the bad file. Fix: validate
the 16-byte 'SQLite format 3\0' header before rename; return 400
on mismatch.
B8 (HIGH — data loss): cleanupLocalBackups used OR semantics for
keep+age. With keep=3 + older_than_days=7 and all 3 backups older
than 7 days, ALL 3 were deleted — operator lost every snapshot.
Fix: change to AND semantics. The newest 'keep' entries ALWAYS
survive even if they're older than the age threshold. Age only
applies to entries beyond the keep window.
C2 (HIGH): Telegram sendBackup had no size guard. DB > 50 MB fails
the Bot API sendDocument limit with HTTP 413, and the scheduled
backup silently stopped working forever (error only in journalctl).
Fix: check buf.Len() > 50 MB before sending; if exceeded, send a
text notice to all chats instead ('DB too large to attach: N MB;
download via the web UI').
Also fixed:
- B6/B7: mihomo config restore was best-effort (errors silently
discarded). Now returns proper errors + a MihomoSkipped flag when
the zip contained mihomo-config.yaml but the panel has no
mihomo.config path configured.
- B13: RestoreDatabase MkdirAll mode 0o750 → 0o700 (matches
database.go, DB contains password hashes + secrets).
- B10: frontend restore shows 'Restarting panel…' modal with
auto-reload indicator; surfaces mihomo_config_skipped message.
Tests: 8 new unit tests in backup_test.go covering:
- RestoreDatabase rejects non-SQLite / empty uploads
- RestoreDatabase accepts raw SQLite + removes -wal/-shm/-journal
- RestoreDatabase restores mihomo config from zip
- RestoreDatabase reports MihomoSkipped when path empty
- cleanupLocalBackups keep+age AND semantics (newest N always safe)
- cleanupLocalBackups keep-only / age-only paths
Full suite: 23/23 backend packages PASS, frontend build + lint clean.
Not addressed in this commit (follow-ups):
- B2/C4: WriteZip still reads live DB without VACUUM INTO / Online
Backup API (inconsistent snapshot risk). Needs sqlite driver work.
- B3/C1: backup zip is unencrypted and contains bot_token + 2FA
secrets. Needs passphrase encryption or sanitized dump.
- A1: shell snapshot has no sha256 checksum verification on restore.
- B11: backup-meta.txt missing panel/schema version.
fix(backup): close critical backup/restore bugs
Audit found 5 HIGH-severity backup bugs. Fixed:
B1 (HIGH — silent data loss): RestoreDatabase handler returned 200 OK
but the panel's GORM pool kept writing to the old (unlinked) SQLite
inode. Every write between restore and a manual 'systemctl restart'
was silently lost. Fix: handler now os.Exit(0)s 500ms after responding
so systemd's Restart=always reboots the panel with the new DB. The
frontend polls /api/v1/health and auto-reloads the page when the
panel is back (no more dismissable 'remember to restart' modal).
B4 (HIGH): RestoreDatabase didn't delete stale -journal / -wal / -shm
siblings. SQLite could roll back the just-restored DB to a stale
transaction state on next open. Fix: remove all three suffixes
(ignore not-exist errors) after the rename.
B5 (HIGH): No SQLite magic-byte validation — any file (txt, corrupt
db, etc.) was accepted as a 'database' and silently overwrote the
live DB, then the panel boot-looped on the bad file. Fix: validate
the 16-byte 'SQLite format 3\0' header before rename; return 400
on mismatch.
B8 (HIGH — data loss): cleanupLocalBackups used OR semantics for
keep+age. With keep=3 + older_than_days=7 and all 3 backups older
than 7 days, ALL 3 were deleted — operator lost every snapshot.
Fix: change to AND semantics. The newest 'keep' entries ALWAYS
survive even if they're older than the age threshold. Age only
applies to entries beyond the keep window.
C2 (HIGH): Telegram sendBackup had no size guard. DB > 50 MB fails
the Bot API sendDocument limit with HTTP 413, and the scheduled
backup silently stopped working forever (error only in journalctl).
Fix: check buf.Len() > 50 MB before sending; if exceeded, send a
text notice to all chats instead ('DB too large to attach: N MB;
download via the web UI').
Also fixed:
- B6/B7: mihomo config restore was best-effort (errors silently
discarded). Now returns proper errors + a MihomoSkipped flag when
the zip contained mihomo-config.yaml but the panel has no
mihomo.config path configured.
- B13: RestoreDatabase MkdirAll mode 0o750 → 0o700 (matches
database.go, DB contains password hashes + secrets).
- B10: frontend restore shows 'Restarting panel…' modal with
auto-reload indicator; surfaces mihomo_config_skipped message.
Tests: 8 new unit tests in backup_test.go covering:
- RestoreDatabase rejects non-SQLite / empty uploads
- RestoreDatabase accepts raw SQLite + removes -wal/-shm/-journal
- RestoreDatabase restores mihomo config from zip
- RestoreDatabase reports MihomoSkipped when path empty
- cleanupLocalBackups keep+age AND semantics (newest N always safe)
- cleanupLocalBackups keep-only / age-only paths
Full suite: 23/23 backend packages PASS, frontend build + lint clean.
Not addressed in this commit (follow-ups):
- B2/C4: WriteZip still reads live DB without VACUUM INTO / Online
Backup API (inconsistent snapshot risk). Needs sqlite driver work.
- B3/C1: backup zip is unencrypted and contains bot_token + 2FA
secrets. Needs passphrase encryption or sanitized dump.
- A1: shell snapshot has no sha256 checksum verification on restore.
- B11: backup-meta.txt missing panel/schema version.
fix(backup): close critical backup/restore bugs
Audit found 5 HIGH-severity backup bugs. Fixed:
B1 (HIGH — silent data loss): RestoreDatabase handler returned 200 OK
but the panel's GORM pool kept writing to the old (unlinked) SQLite
inode. Every write between restore and a manual 'systemctl restart'
was silently lost. Fix: handler now os.Exit(0)s 500ms after responding
so systemd's Restart=always reboots the panel with the new DB. The
frontend polls /api/v1/health and auto-reloads the page when the
panel is back (no more dismissable 'remember to restart' modal).
B4 (HIGH): RestoreDatabase didn't delete stale -journal / -wal / -shm
siblings. SQLite could roll back the just-restored DB to a stale
transaction state on next open. Fix: remove all three suffixes
(ignore not-exist errors) after the rename.
B5 (HIGH): No SQLite magic-byte validation — any file (txt, corrupt
db, etc.) was accepted as a 'database' and silently overwrote the
live DB, then the panel boot-looped on the bad file. Fix: validate
the 16-byte 'SQLite format 3\0' header before rename; return 400
on mismatch.
B8 (HIGH — data loss): cleanupLocalBackups used OR semantics for
keep+age. With keep=3 + older_than_days=7 and all 3 backups older
than 7 days, ALL 3 were deleted — operator lost every snapshot.
Fix: change to AND semantics. The newest 'keep' entries ALWAYS
survive even if they're older than the age threshold. Age only
applies to entries beyond the keep window.
C2 (HIGH): Telegram sendBackup had no size guard. DB > 50 MB fails
the Bot API sendDocument limit with HTTP 413, and the scheduled
backup silently stopped working forever (error only in journalctl).
Fix: check buf.Len() > 50 MB before sending; if exceeded, send a
text notice to all chats instead ('DB too large to attach: N MB;
download via the web UI').
Also fixed:
- B6/B7: mihomo config restore was best-effort (errors silently
discarded). Now returns proper errors + a MihomoSkipped flag when
the zip contained mihomo-config.yaml but the panel has no
mihomo.config path configured.
- B13: RestoreDatabase MkdirAll mode 0o750 → 0o700 (matches
database.go, DB contains password hashes + secrets).
- B10: frontend restore shows 'Restarting panel…' modal with
auto-reload indicator; surfaces mihomo_config_skipped message.
Tests: 8 new unit tests in backup_test.go covering:
- RestoreDatabase rejects non-SQLite / empty uploads
- RestoreDatabase accepts raw SQLite + removes -wal/-shm/-journal
- RestoreDatabase restores mihomo config from zip
- RestoreDatabase reports MihomoSkipped when path empty
- cleanupLocalBackups keep+age AND semantics (newest N always safe)
- cleanupLocalBackups keep-only / age-only paths
Full suite: 23/23 backend packages PASS, frontend build + lint clean.
Not addressed in this commit (follow-ups):
- B2/C4: WriteZip still reads live DB without VACUUM INTO / Online
Backup API (inconsistent snapshot risk). Needs sqlite driver work.
- B3/C1: backup zip is unencrypted and contains bot_token + 2FA
secrets. Needs passphrase encryption or sanitized dump.
- A1: shell snapshot has no sha256 checksum verification on restore.
- B11: backup-meta.txt missing panel/schema version.
fix(backup): close critical backup/restore bugs
Audit found 5 HIGH-severity backup bugs. Fixed:
B1 (HIGH — silent data loss): RestoreDatabase handler returned 200 OK
but the panel's GORM pool kept writing to the old (unlinked) SQLite
inode. Every write between restore and a manual 'systemctl restart'
was silently lost. Fix: handler now os.Exit(0)s 500ms after responding
so systemd's Restart=always reboots the panel with the new DB. The
frontend polls /api/v1/health and auto-reloads the page when the
panel is back (no more dismissable 'remember to restart' modal).
B4 (HIGH): RestoreDatabase didn't delete stale -journal / -wal / -shm
siblings. SQLite could roll back the just-restored DB to a stale
transaction state on next open. Fix: remove all three suffixes
(ignore not-exist errors) after the rename.
B5 (HIGH): No SQLite magic-byte validation — any file (txt, corrupt
db, etc.) was accepted as a 'database' and silently overwrote the
live DB, then the panel boot-looped on the bad file. Fix: validate
the 16-byte 'SQLite format 3\0' header before rename; return 400
on mismatch.
B8 (HIGH — data loss): cleanupLocalBackups used OR semantics for
keep+age. With keep=3 + older_than_days=7 and all 3 backups older
than 7 days, ALL 3 were deleted — operator lost every snapshot.
Fix: change to AND semantics. The newest 'keep' entries ALWAYS
survive even if they're older than the age threshold. Age only
applies to entries beyond the keep window.
C2 (HIGH): Telegram sendBackup had no size guard. DB > 50 MB fails
the Bot API sendDocument limit with HTTP 413, and the scheduled
backup silently stopped working forever (error only in journalctl).
Fix: check buf.Len() > 50 MB before sending; if exceeded, send a
text notice to all chats instead ('DB too large to attach: N MB;
download via the web UI').
Also fixed:
- B6/B7: mihomo config restore was best-effort (errors silently
discarded). Now returns proper errors + a MihomoSkipped flag when
the zip contained mihomo-config.yaml but the panel has no
mihomo.config path configured.
- B13: RestoreDatabase MkdirAll mode 0o750 → 0o700 (matches
database.go, DB contains password hashes + secrets).
- B10: frontend restore shows 'Restarting panel…' modal with
auto-reload indicator; surfaces mihomo_config_skipped message.
Tests: 8 new unit tests in backup_test.go covering:
- RestoreDatabase rejects non-SQLite / empty uploads
- RestoreDatabase accepts raw SQLite + removes -wal/-shm/-journal
- RestoreDatabase restores mihomo config from zip
- RestoreDatabase reports MihomoSkipped when path empty
- cleanupLocalBackups keep+age AND semantics (newest N always safe)
- cleanupLocalBackups keep-only / age-only paths
Full suite: 23/23 backend packages PASS, frontend build + lint clean.
Not addressed in this commit (follow-ups):
- B2/C4: WriteZip still reads live DB without VACUUM INTO / Online
Backup API (inconsistent snapshot risk). Needs sqlite driver work.
- B3/C1: backup zip is unencrypted and contains bot_token + 2FA
secrets. Needs passphrase encryption or sanitized dump.
- A1: shell snapshot has no sha256 checksum verification on restore.
- B11: backup-meta.txt missing panel/schema version.
fix(backup): close critical backup/restore bugs
Audit found 5 HIGH-severity backup bugs. Fixed:
B1 (HIGH — silent data loss): RestoreDatabase handler returned 200 OK
but the panel's GORM pool kept writing to the old (unlinked) SQLite
inode. Every write between restore and a manual 'systemctl restart'
was silently lost. Fix: handler now os.Exit(0)s 500ms after responding
so systemd's Restart=always reboots the panel with the new DB. The
frontend polls /api/v1/health and auto-reloads the page when the
panel is back (no more dismissable 'remember to restart' modal).
B4 (HIGH): RestoreDatabase didn't delete stale -journal / -wal / -shm
siblings. SQLite could roll back the just-restored DB to a stale
transaction state on next open. Fix: remove all three suffixes
(ignore not-exist errors) after the rename.
B5 (HIGH): No SQLite magic-byte validation — any file (txt, corrupt
db, etc.) was accepted as a 'database' and silently overwrote the
live DB, then the panel boot-looped on the bad file. Fix: validate
the 16-byte 'SQLite format 3\0' header before rename; return 400
on mismatch.
B8 (HIGH — data loss): cleanupLocalBackups used OR semantics for
keep+age. With keep=3 + older_than_days=7 and all 3 backups older
than 7 days, ALL 3 were deleted — operator lost every snapshot.
Fix: change to AND semantics. The newest 'keep' entries ALWAYS
survive even if they're older than the age threshold. Age only
applies to entries beyond the keep window.
C2 (HIGH): Telegram sendBackup had no size guard. DB > 50 MB fails
the Bot API sendDocument limit with HTTP 413, and the scheduled
backup silently stopped working forever (error only in journalctl).
Fix: check buf.Len() > 50 MB before sending; if exceeded, send a
text notice to all chats instead ('DB too large to attach: N MB;
download via the web UI').
Also fixed:
- B6/B7: mihomo config restore was best-effort (errors silently
discarded). Now returns proper errors + a MihomoSkipped flag when
the zip contained mihomo-config.yaml but the panel has no
mihomo.config path configured.
- B13: RestoreDatabase MkdirAll mode 0o750 → 0o700 (matches
database.go, DB contains password hashes + secrets).
- B10: frontend restore shows 'Restarting panel…' modal with
auto-reload indicator; surfaces mihomo_config_skipped message.
Tests: 8 new unit tests in backup_test.go covering:
- RestoreDatabase rejects non-SQLite / empty uploads
- RestoreDatabase accepts raw SQLite + removes -wal/-shm/-journal
- RestoreDatabase restores mihomo config from zip
- RestoreDatabase reports MihomoSkipped when path empty
- cleanupLocalBackups keep+age AND semantics (newest N always safe)
- cleanupLocalBackups keep-only / age-only paths
Full suite: 23/23 backend packages PASS, frontend build + lint clean.
Not addressed in this commit (follow-ups):
- B2/C4: WriteZip still reads live DB without VACUUM INTO / Online
Backup API (inconsistent snapshot risk). Needs sqlite driver work.
- B3/C1: backup zip is unencrypted and contains bot_token + 2FA
secrets. Needs passphrase encryption or sanitized dump.
- A1: shell snapshot has no sha256 checksum verification on restore.
- B11: backup-meta.txt missing panel/schema version.
2026-09-25 07:07:00 +00:00
Z User bffb21e415 feat(scripts): add reset-config to interactive menu + CLI entrypoint
The 3m-ui management entrypoint (/usr/local/bin/3m-ui) had an
interactive menu but no reset-config option — operators had to know
the CLI flag syntax to SSH rescue a panel. Now both paths work:
INTERACTIVE MENU (3m-ui with no args)
Select option 16 → SSH 救援 / SSH Rescue sub-menu:
1) 仅面板 (listen + SSL)              [most common]
2) 面板 + 订阅地址 (public_url)
3) 全量重置 (panel + public + access_profile)
0) 返回
→ y/N 确认 → 自动 stop/run binary/start
CLI ENTRYPOINT
3m-ui reset-config --panel --yes     # scripted rescue
3m-ui reset-config --all --yes        # full reset
3m-ui reset-config --help             # help (no service touched)
3m-ui help                            # shows SSH rescue examples
CHANGES
scripts/3m-ui:
- New reset_config() function with interactive scope picker
- run_app() extended to pass-through extra args (was single-arg only)
- Menu: insert option 16 'SSH 救援重置面板配置', renumber 17-23
- main() case: add 'reset-config' subcommand with stop/run/start
cycle; --help short-circuits (read-only, no service touched)
- help text: add SSH rescue examples
Smoke-tested: dispatch + flag passthrough + scope picker + y/N
confirmation all work via fake binary; --help does not stop service.
feat(scripts): add reset-config to interactive menu + CLI entrypoint
The 3m-ui management entrypoint (/usr/local/bin/3m-ui) had an
interactive menu but no reset-config option — operators had to know
the CLI flag syntax to SSH rescue a panel. Now both paths work:
INTERACTIVE MENU (3m-ui with no args)
Select option 16 → SSH 救援 / SSH Rescue sub-menu:
1) 仅面板 (listen + SSL)              [most common]
2) 面板 + 订阅地址 (public_url)
3) 全量重置 (panel + public + access_profile)
0) 返回
→ y/N 确认 → 自动 stop/run binary/start
CLI ENTRYPOINT
3m-ui reset-config --panel --yes     # scripted rescue
3m-ui reset-config --all --yes        # full reset
3m-ui reset-config --help             # help (no service touched)
3m-ui help                            # shows SSH rescue examples
CHANGES
scripts/3m-ui:
- New reset_config() function with interactive scope picker
- run_app() extended to pass-through extra args (was single-arg only)
- Menu: insert option 16 'SSH 救援重置面板配置', renumber 17-23
- main() case: add 'reset-config' subcommand with stop/run/start
cycle; --help short-circuits (read-only, no service touched)
- help text: add SSH rescue examples
Smoke-tested: dispatch + flag passthrough + scope picker + y/N
confirmation all work via fake binary; --help does not stop service.
feat(scripts): add reset-config to interactive menu + CLI entrypoint
The 3m-ui management entrypoint (/usr/local/bin/3m-ui) had an
interactive menu but no reset-config option — operators had to know
the CLI flag syntax to SSH rescue a panel. Now both paths work:
INTERACTIVE MENU (3m-ui with no args)
Select option 16 → SSH 救援 / SSH Rescue sub-menu:
1) 仅面板 (listen + SSL)              [most common]
2) 面板 + 订阅地址 (public_url)
3) 全量重置 (panel + public + access_profile)
0) 返回
→ y/N 确认 → 自动 stop/run binary/start
CLI ENTRYPOINT
3m-ui reset-config --panel --yes     # scripted rescue
3m-ui reset-config --all --yes        # full reset
3m-ui reset-config --help             # help (no service touched)
3m-ui help                            # shows SSH rescue examples
CHANGES
scripts/3m-ui:
- New reset_config() function with interactive scope picker
- run_app() extended to pass-through extra args (was single-arg only)
- Menu: insert option 16 'SSH 救援重置面板配置', renumber 17-23
- main() case: add 'reset-config' subcommand with stop/run/start
cycle; --help short-circuits (read-only, no service touched)
- help text: add SSH rescue examples
Smoke-tested: dispatch + flag passthrough + scope picker + y/N
confirmation all work via fake binary; --help does not stop service.
feat(scripts): add reset-config to interactive menu + CLI entrypoint
The 3m-ui management entrypoint (/usr/local/bin/3m-ui) had an
interactive menu but no reset-config option — operators had to know
the CLI flag syntax to SSH rescue a panel. Now both paths work:
INTERACTIVE MENU (3m-ui with no args)
Select option 16 → SSH 救援 / SSH Rescue sub-menu:
1) 仅面板 (listen + SSL)              [most common]
2) 面板 + 订阅地址 (public_url)
3) 全量重置 (panel + public + access_profile)
0) 返回
→ y/N 确认 → 自动 stop/run binary/start
CLI ENTRYPOINT
3m-ui reset-config --panel --yes     # scripted rescue
3m-ui reset-config --all --yes        # full reset
3m-ui reset-config --help             # help (no service touched)
3m-ui help                            # shows SSH rescue examples
CHANGES
scripts/3m-ui:
- New reset_config() function with interactive scope picker
- run_app() extended to pass-through extra args (was single-arg only)
- Menu: insert option 16 'SSH 救援重置面板配置', renumber 17-23
- main() case: add 'reset-config' subcommand with stop/run/start
cycle; --help short-circuits (read-only, no service touched)
- help text: add SSH rescue examples
Smoke-tested: dispatch + flag passthrough + scope picker + y/N
confirmation all work via fake binary; --help does not stop service.
feat(scripts): add reset-config to interactive menu + CLI entrypoint
The 3m-ui management entrypoint (/usr/local/bin/3m-ui) had an
interactive menu but no reset-config option — operators had to know
the CLI flag syntax to SSH rescue a panel. Now both paths work:
INTERACTIVE MENU (3m-ui with no args)
Select option 16 → SSH 救援 / SSH Rescue sub-menu:
1) 仅面板 (listen + SSL)              [most common]
2) 面板 + 订阅地址 (public_url)
3) 全量重置 (panel + public + access_profile)
0) 返回
→ y/N 确认 → 自动 stop/run binary/start
CLI ENTRYPOINT
3m-ui reset-config --panel --yes     # scripted rescue
3m-ui reset-config --all --yes        # full reset
3m-ui reset-config --help             # help (no service touched)
3m-ui help                            # shows SSH rescue examples
CHANGES
scripts/3m-ui:
- New reset_config() function with interactive scope picker
- run_app() extended to pass-through extra args (was single-arg only)
- Menu: insert option 16 'SSH 救援重置面板配置', renumber 17-23
- main() case: add 'reset-config' subcommand with stop/run/start
cycle; --help short-circuits (read-only, no service touched)
- help text: add SSH rescue examples
Smoke-tested: dispatch + flag passthrough + scope picker + y/N
confirmation all work via fake binary; --help does not stop service.
feat(scripts): add reset-config to interactive menu + CLI entrypoint
The 3m-ui management entrypoint (/usr/local/bin/3m-ui) had an
interactive menu but no reset-config option — operators had to know
the CLI flag syntax to SSH rescue a panel. Now both paths work:
INTERACTIVE MENU (3m-ui with no args)
Select option 16 → SSH 救援 / SSH Rescue sub-menu:
1) 仅面板 (listen + SSL)              [most common]
2) 面板 + 订阅地址 (public_url)
3) 全量重置 (panel + public + access_profile)
0) 返回
→ y/N 确认 → 自动 stop/run binary/start
CLI ENTRYPOINT
3m-ui reset-config --panel --yes     # scripted rescue
3m-ui reset-config --all --yes        # full reset
3m-ui reset-config --help             # help (no service touched)
3m-ui help                            # shows SSH rescue examples
CHANGES
scripts/3m-ui:
- New reset_config() function with interactive scope picker
- run_app() extended to pass-through extra args (was single-arg only)
- Menu: insert option 16 'SSH 救援重置面板配置', renumber 17-23
- main() case: add 'reset-config' subcommand with stop/run/start
cycle; --help short-circuits (read-only, no service touched)
- help text: add SSH rescue examples
Smoke-tested: dispatch + flag passthrough + scope picker + y/N
confirmation all work via fake binary; --help does not stop service.
feat(scripts): add reset-config to interactive menu + CLI entrypoint
The 3m-ui management entrypoint (/usr/local/bin/3m-ui) had an
interactive menu but no reset-config option — operators had to know
the CLI flag syntax to SSH rescue a panel. Now both paths work:
INTERACTIVE MENU (3m-ui with no args)
Select option 16 → SSH 救援 / SSH Rescue sub-menu:
1) 仅面板 (listen + SSL)              [most common]
2) 面板 + 订阅地址 (public_url)
3) 全量重置 (panel + public + access_profile)
0) 返回
→ y/N 确认 → 自动 stop/run binary/start
CLI ENTRYPOINT
3m-ui reset-config --panel --yes     # scripted rescue
3m-ui reset-config --all --yes        # full reset
3m-ui reset-config --help             # help (no service touched)
3m-ui help                            # shows SSH rescue examples
CHANGES
scripts/3m-ui:
- New reset_config() function with interactive scope picker
- run_app() extended to pass-through extra args (was single-arg only)
- Menu: insert option 16 'SSH 救援重置面板配置', renumber 17-23
- main() case: add 'reset-config' subcommand with stop/run/start
cycle; --help short-circuits (read-only, no service touched)
- help text: add SSH rescue examples
Smoke-tested: dispatch + flag passthrough + scope picker + y/N
confirmation all work via fake binary; --help does not stop service.
feat(scripts): add reset-config to interactive menu + CLI entrypoint
The 3m-ui management entrypoint (/usr/local/bin/3m-ui) had an
interactive menu but no reset-config option — operators had to know
the CLI flag syntax to SSH rescue a panel. Now both paths work:
INTERACTIVE MENU (3m-ui with no args)
Select option 16 → SSH 救援 / SSH Rescue sub-menu:
1) 仅面板 (listen + SSL)              [most common]
2) 面板 + 订阅地址 (public_url)
3) 全量重置 (panel + public + access_profile)
0) 返回
→ y/N 确认 → 自动 stop/run binary/start
CLI ENTRYPOINT
3m-ui reset-config --panel --yes     # scripted rescue
3m-ui reset-config --all --yes        # full reset
3m-ui reset-config --help             # help (no service touched)
3m-ui help                            # shows SSH rescue examples
CHANGES
scripts/3m-ui:
- New reset_config() function with interactive scope picker
- run_app() extended to pass-through extra args (was single-arg only)
- Menu: insert option 16 'SSH 救援重置面板配置', renumber 17-23
- main() case: add 'reset-config' subcommand with stop/run/start
cycle; --help short-circuits (read-only, no service touched)
- help text: add SSH rescue examples
Smoke-tested: dispatch + flag passthrough + scope picker + y/N
confirmation all work via fake binary; --help does not stop service.
feat(scripts): add reset-config to interactive menu + CLI entrypoint
The 3m-ui management entrypoint (/usr/local/bin/3m-ui) had an
interactive menu but no reset-config option — operators had to know
the CLI flag syntax to SSH rescue a panel. Now both paths work:
INTERACTIVE MENU (3m-ui with no args)
Select option 16 → SSH 救援 / SSH Rescue sub-menu:
1) 仅面板 (listen + SSL)              [most common]
2) 面板 + 订阅地址 (public_url)
3) 全量重置 (panel + public + access_profile)
0) 返回
→ y/N 确认 → 自动 stop/run binary/start
CLI ENTRYPOINT
3m-ui reset-config --panel --yes     # scripted rescue
3m-ui reset-config --all --yes        # full reset
3m-ui reset-config --help             # help (no service touched)
3m-ui help                            # shows SSH rescue examples
CHANGES
scripts/3m-ui:
- New reset_config() function with interactive scope picker
- run_app() extended to pass-through extra args (was single-arg only)
- Menu: insert option 16 'SSH 救援重置面板配置', renumber 17-23
- main() case: add 'reset-config' subcommand with stop/run/start
cycle; --help short-circuits (read-only, no service touched)
- help text: add SSH rescue examples
Smoke-tested: dispatch + flag passthrough + scope picker + y/N
confirmation all work via fake binary; --help does not stop service.
feat(scripts): add reset-config to interactive menu + CLI entrypoint
The 3m-ui management entrypoint (/usr/local/bin/3m-ui) had an
interactive menu but no reset-config option — operators had to know
the CLI flag syntax to SSH rescue a panel. Now both paths work:
INTERACTIVE MENU (3m-ui with no args)
Select option 16 → SSH 救援 / SSH Rescue sub-menu:
1) 仅面板 (listen + SSL)              [most common]
2) 面板 + 订阅地址 (public_url)
3) 全量重置 (panel + public + access_profile)
0) 返回
→ y/N 确认 → 自动 stop/run binary/start
CLI ENTRYPOINT
3m-ui reset-config --panel --yes     # scripted rescue
3m-ui reset-config --all --yes        # full reset
3m-ui reset-config --help             # help (no service touched)
3m-ui help                            # shows SSH rescue examples
CHANGES
scripts/3m-ui:
- New reset_config() function with interactive scope picker
- run_app() extended to pass-through extra args (was single-arg only)
- Menu: insert option 16 'SSH 救援重置面板配置', renumber 17-23
- main() case: add 'reset-config' subcommand with stop/run/start
cycle; --help short-circuits (read-only, no service touched)
- help text: add SSH rescue examples
Smoke-tested: dispatch + flag passthrough + scope picker + y/N
confirmation all work via fake binary; --help does not stop service.
feat(scripts): add reset-config to interactive menu + CLI entrypoint
The 3m-ui management entrypoint (/usr/local/bin/3m-ui) had an
interactive menu but no reset-config option — operators had to know
the CLI flag syntax to SSH rescue a panel. Now both paths work:
INTERACTIVE MENU (3m-ui with no args)
Select option 16 → SSH 救援 / SSH Rescue sub-menu:
1) 仅面板 (listen + SSL)              [most common]
2) 面板 + 订阅地址 (public_url)
3) 全量重置 (panel + public + access_profile)
0) 返回
→ y/N 确认 → 自动 stop/run binary/start
CLI ENTRYPOINT
3m-ui reset-config --panel --yes     # scripted rescue
3m-ui reset-config --all --yes        # full reset
3m-ui reset-config --help             # help (no service touched)
3m-ui help                            # shows SSH rescue examples
CHANGES
scripts/3m-ui:
- New reset_config() function with interactive scope picker
- run_app() extended to pass-through extra args (was single-arg only)
- Menu: insert option 16 'SSH 救援重置面板配置', renumber 17-23
- main() case: add 'reset-config' subcommand with stop/run/start
cycle; --help short-circuits (read-only, no service touched)
- help text: add SSH rescue examples
Smoke-tested: dispatch + flag passthrough + scope picker + y/N
confirmation all work via fake binary; --help does not stop service.
feat(scripts): add reset-config to interactive menu + CLI entrypoint
The 3m-ui management entrypoint (/usr/local/bin/3m-ui) had an
interactive menu but no reset-config option — operators had to know
the CLI flag syntax to SSH rescue a panel. Now both paths work:
INTERACTIVE MENU (3m-ui with no args)
Select option 16 → SSH 救援 / SSH Rescue sub-menu:
1) 仅面板 (listen + SSL)              [most common]
2) 面板 + 订阅地址 (public_url)
3) 全量重置 (panel + public + access_profile)
0) 返回
→ y/N 确认 → 自动 stop/run binary/start
CLI ENTRYPOINT
3m-ui reset-config --panel --yes     # scripted rescue
3m-ui reset-config --all --yes        # full reset
3m-ui reset-config --help             # help (no service touched)
3m-ui help                            # shows SSH rescue examples
CHANGES
scripts/3m-ui:
- New reset_config() function with interactive scope picker
- run_app() extended to pass-through extra args (was single-arg only)
- Menu: insert option 16 'SSH 救援重置面板配置', renumber 17-23
- main() case: add 'reset-config' subcommand with stop/run/start
cycle; --help short-circuits (read-only, no service touched)
- help text: add SSH rescue examples
Smoke-tested: dispatch + flag passthrough + scope picker + y/N
confirmation all work via fake binary; --help does not stop service.
2026-09-25 06:26:55 +00:00
Z User 00249cd168 feat(scripts): wire reset-config into 3m-ui CLI wrapper + install hint
The new '3m-ui reset-config' Go subcommand (commit 8639b01) lets SSH
operators undo panel misconfigurations (wrong port / 127.0.0.1 bind /
broken SSL). This change exposes it through the systemd management
wrapper and documents it in the install banner.
scripts/3m-ui.sh:
- Add 'reset_config' function: stops the panel service (required by
the Go binary since it writes config.yaml + DB rows the running
panel might cache), invokes 3m-ui-bin reset-config <flags>, then
restarts the panel.
- --help / -h short-circuits the stop/restart cycle (read-only).
- Add 'reset-config' to the main case dispatch.
- Document the new subcommand + flags in usage() with examples.
scripts/install.sh:
- Append a line to the post-install banner pointing operators at
'3m-ui reset-config --panel --yes' for SSH rescue scenarios.
USAGE
sudo 3m-ui reset-config                       # interactive y/N, panel scope
sudo 3m-ui reset-config --panel --yes        # scripted rescue (common case)
sudo 3m-ui reset-config --all --yes          # full reset
sudo 3m-ui reset-config --help               # full help (no service touched)
Smoke-tested with a fake binary: dispatch + flag passthrough + env vars
all correct; --help does not stop the service.
feat(scripts): wire reset-config into 3m-ui CLI wrapper + install hint
The new '3m-ui reset-config' Go subcommand (commit 8639b01) lets SSH
operators undo panel misconfigurations (wrong port / 127.0.0.1 bind /
broken SSL). This change exposes it through the systemd management
wrapper and documents it in the install banner.
scripts/3m-ui.sh:
- Add 'reset_config' function: stops the panel service (required by
the Go binary since it writes config.yaml + DB rows the running
panel might cache), invokes 3m-ui-bin reset-config <flags>, then
restarts the panel.
- --help / -h short-circuits the stop/restart cycle (read-only).
- Add 'reset-config' to the main case dispatch.
- Document the new subcommand + flags in usage() with examples.
scripts/install.sh:
- Append a line to the post-install banner pointing operators at
'3m-ui reset-config --panel --yes' for SSH rescue scenarios.
USAGE
sudo 3m-ui reset-config                       # interactive y/N, panel scope
sudo 3m-ui reset-config --panel --yes        # scripted rescue (common case)
sudo 3m-ui reset-config --all --yes          # full reset
sudo 3m-ui reset-config --help               # full help (no service touched)
Smoke-tested with a fake binary: dispatch + flag passthrough + env vars
all correct; --help does not stop the service.
feat(scripts): wire reset-config into 3m-ui CLI wrapper + install hint
The new '3m-ui reset-config' Go subcommand (commit 8639b01) lets SSH
operators undo panel misconfigurations (wrong port / 127.0.0.1 bind /
broken SSL). This change exposes it through the systemd management
wrapper and documents it in the install banner.
scripts/3m-ui.sh:
- Add 'reset_config' function: stops the panel service (required by
the Go binary since it writes config.yaml + DB rows the running
panel might cache), invokes 3m-ui-bin reset-config <flags>, then
restarts the panel.
- --help / -h short-circuits the stop/restart cycle (read-only).
- Add 'reset-config' to the main case dispatch.
- Document the new subcommand + flags in usage() with examples.
scripts/install.sh:
- Append a line to the post-install banner pointing operators at
'3m-ui reset-config --panel --yes' for SSH rescue scenarios.
USAGE
sudo 3m-ui reset-config                       # interactive y/N, panel scope
sudo 3m-ui reset-config --panel --yes        # scripted rescue (common case)
sudo 3m-ui reset-config --all --yes          # full reset
sudo 3m-ui reset-config --help               # full help (no service touched)
Smoke-tested with a fake binary: dispatch + flag passthrough + env vars
all correct; --help does not stop the service.
feat(scripts): wire reset-config into 3m-ui CLI wrapper + install hint
The new '3m-ui reset-config' Go subcommand (commit 8639b01) lets SSH
operators undo panel misconfigurations (wrong port / 127.0.0.1 bind /
broken SSL). This change exposes it through the systemd management
wrapper and documents it in the install banner.
scripts/3m-ui.sh:
- Add 'reset_config' function: stops the panel service (required by
the Go binary since it writes config.yaml + DB rows the running
panel might cache), invokes 3m-ui-bin reset-config <flags>, then
restarts the panel.
- --help / -h short-circuits the stop/restart cycle (read-only).
- Add 'reset-config' to the main case dispatch.
- Document the new subcommand + flags in usage() with examples.
scripts/install.sh:
- Append a line to the post-install banner pointing operators at
'3m-ui reset-config --panel --yes' for SSH rescue scenarios.
USAGE
sudo 3m-ui reset-config                       # interactive y/N, panel scope
sudo 3m-ui reset-config --panel --yes        # scripted rescue (common case)
sudo 3m-ui reset-config --all --yes          # full reset
sudo 3m-ui reset-config --help               # full help (no service touched)
Smoke-tested with a fake binary: dispatch + flag passthrough + env vars
all correct; --help does not stop the service.
feat(scripts): wire reset-config into 3m-ui CLI wrapper + install hint
The new '3m-ui reset-config' Go subcommand (commit 8639b01) lets SSH
operators undo panel misconfigurations (wrong port / 127.0.0.1 bind /
broken SSL). This change exposes it through the systemd management
wrapper and documents it in the install banner.
scripts/3m-ui.sh:
- Add 'reset_config' function: stops the panel service (required by
the Go binary since it writes config.yaml + DB rows the running
panel might cache), invokes 3m-ui-bin reset-config <flags>, then
restarts the panel.
- --help / -h short-circuits the stop/restart cycle (read-only).
- Add 'reset-config' to the main case dispatch.
- Document the new subcommand + flags in usage() with examples.
scripts/install.sh:
- Append a line to the post-install banner pointing operators at
'3m-ui reset-config --panel --yes' for SSH rescue scenarios.
USAGE
sudo 3m-ui reset-config                       # interactive y/N, panel scope
sudo 3m-ui reset-config --panel --yes        # scripted rescue (common case)
sudo 3m-ui reset-config --all --yes          # full reset
sudo 3m-ui reset-config --help               # full help (no service touched)
Smoke-tested with a fake binary: dispatch + flag passthrough + env vars
all correct; --help does not stop the service.
feat(scripts): wire reset-config into 3m-ui CLI wrapper + install hint
The new '3m-ui reset-config' Go subcommand (commit 8639b01) lets SSH
operators undo panel misconfigurations (wrong port / 127.0.0.1 bind /
broken SSL). This change exposes it through the systemd management
wrapper and documents it in the install banner.
scripts/3m-ui.sh:
- Add 'reset_config' function: stops the panel service (required by
the Go binary since it writes config.yaml + DB rows the running
panel might cache), invokes 3m-ui-bin reset-config <flags>, then
restarts the panel.
- --help / -h short-circuits the stop/restart cycle (read-only).
- Add 'reset-config' to the main case dispatch.
- Document the new subcommand + flags in usage() with examples.
scripts/install.sh:
- Append a line to the post-install banner pointing operators at
'3m-ui reset-config --panel --yes' for SSH rescue scenarios.
USAGE
sudo 3m-ui reset-config                       # interactive y/N, panel scope
sudo 3m-ui reset-config --panel --yes        # scripted rescue (common case)
sudo 3m-ui reset-config --all --yes          # full reset
sudo 3m-ui reset-config --help               # full help (no service touched)
Smoke-tested with a fake binary: dispatch + flag passthrough + env vars
all correct; --help does not stop the service.
feat(scripts): wire reset-config into 3m-ui CLI wrapper + install hint
The new '3m-ui reset-config' Go subcommand (commit 8639b01) lets SSH
operators undo panel misconfigurations (wrong port / 127.0.0.1 bind /
broken SSL). This change exposes it through the systemd management
wrapper and documents it in the install banner.
scripts/3m-ui.sh:
- Add 'reset_config' function: stops the panel service (required by
the Go binary since it writes config.yaml + DB rows the running
panel might cache), invokes 3m-ui-bin reset-config <flags>, then
restarts the panel.
- --help / -h short-circuits the stop/restart cycle (read-only).
- Add 'reset-config' to the main case dispatch.
- Document the new subcommand + flags in usage() with examples.
scripts/install.sh:
- Append a line to the post-install banner pointing operators at
'3m-ui reset-config --panel --yes' for SSH rescue scenarios.
USAGE
sudo 3m-ui reset-config                       # interactive y/N, panel scope
sudo 3m-ui reset-config --panel --yes        # scripted rescue (common case)
sudo 3m-ui reset-config --all --yes          # full reset
sudo 3m-ui reset-config --help               # full help (no service touched)
Smoke-tested with a fake binary: dispatch + flag passthrough + env vars
all correct; --help does not stop the service.
feat(scripts): wire reset-config into 3m-ui CLI wrapper + install hint
The new '3m-ui reset-config' Go subcommand (commit 8639b01) lets SSH
operators undo panel misconfigurations (wrong port / 127.0.0.1 bind /
broken SSL). This change exposes it through the systemd management
wrapper and documents it in the install banner.
scripts/3m-ui.sh:
- Add 'reset_config' function: stops the panel service (required by
the Go binary since it writes config.yaml + DB rows the running
panel might cache), invokes 3m-ui-bin reset-config <flags>, then
restarts the panel.
- --help / -h short-circuits the stop/restart cycle (read-only).
- Add 'reset-config' to the main case dispatch.
- Document the new subcommand + flags in usage() with examples.
scripts/install.sh:
- Append a line to the post-install banner pointing operators at
'3m-ui reset-config --panel --yes' for SSH rescue scenarios.
USAGE
sudo 3m-ui reset-config                       # interactive y/N, panel scope
sudo 3m-ui reset-config --panel --yes        # scripted rescue (common case)
sudo 3m-ui reset-config --all --yes          # full reset
sudo 3m-ui reset-config --help               # full help (no service touched)
Smoke-tested with a fake binary: dispatch + flag passthrough + env vars
all correct; --help does not stop the service.
feat(scripts): wire reset-config into 3m-ui CLI wrapper + install hint
The new '3m-ui reset-config' Go subcommand (commit 8639b01) lets SSH
operators undo panel misconfigurations (wrong port / 127.0.0.1 bind /
broken SSL). This change exposes it through the systemd management
wrapper and documents it in the install banner.
scripts/3m-ui.sh:
- Add 'reset_config' function: stops the panel service (required by
the Go binary since it writes config.yaml + DB rows the running
panel might cache), invokes 3m-ui-bin reset-config <flags>, then
restarts the panel.
- --help / -h short-circuits the stop/restart cycle (read-only).
- Add 'reset-config' to the main case dispatch.
- Document the new subcommand + flags in usage() with examples.
scripts/install.sh:
- Append a line to the post-install banner pointing operators at
'3m-ui reset-config --panel --yes' for SSH rescue scenarios.
USAGE
sudo 3m-ui reset-config                       # interactive y/N, panel scope
sudo 3m-ui reset-config --panel --yes        # scripted rescue (common case)
sudo 3m-ui reset-config --all --yes          # full reset
sudo 3m-ui reset-config --help               # full help (no service touched)
Smoke-tested with a fake binary: dispatch + flag passthrough + env vars
all correct; --help does not stop the service.
feat(scripts): wire reset-config into 3m-ui CLI wrapper + install hint
The new '3m-ui reset-config' Go subcommand (commit 8639b01) lets SSH
operators undo panel misconfigurations (wrong port / 127.0.0.1 bind /
broken SSL). This change exposes it through the systemd management
wrapper and documents it in the install banner.
scripts/3m-ui.sh:
- Add 'reset_config' function: stops the panel service (required by
the Go binary since it writes config.yaml + DB rows the running
panel might cache), invokes 3m-ui-bin reset-config <flags>, then
restarts the panel.
- --help / -h short-circuits the stop/restart cycle (read-only).
- Add 'reset-config' to the main case dispatch.
- Document the new subcommand + flags in usage() with examples.
scripts/install.sh:
- Append a line to the post-install banner pointing operators at
'3m-ui reset-config --panel --yes' for SSH rescue scenarios.
USAGE
sudo 3m-ui reset-config                       # interactive y/N, panel scope
sudo 3m-ui reset-config --panel --yes        # scripted rescue (common case)
sudo 3m-ui reset-config --all --yes          # full reset
sudo 3m-ui reset-config --help               # full help (no service touched)
Smoke-tested with a fake binary: dispatch + flag passthrough + env vars
all correct; --help does not stop the service.
feat(scripts): wire reset-config into 3m-ui CLI wrapper + install hint
The new '3m-ui reset-config' Go subcommand (commit 8639b01) lets SSH
operators undo panel misconfigurations (wrong port / 127.0.0.1 bind /
broken SSL). This change exposes it through the systemd management
wrapper and documents it in the install banner.
scripts/3m-ui.sh:
- Add 'reset_config' function: stops the panel service (required by
the Go binary since it writes config.yaml + DB rows the running
panel might cache), invokes 3m-ui-bin reset-config <flags>, then
restarts the panel.
- --help / -h short-circuits the stop/restart cycle (read-only).
- Add 'reset-config' to the main case dispatch.
- Document the new subcommand + flags in usage() with examples.
scripts/install.sh:
- Append a line to the post-install banner pointing operators at
'3m-ui reset-config --panel --yes' for SSH rescue scenarios.
USAGE
sudo 3m-ui reset-config                       # interactive y/N, panel scope
sudo 3m-ui reset-config --panel --yes        # scripted rescue (common case)
sudo 3m-ui reset-config --all --yes          # full reset
sudo 3m-ui reset-config --help               # full help (no service touched)
Smoke-tested with a fake binary: dispatch + flag passthrough + env vars
all correct; --help does not stop the service.
feat(scripts): wire reset-config into 3m-ui CLI wrapper + install hint
The new '3m-ui reset-config' Go subcommand (commit 8639b01) lets SSH
operators undo panel misconfigurations (wrong port / 127.0.0.1 bind /
broken SSL). This change exposes it through the systemd management
wrapper and documents it in the install banner.
scripts/3m-ui.sh:
- Add 'reset_config' function: stops the panel service (required by
the Go binary since it writes config.yaml + DB rows the running
panel might cache), invokes 3m-ui-bin reset-config <flags>, then
restarts the panel.
- --help / -h short-circuits the stop/restart cycle (read-only).
- Add 'reset-config' to the main case dispatch.
- Document the new subcommand + flags in usage() with examples.
scripts/install.sh:
- Append a line to the post-install banner pointing operators at
'3m-ui reset-config --panel --yes' for SSH rescue scenarios.
USAGE
sudo 3m-ui reset-config                       # interactive y/N, panel scope
sudo 3m-ui reset-config --panel --yes        # scripted rescue (common case)
sudo 3m-ui reset-config --all --yes          # full reset
sudo 3m-ui reset-config --help               # full help (no service touched)
Smoke-tested with a fake binary: dispatch + flag passthrough + env vars
all correct; --help does not stop the service.
2026-09-25 06:22:39 +00:00
Z User 42557d4384 feat(cli): reset-config subcommand for SSH rescue
Adds a new '3m-ui reset-config' CLI subcommand that lets operators undo
panel configuration changes via SSH when the web UI is unreachable
(e.g. panel bound to 127.0.0.1, wrong port, broken SSL config).
USAGE
3m-ui reset-config [--panel] [--access] [--public] [--all] [--yes]
SCOPE FLAGS (default: --panel if none given)
--panel    Clear server.listen (= ':<port>') + disable SSL/ACME
→ panel boots plain HTTP on next restart. Port preserved.
--access   Clear listener.public_host / access_sni / access_alpn
→ nodes fall back to their bind address.
--public   Clear server.public_url
→ subscription links fall back to request host.
--all      Apply all three scopes above.
--yes / -y Skip interactive y/N confirmation (for rescue scripts).
AFTER RESET
sudo systemctl restart 3m-ui
Open http://<server-ip>:<port>  (HTTP, not HTTPS)
Re-apply settings from the web UI.
SAFETY
- Never deletes users, listeners, traffic records, or credentials.
- Preserves server.port (so SSH rescue doesn't require re-reading YAML).
- SSL disabled by flipping Enabled flag only — Domain / cert paths kept
so operator can re-enable from UI after fixing underlying issue.
- Interactive y/N prompt by default; --yes for scripted rescue.
FILES ADDED
backend/cmd/server/reset_config.go      (~210 LOC)
backend/cmd/server/reset_config_test.go (~110 LOC, 5 tests)
FILES MODIFIED
backend/cmd/server/main.go             (wire reset-config subcommand)
backend/internal/config/config.go      (+ClearListenInFile +ClearPublicURLInFile helpers)
Tests: 23/23 backend packages PASS.
feat(cli): reset-config subcommand for SSH rescue
Adds a new '3m-ui reset-config' CLI subcommand that lets operators undo
panel configuration changes via SSH when the web UI is unreachable
(e.g. panel bound to 127.0.0.1, wrong port, broken SSL config).
USAGE
3m-ui reset-config [--panel] [--access] [--public] [--all] [--yes]
SCOPE FLAGS (default: --panel if none given)
--panel    Clear server.listen (= ':<port>') + disable SSL/ACME
→ panel boots plain HTTP on next restart. Port preserved.
--access   Clear listener.public_host / access_sni / access_alpn
→ nodes fall back to their bind address.
--public   Clear server.public_url
→ subscription links fall back to request host.
--all      Apply all three scopes above.
--yes / -y Skip interactive y/N confirmation (for rescue scripts).
AFTER RESET
sudo systemctl restart 3m-ui
Open http://<server-ip>:<port>  (HTTP, not HTTPS)
Re-apply settings from the web UI.
SAFETY
- Never deletes users, listeners, traffic records, or credentials.
- Preserves server.port (so SSH rescue doesn't require re-reading YAML).
- SSL disabled by flipping Enabled flag only — Domain / cert paths kept
so operator can re-enable from UI after fixing underlying issue.
- Interactive y/N prompt by default; --yes for scripted rescue.
FILES ADDED
backend/cmd/server/reset_config.go      (~210 LOC)
backend/cmd/server/reset_config_test.go (~110 LOC, 5 tests)
FILES MODIFIED
backend/cmd/server/main.go             (wire reset-config subcommand)
backend/internal/config/config.go      (+ClearListenInFile +ClearPublicURLInFile helpers)
Tests: 23/23 backend packages PASS.
feat(cli): reset-config subcommand for SSH rescue
Adds a new '3m-ui reset-config' CLI subcommand that lets operators undo
panel configuration changes via SSH when the web UI is unreachable
(e.g. panel bound to 127.0.0.1, wrong port, broken SSL config).
USAGE
3m-ui reset-config [--panel] [--access] [--public] [--all] [--yes]
SCOPE FLAGS (default: --panel if none given)
--panel    Clear server.listen (= ':<port>') + disable SSL/ACME
→ panel boots plain HTTP on next restart. Port preserved.
--access   Clear listener.public_host / access_sni / access_alpn
→ nodes fall back to their bind address.
--public   Clear server.public_url
→ subscription links fall back to request host.
--all      Apply all three scopes above.
--yes / -y Skip interactive y/N confirmation (for rescue scripts).
AFTER RESET
sudo systemctl restart 3m-ui
Open http://<server-ip>:<port>  (HTTP, not HTTPS)
Re-apply settings from the web UI.
SAFETY
- Never deletes users, listeners, traffic records, or credentials.
- Preserves server.port (so SSH rescue doesn't require re-reading YAML).
- SSL disabled by flipping Enabled flag only — Domain / cert paths kept
so operator can re-enable from UI after fixing underlying issue.
- Interactive y/N prompt by default; --yes for scripted rescue.
FILES ADDED
backend/cmd/server/reset_config.go      (~210 LOC)
backend/cmd/server/reset_config_test.go (~110 LOC, 5 tests)
FILES MODIFIED
backend/cmd/server/main.go             (wire reset-config subcommand)
backend/internal/config/config.go      (+ClearListenInFile +ClearPublicURLInFile helpers)
Tests: 23/23 backend packages PASS.
feat(cli): reset-config subcommand for SSH rescue
Adds a new '3m-ui reset-config' CLI subcommand that lets operators undo
panel configuration changes via SSH when the web UI is unreachable
(e.g. panel bound to 127.0.0.1, wrong port, broken SSL config).
USAGE
3m-ui reset-config [--panel] [--access] [--public] [--all] [--yes]
SCOPE FLAGS (default: --panel if none given)
--panel    Clear server.listen (= ':<port>') + disable SSL/ACME
→ panel boots plain HTTP on next restart. Port preserved.
--access   Clear listener.public_host / access_sni / access_alpn
→ nodes fall back to their bind address.
--public   Clear server.public_url
→ subscription links fall back to request host.
--all      Apply all three scopes above.
--yes / -y Skip interactive y/N confirmation (for rescue scripts).
AFTER RESET
sudo systemctl restart 3m-ui
Open http://<server-ip>:<port>  (HTTP, not HTTPS)
Re-apply settings from the web UI.
SAFETY
- Never deletes users, listeners, traffic records, or credentials.
- Preserves server.port (so SSH rescue doesn't require re-reading YAML).
- SSL disabled by flipping Enabled flag only — Domain / cert paths kept
so operator can re-enable from UI after fixing underlying issue.
- Interactive y/N prompt by default; --yes for scripted rescue.
FILES ADDED
backend/cmd/server/reset_config.go      (~210 LOC)
backend/cmd/server/reset_config_test.go (~110 LOC, 5 tests)
FILES MODIFIED
backend/cmd/server/main.go             (wire reset-config subcommand)
backend/internal/config/config.go      (+ClearListenInFile +ClearPublicURLInFile helpers)
Tests: 23/23 backend packages PASS.
feat(cli): reset-config subcommand for SSH rescue
Adds a new '3m-ui reset-config' CLI subcommand that lets operators undo
panel configuration changes via SSH when the web UI is unreachable
(e.g. panel bound to 127.0.0.1, wrong port, broken SSL config).
USAGE
3m-ui reset-config [--panel] [--access] [--public] [--all] [--yes]
SCOPE FLAGS (default: --panel if none given)
--panel    Clear server.listen (= ':<port>') + disable SSL/ACME
→ panel boots plain HTTP on next restart. Port preserved.
--access   Clear listener.public_host / access_sni / access_alpn
→ nodes fall back to their bind address.
--public   Clear server.public_url
→ subscription links fall back to request host.
--all      Apply all three scopes above.
--yes / -y Skip interactive y/N confirmation (for rescue scripts).
AFTER RESET
sudo systemctl restart 3m-ui
Open http://<server-ip>:<port>  (HTTP, not HTTPS)
Re-apply settings from the web UI.
SAFETY
- Never deletes users, listeners, traffic records, or credentials.
- Preserves server.port (so SSH rescue doesn't require re-reading YAML).
- SSL disabled by flipping Enabled flag only — Domain / cert paths kept
so operator can re-enable from UI after fixing underlying issue.
- Interactive y/N prompt by default; --yes for scripted rescue.
FILES ADDED
backend/cmd/server/reset_config.go      (~210 LOC)
backend/cmd/server/reset_config_test.go (~110 LOC, 5 tests)
FILES MODIFIED
backend/cmd/server/main.go             (wire reset-config subcommand)
backend/internal/config/config.go      (+ClearListenInFile +ClearPublicURLInFile helpers)
Tests: 23/23 backend packages PASS.
feat(cli): reset-config subcommand for SSH rescue
Adds a new '3m-ui reset-config' CLI subcommand that lets operators undo
panel configuration changes via SSH when the web UI is unreachable
(e.g. panel bound to 127.0.0.1, wrong port, broken SSL config).
USAGE
3m-ui reset-config [--panel] [--access] [--public] [--all] [--yes]
SCOPE FLAGS (default: --panel if none given)
--panel    Clear server.listen (= ':<port>') + disable SSL/ACME
→ panel boots plain HTTP on next restart. Port preserved.
--access   Clear listener.public_host / access_sni / access_alpn
→ nodes fall back to their bind address.
--public   Clear server.public_url
→ subscription links fall back to request host.
--all      Apply all three scopes above.
--yes / -y Skip interactive y/N confirmation (for rescue scripts).
AFTER RESET
sudo systemctl restart 3m-ui
Open http://<server-ip>:<port>  (HTTP, not HTTPS)
Re-apply settings from the web UI.
SAFETY
- Never deletes users, listeners, traffic records, or credentials.
- Preserves server.port (so SSH rescue doesn't require re-reading YAML).
- SSL disabled by flipping Enabled flag only — Domain / cert paths kept
so operator can re-enable from UI after fixing underlying issue.
- Interactive y/N prompt by default; --yes for scripted rescue.
FILES ADDED
backend/cmd/server/reset_config.go      (~210 LOC)
backend/cmd/server/reset_config_test.go (~110 LOC, 5 tests)
FILES MODIFIED
backend/cmd/server/main.go             (wire reset-config subcommand)
backend/internal/config/config.go      (+ClearListenInFile +ClearPublicURLInFile helpers)
Tests: 23/23 backend packages PASS.
feat(cli): reset-config subcommand for SSH rescue
Adds a new '3m-ui reset-config' CLI subcommand that lets operators undo
panel configuration changes via SSH when the web UI is unreachable
(e.g. panel bound to 127.0.0.1, wrong port, broken SSL config).
USAGE
3m-ui reset-config [--panel] [--access] [--public] [--all] [--yes]
SCOPE FLAGS (default: --panel if none given)
--panel    Clear server.listen (= ':<port>') + disable SSL/ACME
→ panel boots plain HTTP on next restart. Port preserved.
--access   Clear listener.public_host / access_sni / access_alpn
→ nodes fall back to their bind address.
--public   Clear server.public_url
→ subscription links fall back to request host.
--all      Apply all three scopes above.
--yes / -y Skip interactive y/N confirmation (for rescue scripts).
AFTER RESET
sudo systemctl restart 3m-ui
Open http://<server-ip>:<port>  (HTTP, not HTTPS)
Re-apply settings from the web UI.
SAFETY
- Never deletes users, listeners, traffic records, or credentials.
- Preserves server.port (so SSH rescue doesn't require re-reading YAML).
- SSL disabled by flipping Enabled flag only — Domain / cert paths kept
so operator can re-enable from UI after fixing underlying issue.
- Interactive y/N prompt by default; --yes for scripted rescue.
FILES ADDED
backend/cmd/server/reset_config.go      (~210 LOC)
backend/cmd/server/reset_config_test.go (~110 LOC, 5 tests)
FILES MODIFIED
backend/cmd/server/main.go             (wire reset-config subcommand)
backend/internal/config/config.go      (+ClearListenInFile +ClearPublicURLInFile helpers)
Tests: 23/23 backend packages PASS.
feat(cli): reset-config subcommand for SSH rescue
Adds a new '3m-ui reset-config' CLI subcommand that lets operators undo
panel configuration changes via SSH when the web UI is unreachable
(e.g. panel bound to 127.0.0.1, wrong port, broken SSL config).
USAGE
3m-ui reset-config [--panel] [--access] [--public] [--all] [--yes]
SCOPE FLAGS (default: --panel if none given)
--panel    Clear server.listen (= ':<port>') + disable SSL/ACME
→ panel boots plain HTTP on next restart. Port preserved.
--access   Clear listener.public_host / access_sni / access_alpn
→ nodes fall back to their bind address.
--public   Clear server.public_url
→ subscription links fall back to request host.
--all      Apply all three scopes above.
--yes / -y Skip interactive y/N confirmation (for rescue scripts).
AFTER RESET
sudo systemctl restart 3m-ui
Open http://<server-ip>:<port>  (HTTP, not HTTPS)
Re-apply settings from the web UI.
SAFETY
- Never deletes users, listeners, traffic records, or credentials.
- Preserves server.port (so SSH rescue doesn't require re-reading YAML).
- SSL disabled by flipping Enabled flag only — Domain / cert paths kept
so operator can re-enable from UI after fixing underlying issue.
- Interactive y/N prompt by default; --yes for scripted rescue.
FILES ADDED
backend/cmd/server/reset_config.go      (~210 LOC)
backend/cmd/server/reset_config_test.go (~110 LOC, 5 tests)
FILES MODIFIED
backend/cmd/server/main.go             (wire reset-config subcommand)
backend/internal/config/config.go      (+ClearListenInFile +ClearPublicURLInFile helpers)
Tests: 23/23 backend packages PASS.
feat(cli): reset-config subcommand for SSH rescue
Adds a new '3m-ui reset-config' CLI subcommand that lets operators undo
panel configuration changes via SSH when the web UI is unreachable
(e.g. panel bound to 127.0.0.1, wrong port, broken SSL config).
USAGE
3m-ui reset-config [--panel] [--access] [--public] [--all] [--yes]
SCOPE FLAGS (default: --panel if none given)
--panel    Clear server.listen (= ':<port>') + disable SSL/ACME
→ panel boots plain HTTP on next restart. Port preserved.
--access   Clear listener.public_host / access_sni / access_alpn
→ nodes fall back to their bind address.
--public   Clear server.public_url
→ subscription links fall back to request host.
--all      Apply all three scopes above.
--yes / -y Skip interactive y/N confirmation (for rescue scripts).
AFTER RESET
sudo systemctl restart 3m-ui
Open http://<server-ip>:<port>  (HTTP, not HTTPS)
Re-apply settings from the web UI.
SAFETY
- Never deletes users, listeners, traffic records, or credentials.
- Preserves server.port (so SSH rescue doesn't require re-reading YAML).
- SSL disabled by flipping Enabled flag only — Domain / cert paths kept
so operator can re-enable from UI after fixing underlying issue.
- Interactive y/N prompt by default; --yes for scripted rescue.
FILES ADDED
backend/cmd/server/reset_config.go      (~210 LOC)
backend/cmd/server/reset_config_test.go (~110 LOC, 5 tests)
FILES MODIFIED
backend/cmd/server/main.go             (wire reset-config subcommand)
backend/internal/config/config.go      (+ClearListenInFile +ClearPublicURLInFile helpers)
Tests: 23/23 backend packages PASS.
feat(cli): reset-config subcommand for SSH rescue
Adds a new '3m-ui reset-config' CLI subcommand that lets operators undo
panel configuration changes via SSH when the web UI is unreachable
(e.g. panel bound to 127.0.0.1, wrong port, broken SSL config).
USAGE
3m-ui reset-config [--panel] [--access] [--public] [--all] [--yes]
SCOPE FLAGS (default: --panel if none given)
--panel    Clear server.listen (= ':<port>') + disable SSL/ACME
→ panel boots plain HTTP on next restart. Port preserved.
--access   Clear listener.public_host / access_sni / access_alpn
→ nodes fall back to their bind address.
--public   Clear server.public_url
→ subscription links fall back to request host.
--all      Apply all three scopes above.
--yes / -y Skip interactive y/N confirmation (for rescue scripts).
AFTER RESET
sudo systemctl restart 3m-ui
Open http://<server-ip>:<port>  (HTTP, not HTTPS)
Re-apply settings from the web UI.
SAFETY
- Never deletes users, listeners, traffic records, or credentials.
- Preserves server.port (so SSH rescue doesn't require re-reading YAML).
- SSL disabled by flipping Enabled flag only — Domain / cert paths kept
so operator can re-enable from UI after fixing underlying issue.
- Interactive y/N prompt by default; --yes for scripted rescue.
FILES ADDED
backend/cmd/server/reset_config.go      (~210 LOC)
backend/cmd/server/reset_config_test.go (~110 LOC, 5 tests)
FILES MODIFIED
backend/cmd/server/main.go             (wire reset-config subcommand)
backend/internal/config/config.go      (+ClearListenInFile +ClearPublicURLInFile helpers)
Tests: 23/23 backend packages PASS.
feat(cli): reset-config subcommand for SSH rescue
Adds a new '3m-ui reset-config' CLI subcommand that lets operators undo
panel configuration changes via SSH when the web UI is unreachable
(e.g. panel bound to 127.0.0.1, wrong port, broken SSL config).
USAGE
3m-ui reset-config [--panel] [--access] [--public] [--all] [--yes]
SCOPE FLAGS (default: --panel if none given)
--panel    Clear server.listen (= ':<port>') + disable SSL/ACME
→ panel boots plain HTTP on next restart. Port preserved.
--access   Clear listener.public_host / access_sni / access_alpn
→ nodes fall back to their bind address.
--public   Clear server.public_url
→ subscription links fall back to request host.
--all      Apply all three scopes above.
--yes / -y Skip interactive y/N confirmation (for rescue scripts).
AFTER RESET
sudo systemctl restart 3m-ui
Open http://<server-ip>:<port>  (HTTP, not HTTPS)
Re-apply settings from the web UI.
SAFETY
- Never deletes users, listeners, traffic records, or credentials.
- Preserves server.port (so SSH rescue doesn't require re-reading YAML).
- SSL disabled by flipping Enabled flag only — Domain / cert paths kept
so operator can re-enable from UI after fixing underlying issue.
- Interactive y/N prompt by default; --yes for scripted rescue.
FILES ADDED
backend/cmd/server/reset_config.go      (~210 LOC)
backend/cmd/server/reset_config_test.go (~110 LOC, 5 tests)
FILES MODIFIED
backend/cmd/server/main.go             (wire reset-config subcommand)
backend/internal/config/config.go      (+ClearListenInFile +ClearPublicURLInFile helpers)
Tests: 23/23 backend packages PASS.
feat(cli): reset-config subcommand for SSH rescue
Adds a new '3m-ui reset-config' CLI subcommand that lets operators undo
panel configuration changes via SSH when the web UI is unreachable
(e.g. panel bound to 127.0.0.1, wrong port, broken SSL config).
USAGE
3m-ui reset-config [--panel] [--access] [--public] [--all] [--yes]
SCOPE FLAGS (default: --panel if none given)
--panel    Clear server.listen (= ':<port>') + disable SSL/ACME
→ panel boots plain HTTP on next restart. Port preserved.
--access   Clear listener.public_host / access_sni / access_alpn
→ nodes fall back to their bind address.
--public   Clear server.public_url
→ subscription links fall back to request host.
--all      Apply all three scopes above.
--yes / -y Skip interactive y/N confirmation (for rescue scripts).
AFTER RESET
sudo systemctl restart 3m-ui
Open http://<server-ip>:<port>  (HTTP, not HTTPS)
Re-apply settings from the web UI.
SAFETY
- Never deletes users, listeners, traffic records, or credentials.
- Preserves server.port (so SSH rescue doesn't require re-reading YAML).
- SSL disabled by flipping Enabled flag only — Domain / cert paths kept
so operator can re-enable from UI after fixing underlying issue.
- Interactive y/N prompt by default; --yes for scripted rescue.
FILES ADDED
backend/cmd/server/reset_config.go      (~210 LOC)
backend/cmd/server/reset_config_test.go (~110 LOC, 5 tests)
FILES MODIFIED
backend/cmd/server/main.go             (wire reset-config subcommand)
backend/internal/config/config.go      (+ClearListenInFile +ClearPublicURLInFile helpers)
Tests: 23/23 backend packages PASS.
2026-09-25 06:10:45 +00:00
kazeyukiro 30d143d3ac feat(routing): cn_direct template uses GEOSITE + GEOIP for domestic direct
GEOSITE private/cn and GEOIP private/CN → DIRECT; MATCH → policy group.
feat(routing): cn_direct template uses GEOSITE + GEOIP for domestic direct
GEOSITE private/cn and GEOIP private/CN → DIRECT; MATCH → policy group.
feat(routing): cn_direct template uses GEOSITE + GEOIP for domestic direct
GEOSITE private/cn and GEOIP private/CN → DIRECT; MATCH → policy group.
feat(routing): cn_direct template uses GEOSITE + GEOIP for domestic direct
GEOSITE private/cn and GEOIP private/CN → DIRECT; MATCH → policy group.
feat(routing): cn_direct template uses GEOSITE + GEOIP for domestic direct
GEOSITE private/cn and GEOIP private/CN → DIRECT; MATCH → policy group.
feat(routing): cn_direct template uses GEOSITE + GEOIP for domestic direct
GEOSITE private/cn and GEOIP private/CN → DIRECT; MATCH → policy group.
feat(routing): cn_direct template uses GEOSITE + GEOIP for domestic direct
GEOSITE private/cn and GEOIP private/CN → DIRECT; MATCH → policy group.
feat(routing): cn_direct template uses GEOSITE + GEOIP for domestic direct
GEOSITE private/cn and GEOIP private/CN → DIRECT; MATCH → policy group.
feat(routing): cn_direct template uses GEOSITE + GEOIP for domestic direct
GEOSITE private/cn and GEOIP private/CN → DIRECT; MATCH → policy group.
feat(routing): cn_direct template uses GEOSITE + GEOIP for domestic direct
GEOSITE private/cn and GEOIP private/CN → DIRECT; MATCH → policy group.
feat(routing): cn_direct template uses GEOSITE + GEOIP for domestic direct
GEOSITE private/cn and GEOIP private/CN → DIRECT; MATCH → policy group.
feat(routing): cn_direct template uses GEOSITE + GEOIP for domestic direct
GEOSITE private/cn and GEOIP private/CN → DIRECT; MATCH → policy group.
2026-09-24 17:25:23 +00:00
kazeyukiro 74a136ab84 refactor(ssl): issue IP certs with mholt/acmez
Replace hand-rolled ACME order/JWS path with acmez (HTTP-01 + TLS-ALPN-01).
CSR uses SAN IPAddresses only (no CN) to satisfy Let's Encrypt.
refactor(ssl): issue IP certs with mholt/acmez
Replace hand-rolled ACME order/JWS path with acmez (HTTP-01 + TLS-ALPN-01).
CSR uses SAN IPAddresses only (no CN) to satisfy Let's Encrypt.
refactor(ssl): issue IP certs with mholt/acmez
Replace hand-rolled ACME order/JWS path with acmez (HTTP-01 + TLS-ALPN-01).
CSR uses SAN IPAddresses only (no CN) to satisfy Let's Encrypt.
refactor(ssl): issue IP certs with mholt/acmez
Replace hand-rolled ACME order/JWS path with acmez (HTTP-01 + TLS-ALPN-01).
CSR uses SAN IPAddresses only (no CN) to satisfy Let's Encrypt.
refactor(ssl): issue IP certs with mholt/acmez
Replace hand-rolled ACME order/JWS path with acmez (HTTP-01 + TLS-ALPN-01).
CSR uses SAN IPAddresses only (no CN) to satisfy Let's Encrypt.
refactor(ssl): issue IP certs with mholt/acmez
Replace hand-rolled ACME order/JWS path with acmez (HTTP-01 + TLS-ALPN-01).
CSR uses SAN IPAddresses only (no CN) to satisfy Let's Encrypt.
refactor(ssl): issue IP certs with mholt/acmez
Replace hand-rolled ACME order/JWS path with acmez (HTTP-01 + TLS-ALPN-01).
CSR uses SAN IPAddresses only (no CN) to satisfy Let's Encrypt.
refactor(ssl): issue IP certs with mholt/acmez
Replace hand-rolled ACME order/JWS path with acmez (HTTP-01 + TLS-ALPN-01).
CSR uses SAN IPAddresses only (no CN) to satisfy Let's Encrypt.
refactor(ssl): issue IP certs with mholt/acmez
Replace hand-rolled ACME order/JWS path with acmez (HTTP-01 + TLS-ALPN-01).
CSR uses SAN IPAddresses only (no CN) to satisfy Let's Encrypt.
refactor(ssl): issue IP certs with mholt/acmez
Replace hand-rolled ACME order/JWS path with acmez (HTTP-01 + TLS-ALPN-01).
CSR uses SAN IPAddresses only (no CN) to satisfy Let's Encrypt.
refactor(ssl): issue IP certs with mholt/acmez
Replace hand-rolled ACME order/JWS path with acmez (HTTP-01 + TLS-ALPN-01).
CSR uses SAN IPAddresses only (no CN) to satisfy Let's Encrypt.
refactor(ssl): issue IP certs with mholt/acmez
Replace hand-rolled ACME order/JWS path with acmez (HTTP-01 + TLS-ALPN-01).
CSR uses SAN IPAddresses only (no CN) to satisfy Let's Encrypt.
2026-09-24 17:21:15 +00:00
kazeyukiro a2f28751c0 feat(ssl): IP cert TLS-ALPN-01 fallback alongside HTTP-01
- Prefer HTTP-01 (:80); on failure retry TLS-ALPN-01 (:443)
- Temporary challenge listeners during first obtain before panel binds
- Permanent :443 GetCertificate serves acme-tls/1 for renewals
- Fix race: challenge answers available during WaitAuthorization
feat(ssl): IP cert TLS-ALPN-01 fallback alongside HTTP-01
- Prefer HTTP-01 (:80); on failure retry TLS-ALPN-01 (:443)
- Temporary challenge listeners during first obtain before panel binds
- Permanent :443 GetCertificate serves acme-tls/1 for renewals
- Fix race: challenge answers available during WaitAuthorization
feat(ssl): IP cert TLS-ALPN-01 fallback alongside HTTP-01
- Prefer HTTP-01 (:80); on failure retry TLS-ALPN-01 (:443)
- Temporary challenge listeners during first obtain before panel binds
- Permanent :443 GetCertificate serves acme-tls/1 for renewals
- Fix race: challenge answers available during WaitAuthorization
feat(ssl): IP cert TLS-ALPN-01 fallback alongside HTTP-01
- Prefer HTTP-01 (:80); on failure retry TLS-ALPN-01 (:443)
- Temporary challenge listeners during first obtain before panel binds
- Permanent :443 GetCertificate serves acme-tls/1 for renewals
- Fix race: challenge answers available during WaitAuthorization
feat(ssl): IP cert TLS-ALPN-01 fallback alongside HTTP-01
- Prefer HTTP-01 (:80); on failure retry TLS-ALPN-01 (:443)
- Temporary challenge listeners during first obtain before panel binds
- Permanent :443 GetCertificate serves acme-tls/1 for renewals
- Fix race: challenge answers available during WaitAuthorization
feat(ssl): IP cert TLS-ALPN-01 fallback alongside HTTP-01
- Prefer HTTP-01 (:80); on failure retry TLS-ALPN-01 (:443)
- Temporary challenge listeners during first obtain before panel binds
- Permanent :443 GetCertificate serves acme-tls/1 for renewals
- Fix race: challenge answers available during WaitAuthorization
feat(ssl): IP cert TLS-ALPN-01 fallback alongside HTTP-01
- Prefer HTTP-01 (:80); on failure retry TLS-ALPN-01 (:443)
- Temporary challenge listeners during first obtain before panel binds
- Permanent :443 GetCertificate serves acme-tls/1 for renewals
- Fix race: challenge answers available during WaitAuthorization
feat(ssl): IP cert TLS-ALPN-01 fallback alongside HTTP-01
- Prefer HTTP-01 (:80); on failure retry TLS-ALPN-01 (:443)
- Temporary challenge listeners during first obtain before panel binds
- Permanent :443 GetCertificate serves acme-tls/1 for renewals
- Fix race: challenge answers available during WaitAuthorization
feat(ssl): IP cert TLS-ALPN-01 fallback alongside HTTP-01
- Prefer HTTP-01 (:80); on failure retry TLS-ALPN-01 (:443)
- Temporary challenge listeners during first obtain before panel binds
- Permanent :443 GetCertificate serves acme-tls/1 for renewals
- Fix race: challenge answers available during WaitAuthorization
feat(ssl): IP cert TLS-ALPN-01 fallback alongside HTTP-01
- Prefer HTTP-01 (:80); on failure retry TLS-ALPN-01 (:443)
- Temporary challenge listeners during first obtain before panel binds
- Permanent :443 GetCertificate serves acme-tls/1 for renewals
- Fix race: challenge answers available during WaitAuthorization
feat(ssl): IP cert TLS-ALPN-01 fallback alongside HTTP-01
- Prefer HTTP-01 (:80); on failure retry TLS-ALPN-01 (:443)
- Temporary challenge listeners during first obtain before panel binds
- Permanent :443 GetCertificate serves acme-tls/1 for renewals
- Fix race: challenge answers available during WaitAuthorization
feat(ssl): IP cert TLS-ALPN-01 fallback alongside HTTP-01
- Prefer HTTP-01 (:80); on failure retry TLS-ALPN-01 (:443)
- Temporary challenge listeners during first obtain before panel binds
- Permanent :443 GetCertificate serves acme-tls/1 for renewals
- Fix race: challenge answers available during WaitAuthorization
2026-09-24 16:25:24 +00:00
kazeyukiro 8bfa8b7034 feat(ssl): Let's Encrypt IP address certificates (shortlived)
When panel SSL domain is a public IP, obtain LE shortlived (~6 day) certs
via ACME profile=shortlived + HTTP-01, with automatic renewal under 48h.
Domain hostnames still use autocert as before.
feat(ssl): Let's Encrypt IP address certificates (shortlived)
When panel SSL domain is a public IP, obtain LE shortlived (~6 day) certs
via ACME profile=shortlived + HTTP-01, with automatic renewal under 48h.
Domain hostnames still use autocert as before.
feat(ssl): Let's Encrypt IP address certificates (shortlived)
When panel SSL domain is a public IP, obtain LE shortlived (~6 day) certs
via ACME profile=shortlived + HTTP-01, with automatic renewal under 48h.
Domain hostnames still use autocert as before.
feat(ssl): Let's Encrypt IP address certificates (shortlived)
When panel SSL domain is a public IP, obtain LE shortlived (~6 day) certs
via ACME profile=shortlived + HTTP-01, with automatic renewal under 48h.
Domain hostnames still use autocert as before.
feat(ssl): Let's Encrypt IP address certificates (shortlived)
When panel SSL domain is a public IP, obtain LE shortlived (~6 day) certs
via ACME profile=shortlived + HTTP-01, with automatic renewal under 48h.
Domain hostnames still use autocert as before.
feat(ssl): Let's Encrypt IP address certificates (shortlived)
When panel SSL domain is a public IP, obtain LE shortlived (~6 day) certs
via ACME profile=shortlived + HTTP-01, with automatic renewal under 48h.
Domain hostnames still use autocert as before.
feat(ssl): Let's Encrypt IP address certificates (shortlived)
When panel SSL domain is a public IP, obtain LE shortlived (~6 day) certs
via ACME profile=shortlived + HTTP-01, with automatic renewal under 48h.
Domain hostnames still use autocert as before.
feat(ssl): Let's Encrypt IP address certificates (shortlived)
When panel SSL domain is a public IP, obtain LE shortlived (~6 day) certs
via ACME profile=shortlived + HTTP-01, with automatic renewal under 48h.
Domain hostnames still use autocert as before.
feat(ssl): Let's Encrypt IP address certificates (shortlived)
When panel SSL domain is a public IP, obtain LE shortlived (~6 day) certs
via ACME profile=shortlived + HTTP-01, with automatic renewal under 48h.
Domain hostnames still use autocert as before.
feat(ssl): Let's Encrypt IP address certificates (shortlived)
When panel SSL domain is a public IP, obtain LE shortlived (~6 day) certs
via ACME profile=shortlived + HTTP-01, with automatic renewal under 48h.
Domain hostnames still use autocert as before.
feat(ssl): Let's Encrypt IP address certificates (shortlived)
When panel SSL domain is a public IP, obtain LE shortlived (~6 day) certs
via ACME profile=shortlived + HTTP-01, with automatic renewal under 48h.
Domain hostnames still use autocert as before.
feat(ssl): Let's Encrypt IP address certificates (shortlived)
When panel SSL domain is a public IP, obtain LE shortlived (~6 day) certs
via ACME profile=shortlived + HTTP-01, with automatic renewal under 48h.
Domain hostnames still use autocert as before.
2026-09-24 15:28:25 +00:00
kazeyukiro a6f97d5446 ui: denser mobile/desktop layout for lists and toolbars
Tighter page padding, mobile toolbar grid, compact entity cards and
tables; Users list uses small table size.
ui: denser mobile/desktop layout for lists and toolbars
Tighter page padding, mobile toolbar grid, compact entity cards and
tables; Users list uses small table size.
ui: denser mobile/desktop layout for lists and toolbars
Tighter page padding, mobile toolbar grid, compact entity cards and
tables; Users list uses small table size.
ui: denser mobile/desktop layout for lists and toolbars
Tighter page padding, mobile toolbar grid, compact entity cards and
tables; Users list uses small table size.
ui: denser mobile/desktop layout for lists and toolbars
Tighter page padding, mobile toolbar grid, compact entity cards and
tables; Users list uses small table size.
ui: denser mobile/desktop layout for lists and toolbars
Tighter page padding, mobile toolbar grid, compact entity cards and
tables; Users list uses small table size.
ui: denser mobile/desktop layout for lists and toolbars
Tighter page padding, mobile toolbar grid, compact entity cards and
tables; Users list uses small table size.
ui: denser mobile/desktop layout for lists and toolbars
Tighter page padding, mobile toolbar grid, compact entity cards and
tables; Users list uses small table size.
ui: denser mobile/desktop layout for lists and toolbars
Tighter page padding, mobile toolbar grid, compact entity cards and
tables; Users list uses small table size.
ui: denser mobile/desktop layout for lists and toolbars
Tighter page padding, mobile toolbar grid, compact entity cards and
tables; Users list uses small table size.
ui: denser mobile/desktop layout for lists and toolbars
Tighter page padding, mobile toolbar grid, compact entity cards and
tables; Users list uses small table size.
ui: denser mobile/desktop layout for lists and toolbars
Tighter page padding, mobile toolbar grid, compact entity cards and
tables; Users list uses small table size.
2026-09-24 14:54:19 +00:00
kazeyukiro 3bb1abe43a docs: document POST /users/quick in OpenAPI and user guides 2026-09-24 14:51:09 +00:00