2753 Commits

Author SHA1 Message Date
Jacky
c2e16938c8 Merge pull request #1846 from 0xJacky/chore/dependency-updates
chore(deps): update dependencies and take apexcharts to v7
2026-09-03 02:13:07 +08:00
0xJacky
1b50bb8979 test(nginx_log): lock the read of completedFiles in the index rebuild test
The completion callback appends to completedFiles under a mutex, but the
final t.Logf read it without one, so -race flagged the write against the
read. The bleve bump only changed the timing enough to make it show up; the
race was always there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 01:29:29 +08:00
0xJacky
3523d72d55 fix(deps): regenerate the lockfiles with the Bun the workflows now use
The sweep moves CI from Bun 1.3.14 to 1.4.0, and 1.4.0 reads the lockfile
differently: it drops the `@uozi-admin/curd@6.0.2>xlsx` override entry, which
it has always warned it cannot honour anyway. A lockfile written by 1.3.14
therefore fails `bun ci` under 1.4.0 even though nothing about the tree
changed. Regenerated with 1.4.0 so the frozen install agrees.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 00:52:13 +08:00
0xJacky
ac1e491486 test: steady two tests the dependency sweep shook loose
apexcharts 7 renders legend and tooltip markers as their own svg inside the
chart canvas, so matching any descendant svg now resolves five elements and
trips strict mode. Match the chart's own root instead; the chart itself was
always fine.

The AES test corrupted one byte of a real ciphertext and expected the base64
decode to reject the result, which only happens by chance because the IV is
random: 1 failure in 30 runs on unchanged code, and it took out this branch's
CI. Feed the decrypter a body that decrypts to something base64 always
rejects. 40 runs, no failures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 00:50:16 +08:00
0xJacky
a761116f41 fix(deps): hold dayjs at 1.11.21 for antdv-next's picker
Raising dayjs to 1.11.23 leaves antdv-next 1.5.3 unable to use it: the
picker's generateConfig calls `.weekday()`, that plugin is never registered
against the copy the picker sees, and every calendar opens with no date
cells. The console says `t.weekday is not a function`; nothing else does.

Verified by isolation on a real build: 1.11.23 renders 0 cells, 1.11.21
renders 42 with a clean console. Held back with an override so a transitive
bump cannot reintroduce it, and everything else in the sweep still moves.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 00:50:16 +08:00
0xJacky
a00b85ec22 fix(deps): pin dayjs to one copy so the date pickers keep working
Raising the app to dayjs ^1.11.23 while antdv-next still asks for ^1.11.21
let Bun keep a nested 1.11.21 for antdv-next alongside 1.11.23 at the root.
The picker then reads a dayjs instance that never had the app's plugins
registered, and every calendar popup opened with no date cells at all: the
log dashboard and the structured log viewer both lost their range picker.

