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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
/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>
- 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>
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>
- 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>
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>
ErrProcessInfoUnavailable shared 51005 with ErrConfigParseError and
ErrClientClosed shared 510008 with ErrKnownHostsRead.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- 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>