Nothing failed to build or type-check; the e2e suite caught it. An override
keeps a single copy, which is what antdv-next's FAQ asks for.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 00:32:28 +08:00
0xJacky
42d12a8b42 chore(deps): build the demo binary on the Go version go.mod now requires
go.mod moved to 1.27.0 with the rest of the dependency sweep, and the
workflows moved with it, but the demo image's cross-build container was
still pinned to golang:1.26-trixie and refused the module.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 00:20:43 +08:00
0xJacky
ad0b313778 chore(deps): update dependencies and take apexcharts to v7
Folds three Renovate pull requests into one change: the non-major sweep
(#1770), apexcharts v7 (#1836), and the @uozi-admin/curd v6 bump (#1662),
which the antdv-next migration already carried.

apexcharts 7 moves nine features out of the default bundle and removes
plotOptions.bar.borderRadiusWhenStacked. None of the nine are used here,
that option appears nowhere, and every chart sets dataLabels.enabled to
false, so the changed animate default does not reach them either.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 00:19:20 +08:00
Jacky
c13f2d68f0 Merge pull request #1845 from 0xJacky/chore/antdv-next-migration
refactor(app): migrate from ant-design-vue to antdv-next
2026-09-03 00:13:24 +08:00
0xJacky
89d8fd8b5a Merge branch 'dev' into chore/antdv-next-migration
dev brought the host-via-SSH setup wizard, whose components were written
against ant-design-vue, so they are migrated here alongside the merge.

Preference.vue keeps the items-based Tabs from this branch and picks up
dev's control-editing binding on NginxSettings.

Two antdv-next differences the wizard tripped over: Typography's copyable
config calls the field `tooltips`, not `tooltip`, and Divider `orientation`
now selects the axis so the text position moved to `title-placement`. Both
are added to check-antdv-legacy.sh.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 23:53:26 +08:00
0xJacky
45fba9f3f8 test(e2e): cover the antdv-next migration surface
antdv-next fails by ignoring a prop or dropping a slot, which lint, vue-tsc
and vite build cannot see. These specs target that: every reachable Select is
opened and asserted to have options, every tab must render a non-empty panel,
overlays must have real content and a non-zero width, tables must still show
their custom cell output, and browser console errors fail the test.

One spec covers each area the migration touched. Also repoints the quick
setup status-code lookup: antdv-next renders dropdown options without
role="option" or aria-selected, so the option has to be matched by class,
and .ant-select-selector no longer exists as the trigger.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 23:38:15 +08:00
0xJacky
45ac55029b refactor(app): migrate from ant-design-vue to antdv-next
Swap the UI library for antdv-next 1.5.3. @uozi-admin/curd 5.0.0 dropped
ant-design-vue in favour of antdv-next, so the curd bump to 6.0.2 and the
UI library switch have to land together; the two cannot coexist.

Components removed by antdv-next are replaced locally: List/ListItem/
ListItemMeta gain a minimal in-repo implementation under components/List,
Comment becomes plain markup, Step children become a Steps items array and
the stray Icon tag becomes LoadingOutlined.

Several APIs are ignored rather than rejected by antdv-next, so they were
converted even though nothing failed to build: Drawer width to a numeric
size, Badge numberStyle to styles.indicator, Popover width onto both the
root and container style slots, native Table customRender to render with
its new positional signature, Dropdown overlay slots to popupRender, and
Divider orientation to titlePlacement.

Select option children deserve their own note: antdv-next still exports
ASelectOption and its resolver still maps it, but the Select never forwards
its default slot, so every dropdown declared that way rendered empty while
lint, vue-tsc and vite build all stayed green. All 52 usages moved to the
options prop. scripts/check-antdv-legacy.sh guards this and the rest of the
silent-failure list, wired up as `bun run check:antdv`.

Also fixes the login container clamping itself to the viewport height, which
pushed the footer outside the page background now that the form is taller.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 23:37:57 +08:00
0xJacky
f38d3a49a4 refactor(app): drop the stale platform check label
The backend no longer emits a bare "platform" verify step now that the
verify pipeline runs on every OS, so the label had no row to describe.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 23:20:38 +08:00
0xJacky
5f26a639fe refactor(host): drop the unused password authentication plumbing
The nginx control settings validator only ever accepted key
authentication and nothing set a password reference, so the auth method
and password fields, the password branch in the SSH client, the runner
warning, the API payload fields, the TS types and the doc rows all
carried state that could never be exercised. Remove them; the SSH client
now always authenticates with the configured private key.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 23:19:50 +08:00
0xJacky
99dfd0786c refactor(app): derive use_generated_key from the wizard key source
The wizard kept both key_source and use_generated_key on the request
params and re-derived the boolean by hand in three places, while the
backend only reads use_generated_key. Keep key_source as the single wizard
state (HostSetupParams), compute use_generated_key once in toSetupParams()
when a request body is built, and expose the result as requestParams for
every API call. Drop key_source from the wire SetupParams type and the
normalizeSource() reconciler.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 23:16:22 +08:00
0xJacky
1b4c41c751 refactor(host/setup): quote snippets with the SSH client's ShellQuote
The setup snippets and the SSH client each carried their own POSIX
single-quote helper with a different escaping idiom for inner quotes. The
client's rule (bare safe tokens, '\'' escaping) is now exported as
hostssh.ShellQuote and registered in the snippet template FuncMap, so a
value the wizard pastes into a shell is quoted exactly like one it executes.
The snippet test asserts the unified form and round-trips the rendered
authorized_keys line through /bin/sh; the goldens carry no quoted values
and are unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 23:15:15 +08:00
0xJacky
d1840bae01 refactor(host/setup): share one host key source fallback rule
The empty host_key_source fallback (managed default path means generated,
anything else existing) lived once in settings.Nginx.GetHostKeySource and
once more in the settings API normaliser. settings.NormalizeHostKeySource now
holds the rule; it only fills the empty case so an unknown value still
reaches the validator, which is why the API cannot simply call the getter.
GetHostKeySource keeps coercing unknown persisted values through the same
helper. Table tests cover both entry points.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 23:15:15 +08:00
0xJacky
9d53f5a6d5 refactor(app): share request scaffolding in host setup wizard
Extract useLatestRequest(), which owns the loading and error refs plus the
increment-id guard that lets only the latest run write results or clear the
spinner, and use it in every wizard step instead of the hand-copied
requestID pattern. Add onDeactivated cleanup to the steps that lacked it.

Render StepVerify through CheckPanel so the run/reset/rows/blocking logic
lives once; CheckPanel gains checkKeys, errorTitle and resultsOutsideCard
props plus action and default slots to keep the Verify layout unchanged.

Collapse the three identical try/finally shells in HostKeyTrust into one
runKeyOperation helper.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 23:15:03 +08:00
0xJacky
3dd51bb73c refactor(nginx): drop the pre-Stat from SFTP WriteFile
Decide whether a file is new from an exclusive create instead of a
separate Stat round trip: O_EXCL succeeds only for a new file, and the
generic SSH_FX_FAILURE it yields for an existing one is retried as a
plain truncate. mode is still applied only to files this call creates,
so existing permissions are preserved exactly as before. The decision
lives in openForWrite so it can be tested against a stub opener and
real local files without an SFTP server.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 23:12:12 +08:00
0xJacky
c4215fcb3a perf(host/setup): run independent verify checks concurrently
A full verify issued a dozen SSH commands one after another, each paying a
session round trip, although only ssh_connect (the gate) and nginx -t (the
final, costly check) have an ordering requirement. The platform, directory,
same-host and privilege checks now run through a bounded runner capped at 4
concurrent sessions, well under sshd's default MaxSessions of 10, and record
their outcomes under a mutex so the VerifyResult JSON shape is unchanged.
The two privilege checks share a single id -u probe through a sync.Once.

DiagnoseHost keeps its dependent chain in order but overlaps the
architecture probe with the platform probes and, on macOS, the launchctl and
Homebrew probes with each other; warnings are merged in a fixed order.

The fake runner is now concurrency safe and records its peak in-flight
count, and new tests pin the cap, the sequential gates and the warning order.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 23:12:11 +08:00
0xJacky
fada4911d8 refactor(nginx): resolve paths against the target OS
Add Runner.GOOS so resolvePath, GetPrefix, GetConfPath, GetModulesPath
and execShell follow the OS of the machine that runs nginx instead of
runtime.GOOS: local reports the host OS, docker is always linux, and an
SSH host is darwin for launchd and linux for systemd. getNginxSbinPath
no longer looks nginx up in this container's PATH for a remote target;
SSH keeps the host default and docker hands the bare name to the
container's PATH like the nginx -t fallback already did. shellCommand
takes the target GOOS directly instead of a remote flag.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 23:11:18 +08:00
0xJacky
31f6518e33 refactor(host/setup): build the verify pipeline on every OS
verify.go was tagged linux and a !linux stub hard-failed every verify with a
single "platform" step, so the macOS and Windows builds could not verify an
SSH host at all and the pipeline tests never ran on darwin. Only two calls
were platform specific: unix.Access and the syscall.Stat_t inode. They now
live in verify_unix.go and verify_windows.go behind dirAccessible and
localInode; the Windows variants make the mounted-only checks warn instead of
failing, and the launchd shared-path check already degrades without
/proc/self/mountinfo. The stub and its "platform" step are gone, and the
former verify_linux_test.go runs everywhere as verify_checks_test.go with
extra coverage for the platform helpers.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 23:09:06 +08:00
0xJacky
174fd493e2 refactor(nginx): route host service control through a strategy
Move the systemd/launchd branches out of reload, restart and IsRunning
into a hostService interface with one implementation per manager, built
from the settings getters so every default keeps a single home. The
generated commands are unchanged; the existing assertions now go through
newHostService and cover restart, status and is-active parsing.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 23:08:11 +08:00
Jacky
d6e3152375 Merge pull request #1686 from 0xJacky/feat/host-via-ssh-setup
feat: add host SSH setup for Docker-managed host nginx
2026-09-02 22:32:08 +08:00
0xJacky
c8c7de30ce docs: reflect the final host SSH behaviour
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 22:31:10 +08:00
0xJacky
32ba740bc1 refactor(host): report setup handler failures through scoped Cosy errors
Replace the raw errors.New private key path failures, the duplicated
algorithm mismatch gin.H response and the ad hoc 500 for a failed key
removal with scoped errors (ErrPrivateKeyPathRequired,
ErrPrivateKeyPathNotAbsolute, ErrKeyfileDelete in host_setup;
ErrHostKeyAlgorithmMismatch in host_ssh). Validation failures keep their
400 status via abortBadRequest, the remove failure goes through
cosy.ErrHandler.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 21:16:00 +08:00
0xJacky
0a717f6735 perf(host/ssh): probe the cached connection only when the keepalive is stale
connectLocked sent a keepalive request under the mutex on every Exec and
SFTP call, so each remote command paid an extra round trip even though
the keepalive goroutine already exercises the connection every interval.
Record the last successful probe (from dial, the keepalive goroutine and
completed sessions) and only probe synchronously when it is older than
the keepalive interval. A connection that died in between is still
recovered by one redial-and-retry when NewSession or the SFTP subsystem
fails to open.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 21:11:12 +08:00
0xJacky
ea07f679ba refactor(nginx): export ExtractConfigureArg and reuse it in host discovery
internal/host/setup carried a line-for-line copy of the nginx -V
configure-argument parser. Export the internal/nginx version and call it
from discovery. The prefix join stays local to setup because it must use
POSIX path rules for the SSH host regardless of the nginx-ui platform.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 21:09:25 +08:00
0xJacky
58de54733d refactor(host): drop dead host key helpers and the legacy known-host route
POST host/setup/known-host and hostSetup.trustHostKey had no UI callers
and were a weaker duplicate of host-key/trust, which checks the algorithm
and requires explicit confirmation. hostssh.ScanHostKeys had no callers
and ClassifyHostKeys was only used by tests; the tests now exercise
ClassifyScannedHostKeys like production code does. forgetDetected in the
wizard composable was exported but unused.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 21:09:24 +08:00
0xJacky
03af49ef4a refactor(settings): validate nginx control settings with scoped errors
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 21:08:09 +08:00
0xJacky
29908f5f3c perf(nginx): memoize the discovered PID path
GetPIDPath probed the target on every call, which in SSH mode meant one
remote exec per candidate for the performance ticker and every settings
read. Cache the discovered path like the other resolved values and drop
it in resetPathCaches when the control target changes. The configured
override is still read on every call.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 18:35:55 +08:00
0xJacky
412bbd02ff perf(nginx): resolve the PID path only in the IsRunning branches that use it
The host_via_ssh branch never used the value computed before the switch
and re-resolved it inside isRunningViaHostService, so SSH mode paid for
the discovery twice per call.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 18:35:55 +08:00
0xJacky
5b32c0dbfa refactor(host): reuse the known_hosts default and persistence rule
Use settings.DefaultHostKnownHostsPath instead of repeating the literal,
and add setup.IsPersistedDataPath so the verify pipeline and the host key
scan handler share one definition of the persisted data directory.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 18:35:21 +08:00
0xJacky
c0b6507153 refactor(host): share the private key file reader
Export hostssh.ReadPrivateKeyFile and MaxPrivateKeyFileSize and make
setup.ReadPrivateKeyFile a thin wrapper that only re-wraps the failure in
the host_setup error scope, instead of duplicating the stat/regular/size
checks in both packages.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 18:34:52 +08:00
0xJacky
7db0a0a6fe refactor(nginx): share the systemd defaults through settings getters
The /bin/systemctl and nginx.service fallbacks were copied into every
host command builder. Expose them as GetHostSystemctlPath and
GetHostSystemdUnitName next to the launchd getters so a default change
lands in one place.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 18:34:47 +08:00
0xJacky
9d1a6ef230 refactor(host/setup): share verify types across build tags
Move StepOutcome, VerifyResult, VerifyOptions and findMissingSudoEntries
from the linux/!linux tagged files into the untagged verify_common.go so
each tagged file only carries its Verify implementation.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 18:34:04 +08:00
0xJacky
b8637618a5 fix(cert): write and read certificates on the nginx target filesystem
Content.WriteFile, ConfigPayload.WriteFile and writeFileWithMode staged and
renamed certificate files with os.* calls, so in host_via_ssh + sftp mode the
ACME, self-signed and manual certificate flows landed in the container while
the site config referencing them was written to the host, and nginx -t on the
host failed to load them.

Route directory creation, temp-file staging, chmod, rename and cleanup
through nginx.MkdirAll/OpenFile/Chmod/Rename/Remove, keeping the atomic
replace and the owner-only private key mode in local mode. The temp file is
chmod'd before any content is written because the SFTP backend ignores the
mode passed to OpenFile. Rename onto an existing file is retried after
removing the target, which SFTP servers commonly require.

Readers that inspect the same files (certificate info, renewal checks, the
self-signed key loader, remote sync, fingerprints, import validation, the API
transformer) and the self-signed directory cleanup now use nginx.ReadFile/
Stat/RemoveAll as well.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 18:31:06 +08:00
0xJacky
b11b690a05 fix(nginx): skip the shared config self-check under SFTP access
The shared-directory check ran for host_via_ssh regardless of access
mode, but it writes the probe with os.CreateTemp inside this container
and stats it on the host. With SFTP access there is no bind mount by
design, so it always reported an unshared directory. Register and run
the task only when a shared directory is expected.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 18:30:49 +08:00
0xJacky
84111b9174 fix(nginx): resolve the host nginx binary in SSH mode when sbin_path is empty
With host_via_ssh and an empty SbinPath, getNginxSbinPath fell back to
exec.LookPath inside the nginx-ui container and hostssh.Config carried
an empty NginxSbinPath, so nginx -t/-T ran on the host without sudo or
with a path the host does not have. Share the setup wizard's defaults
through settings.Nginx.GetHostSbinPath, use it for path resolution, the
SSH sudo whitelist and the launchd reload command, and persist it when
the control settings are saved without a path.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 18:29:55 +08:00
0xJacky
bca0c19411 fix(nginx): honour custom reload and restart commands in SSH mode
reloadContext and restartContext took the host_via_ssh branch before
checking ReloadCmd/RestartCmd, so an operator-authored command was
replaced by systemctl even though TestConfigCmd was honoured first.
Check the custom commands first; execShellContext already routes them
through the SSH runner.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 18:28:10 +08:00
0xJacky
546f8ee3be fix(nginx): relink any existing enabled entry on site and stream rename
The rename path used to gate relinking on helper.SymbolLinkExists, which
is true for any Lstat-able entry. The target-filesystem port switched it
to nginx.SymlinkExists, so a regular file in sites-enabled or
streams-enabled was left behind serving the old content under the old
name. Gate on nginx.EntryExists instead so the old entry is removed and
replaced by a symlink regardless of its type.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 18:27:22 +08:00
0xJacky
a917352f0c fix(cache): index the nginx target filesystem in SFTP mode
The config scanner read the container's own filesystem with os.Lstat/os.Stat/
os.ReadFile/os.ReadDir and watched it with fsnotify rooted at the nginx config
path. In host_via_ssh + sftp mode that path lives on the remote host, so the
index, certificate discovery and upstream availability described the wrong
tree and host-side edits never produced events.

Route every scanner file operation through the target-aware nginx.* helpers
and, when nginx.UsesSFTPTarget() reports SFTP, skip fsnotify entirely and
rescan on a short RemoteScanInterval (30s) instead. Local, external container
and mounted modes keep fsnotify and the 5 minute safety-net rescan unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 18:27:16 +08:00
0xJacky
3b9dce1ddb fix(host/ssh): let x/crypto pick host key algorithms when known_hosts is silent
HostKeyAlgorithms returned an empty non-nil slice when no non-CA entry
matched, which x/crypto offers verbatim and fails with "no common
algorithm" before HostKeyCallback can report the unknown host. Return
nil in that case and whenever a @cert-authority line is present so the
default list is used and the callback decides.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 18:26:09 +08:00
0xJacky
fc042dda31 fix(host/ssh): probe remote paths with /bin/test
/usr/bin/test only exists on Linux, so Client.Stat always reported a
missing file against a macOS host while the setup verifier already used
/bin/test. Share one probe builder so both agree.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 18:26:09 +08:00
0xJacky
9fa6be8195 fix(app): harden nginx host setup control mode and wizard steps
- Render the control-mode radio group only while editing, so the read-only
  view can no longer mutate the store without passing the 2FA-gated Edit flow
- Show a dedicated message when no private key exists at the given path
  instead of the raw transport error
- Share parseHostAddress between the SSH target and host key steps so
  bracketed IPv6 loopback targets no longer trigger the remote-address warning
- Keep a stale host key scan or connection test from clearing the loading
  state of a newer in-flight run
- Align the E2E spec with the read-only control mode view

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 18:25:29 +08:00
0xJacky
69cfff126b test(e2e): cover the nginx host setup entry points in demo mode
Assert the control-mode editor exposes Host via SSH and the wizard route
stops at the two-factor guard without calling the host setup API.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 18:20:19 +08:00
0xJacky
4445049754 docs: align host SSH mode guides with the implementation
- Document host_access_mode, host_key_source and the host-side sbin_path in
  the env and nginx configuration references.
- Add the required --access-mode flag to the host-setup CLI examples.
- Describe the five wizard steps and name Trust & Test / Access & Install.
- Homebrew discovery happens in Detect Platform, not during verification.
- The Verify step only runs the nginx checks; same_host and
  known_hosts_persistence only run from host-setup test.
- Use the comma-separated NGINX_UI_CLUSTER_NODE variable.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 18:18:49 +08:00
0xJacky
9c0f5fb087 fix(pages): read maintenance templates through the nginx target filesystem
In host_via_ssh with SFTP access the maintenance directory is on the host,
so os.ReadFile never found the per-site templates.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 18:16:43 +08:00
0xJacky
d6bee5d709 fix: assign unique error codes in performance and host_ssh scopes
ErrProcessInfoUnavailable shared 51005 with ErrConfigParseError and
ErrClientClosed shared 510008 with ErrKnownHostsRead.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 18:16:01 +08:00
0xJacky
275094fd09 fix(host): harden the host setup API surface
- Reject the whole host/setup group in demo mode.
- Require a verified two-factor session (or a proxied node principal) for
  every endpoint that writes SSH material or opens an outbound connection,
  sharing the guard with POST settings/nginx/control via internal/middleware.
- Validate host_address before it reaches known_hosts, where a comma,
  wildcard or newline would trust a key for other hosts.
- Only read the public key of an unmanaged private key path for a verified
  session, so the endpoint stops being a container-wide file oracle.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 18:15:53 +08:00