@astralyn/sash 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/README.md +6 -3
- package/dist/commands/lifecycle.js +0 -16
- package/dist/commands/status.js +1 -6
- package/dist/commands/web.js +40 -12
- package/dist/contracts.js +16 -0
- package/dist/daemon/app.js +10 -3
- package/dist/daemon/errors.js +3 -1
- package/dist/daemon/handlers/daemon.js +15 -0
- package/dist/daemon/router.js +16 -8
- package/dist/daemon/server.js +1 -1
- package/dist/daemon/web-auth.js +52 -0
- package/dist/daemon-auth.js +2 -2
- package/dist/daemon-client.js +6 -0
- package/dist/daemon-http.js +1 -0
- package/dist/log-follow.js +2 -1
- package/dist/managed-state-transaction.js +6 -8
- package/dist/mihomo-config.js +5 -8
- package/dist/sash-client.js +24 -3
- package/dist/settings-service.js +14 -18
- package/dist/settings.js +5 -1
- package/dist/status.js +0 -24
- package/dist/ui/assets/{CodeEditorModal-CFEnWsyh.js → CodeEditorModal-D1zn-jRS.js} +1 -1
- package/dist/ui/assets/{ConnectionsView-DNGmZBSU.js → ConnectionsView-CfaOG2JV.js} +1 -1
- package/dist/ui/assets/{LogsView-fSiaxQ13.js → LogsView-BDiRATXN.js} +1 -1
- package/dist/ui/assets/{PaginationFooter-B3kHzRfB.js → PaginationFooter-DTytr1iu.js} +1 -1
- package/dist/ui/assets/{ProfileEditorDialog-BydoZthX.js → ProfileEditorDialog-CyyP6OF1.js} +1 -1
- package/dist/ui/assets/{ProfilesView-Btc1DOxE.js → ProfilesView-hFa7-O72.js} +2 -2
- package/dist/ui/assets/{RulesView-D9vZBiJ1.js → RulesView-CjEyCN1A.js} +1 -1
- package/dist/ui/assets/SettingsFileDialog-CjBSyH4K.js +1 -0
- package/dist/ui/assets/SettingsView-D5QvJId6.css +1 -0
- package/dist/ui/assets/SettingsView-DsRntRfn.js +2 -0
- package/dist/ui/assets/{0be242294f7d791af850c6df38ac78a0-2cL6Ntwf.woff2 → e2a57555d97d0b02b45d9418eb6ee295-D9nhF3rM.woff2} +0 -0
- package/dist/ui/assets/index-CxQTf_P9.css +1 -0
- package/dist/ui/assets/index-_mb4OfYp.js +19 -0
- package/dist/ui/assets/{theme-BNq4FkXS.js → theme-BwDBMsKO.js} +1 -1
- package/dist/ui/index.html +2 -2
- package/dist/web-bootstrap.js +113 -0
- package/docs/backend.md +12 -4
- package/docs/frontend.md +13 -3
- package/docs/usage.md +10 -36
- package/package.json +8 -4
- package/dist/tun-guidance.js +0 -11
- package/dist/ui/assets/SettingsFileDialog-CNFEAVs4.js +0 -1
- package/dist/ui/assets/SettingsView-BlDhZkXQ.js +0 -2
- package/dist/ui/assets/SettingsView-CngS3vBM.css +0 -1
- package/dist/ui/assets/index-B61V60w_.js +0 -19
- package/dist/ui/assets/index-bkyxJG8J.css +0 -1
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
|
+
import { WEB_BOOTSTRAP_TTL_MS } from "./daemon/web-auth.js";
|
|
6
|
+
import { atomicWriteFileSync } from "./fs-atomic.js";
|
|
7
|
+
import { runSanitizedCommand, windowsSystemExecutable } from "./process.js";
|
|
8
|
+
const BOOTSTRAP_DIRECTORY = /^web-bootstrap-(\d{13})-[a-f0-9]{16}$/;
|
|
9
|
+
export function removeStaleBootstrapFiles(root, now = Date.now()) {
|
|
10
|
+
let entries;
|
|
11
|
+
try {
|
|
12
|
+
entries = fs.readdirSync(root, { withFileTypes: true });
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
for (const entry of entries) {
|
|
18
|
+
const match = BOOTSTRAP_DIRECTORY.exec(entry.name);
|
|
19
|
+
if (entry.isDirectory() && !entry.isSymbolicLink() && match && Number(match[1]) <= now) {
|
|
20
|
+
removeBootstrapFile(path.join(root, entry.name, "index.html"));
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function createPrivateDirectory(directory) {
|
|
25
|
+
if (process.platform !== "win32") {
|
|
26
|
+
fs.mkdirSync(directory, { mode: 0o700 });
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const executable = windowsSystemExecutable("WindowsPowerShell/v1.0/powershell.exe");
|
|
30
|
+
if (!path.isAbsolute(executable))
|
|
31
|
+
throw new Error("Windows PowerShell is unavailable");
|
|
32
|
+
const encoded = Buffer.from(directory, "utf8").toString("base64");
|
|
33
|
+
const script = [
|
|
34
|
+
"$ErrorActionPreference = 'Stop'",
|
|
35
|
+
`$directory = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${encoded}'))`,
|
|
36
|
+
"if (Test-Path -LiteralPath $directory) { throw 'Bootstrap directory already exists' }",
|
|
37
|
+
"$sid = [Security.Principal.WindowsIdentity]::GetCurrent().User",
|
|
38
|
+
"$acl = [Security.AccessControl.DirectorySecurity]::new()",
|
|
39
|
+
"$acl.SetOwner($sid)",
|
|
40
|
+
"$acl.SetAccessRuleProtection($true, $false)",
|
|
41
|
+
"$acl.AddAccessRule([Security.AccessControl.FileSystemAccessRule]::new($sid, 'FullControl', 'ContainerInherit,ObjectInherit', 'None', 'Allow'))",
|
|
42
|
+
"[IO.Directory]::CreateDirectory($directory, $acl) | Out-Null",
|
|
43
|
+
"$actual = [IO.Directory]::GetAccessControl($directory)",
|
|
44
|
+
"if (!$actual.AreAccessRulesProtected -or $actual.GetOwner([Security.Principal.SecurityIdentifier]).Value -ne $sid.Value) { throw 'Bootstrap directory is not private' }",
|
|
45
|
+
].join("; ");
|
|
46
|
+
runSanitizedCommand(executable, ["-NoProfile", "-NonInteractive", "-Command", script]);
|
|
47
|
+
}
|
|
48
|
+
export function writeBootstrapFile(layout, opts) {
|
|
49
|
+
const now = Date.now();
|
|
50
|
+
const expiresAt = Date.parse(opts.expiresAt);
|
|
51
|
+
if (!/^[a-f0-9]{64}$/.test(opts.token) ||
|
|
52
|
+
!Number.isFinite(expiresAt) ||
|
|
53
|
+
expiresAt <= now ||
|
|
54
|
+
expiresAt > now + WEB_BOOTSTRAP_TTL_MS) {
|
|
55
|
+
throw new Error("Invalid or expired browser authorization; run 'sash web' again.");
|
|
56
|
+
}
|
|
57
|
+
const target = new URL(opts.dashboardUrl);
|
|
58
|
+
if (target.protocol !== "http:" ||
|
|
59
|
+
target.hostname !== "127.0.0.1" ||
|
|
60
|
+
target.username ||
|
|
61
|
+
target.password) {
|
|
62
|
+
throw new Error("The dashboard must use the local Sash address");
|
|
63
|
+
}
|
|
64
|
+
fs.mkdirSync(layout.tempDir, { recursive: true });
|
|
65
|
+
removeStaleBootstrapFiles(layout.tempDir, now);
|
|
66
|
+
const name = `web-bootstrap-${expiresAt}-${crypto.randomBytes(8).toString("hex")}`;
|
|
67
|
+
const directory = path.join(layout.tempDir, name);
|
|
68
|
+
const filePath = path.join(directory, "index.html");
|
|
69
|
+
createPrivateDirectory(directory);
|
|
70
|
+
const handoff = JSON.stringify({ url: opts.dashboardUrl, token: opts.token }).replaceAll("<", "\\u003c");
|
|
71
|
+
const html = `<!doctype html>
|
|
72
|
+
<html lang="en">
|
|
73
|
+
<head>
|
|
74
|
+
<meta charset="utf-8" />
|
|
75
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
76
|
+
<meta name="referrer" content="no-referrer" />
|
|
77
|
+
<title>Sash</title>
|
|
78
|
+
</head>
|
|
79
|
+
<body>
|
|
80
|
+
<p>Opening the Sash dashboard...</p>
|
|
81
|
+
<script>
|
|
82
|
+
(() => {
|
|
83
|
+
const handoff = ${handoff};
|
|
84
|
+
window.location.replace(handoff.url + "#boot=" + handoff.token);
|
|
85
|
+
})();
|
|
86
|
+
</script>
|
|
87
|
+
</body>
|
|
88
|
+
</html>
|
|
89
|
+
`;
|
|
90
|
+
try {
|
|
91
|
+
atomicWriteFileSync(filePath, html, 0o600);
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
removeBootstrapFile(filePath);
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
return { filePath, fileUrl: pathToFileURL(filePath).href };
|
|
98
|
+
}
|
|
99
|
+
export function removeBootstrapFile(filePath) {
|
|
100
|
+
const directory = path.dirname(filePath);
|
|
101
|
+
if (path.basename(filePath) !== "index.html" ||
|
|
102
|
+
!BOOTSTRAP_DIRECTORY.test(path.basename(directory)))
|
|
103
|
+
return;
|
|
104
|
+
try {
|
|
105
|
+
const entry = fs.lstatSync(directory);
|
|
106
|
+
if (!entry.isDirectory() || entry.isSymbolicLink())
|
|
107
|
+
return;
|
|
108
|
+
fs.rmSync(filePath, { force: true });
|
|
109
|
+
fs.rmdirSync(directory);
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
}
|
|
113
|
+
}
|
package/docs/backend.md
CHANGED
|
@@ -25,6 +25,7 @@ The Core remains a non-detached child of `sashd`. Runtime transitions, disk muta
|
|
|
25
25
|
- `src/daemon.ts`: public facade re-exporting the daemon surface.
|
|
26
26
|
- `src/daemon/router.ts`: the single URLPattern route table plus match → auth → dispatch; `src/daemon/handlers/*` own the per-domain handlers, `src/daemon/errors.ts` maps domain errors onto the unified error envelope, and `src/daemon/context.ts` holds the shared `DaemonContext`/`DaemonGate`.
|
|
27
27
|
- `src/daemon/app.ts`: service assembly around one mutation queue; `src/daemon/server.ts` owns the HTTP/WebSocket listeners and listener close; `src/daemon/scheduler.ts` owns profile auto-update timers; `src/daemon/entry.ts` is the production entrypoint.
|
|
28
|
+
- `src/daemon/web-auth.ts`: bounded in-memory bootstrap/session credentials. `src/web-bootstrap.ts` writes the private browser handoff used by `sash web`.
|
|
28
29
|
- `src/runtime-lifecycle.ts`: serialized Core/proxy state transitions.
|
|
29
30
|
- `src/supervisor.ts`: child ownership, readiness probes and verified termination.
|
|
30
31
|
- `src/daemon-lifecycle.ts`: daemon discovery, singleton startup, CLI shutdown and the maintenance boundary used by full restarts and Core updates.
|
|
@@ -105,7 +106,9 @@ There are exactly two HTTP namespaces plus the static dashboard. Every `/sash/*`
|
|
|
105
106
|
|
|
106
107
|
| Endpoint | Method | Auth | Description |
|
|
107
108
|
| :--- | :--- | :--- | :--- |
|
|
108
|
-
| `/sash/daemon/health` | `GET` | public | Readiness, PID, start time and per-boot
|
|
109
|
+
| `/sash/daemon/health` | `GET` | public | Readiness, PID, start time and per-boot identity nonce; never a credential. |
|
|
110
|
+
| `/sash/web/bootstrap` | `POST` | control | Mint a single-use browser handoff, returning `{token, expiresAt}`. |
|
|
111
|
+
| `/sash/web/session` | `POST` | bootstrap body | Exchange `{token}` for a private browser session `{token, daemonToken}`. |
|
|
109
112
|
| `/sash/daemon/status` | `GET` | public | Daemon/Core/proxy/public-settings snapshot; `core.tunActive` is the verified runtime TUN state when available, while proxy `appliedKnown`/`stateKnown` and `queryError` preserve OS observation uncertainty. |
|
|
110
113
|
| `/sash/daemon/shutdown` | `POST` | control | Under the daemon mutation queue, snapshot whether Core was running, restore proxy/stop Core, return `{coreWasRunning}`, then close. Cleanup failure returns `500` and leaves the daemon available for retry. |
|
|
111
114
|
| `/sash/core/start` | `POST` | control | Rebuild config, start and wait for readiness; returns `{pid, version?, tunActive?}`. |
|
|
@@ -150,21 +153,26 @@ Traffic/log streams are authenticated `GET` WebSocket upgrades under `/core/api/
|
|
|
150
153
|
## 4. Control-Request Security
|
|
151
154
|
|
|
152
155
|
- The daemon listener binds only to `127.0.0.1`, rejects non-loopback Host headers, and only accepts loopback Core controller addresses.
|
|
153
|
-
- State-changing methods and every HTTP Core-gateway route require the persistent CLI bearer or
|
|
156
|
+
- State-changing methods and every HTTP Core-gateway route require the persistent CLI bearer or a private WebUI session token. The sole mutation exception, `POST /sash/web/session`, validates its single-use bootstrap credential independently. Any request carrying a non-loopback Origin header is rejected outright, regardless of method.
|
|
154
157
|
- WebSocket upgrades validate loopback Origin, authentication and route boundaries.
|
|
155
158
|
- Public settings/status contracts omit controller and daemon secrets.
|
|
159
|
+
- Public health's `token` identifies the daemon boot for lifecycle checks; HTTP and WebSocket authorization never accept it. JSON API responses use `Cache-Control: no-store`.
|
|
156
160
|
- Controller and daemon clients use a direct dispatcher with normal TLS verification; proxy environment variables apply only to remote downloads.
|
|
157
161
|
- Every managed runtime and OS/browser/package helper child removes GitHub/npm tokens, npm credential-file/auth variables and npm registry credentials. Fixed Windows/macOS system tools use trusted absolute paths; Linux desktop helpers are resolved only through absolute PATH entries.
|
|
158
162
|
|
|
163
|
+
`sash web` authenticates with the CLI bearer to mint a 90-second, single-use bootstrap token. It writes an atomic HTML handoff in a new `<root>/temp/web-bootstrap-<expiry>-<random>/` directory: POSIX directories/files use `0700`/`0600`, and Windows directories are created with a protected owner-only DACL before writing credentials. Only the non-secret file URL reaches the browser launcher and only the ordinary dashboard address reaches CLI output. Expired handoff directories are cleaned on the next invocation; live handoffs survive concurrent invocations and browser cold starts.
|
|
164
|
+
|
|
165
|
+
The document navigates to the dashboard with a fragment containing the bootstrap token. The frontend removes the fragment before making requests, then exchanges it for a session through the request body. The daemon stores only token hashes in memory, with at most 32 pending bootstraps and 256 sessions; oldest entries are evicted at capacity. Redemption consumes a token before issuing its session, and a restart invalidates both collections. The response's public `daemonToken` binds browser storage to the issuing boot. The persistent CLI bearer never enters the browser handoff.
|
|
166
|
+
|
|
159
167
|
---
|
|
160
168
|
|
|
161
169
|
## 5. Settings, Profiles and Config Transactions
|
|
162
170
|
|
|
163
|
-
`sash.json` has explicit `schemaVersion: 1`. Loading validates the JSON root, every field type, nonblank control-character-free secrets, port range, loopback controller address, unknown keys and all three listener ports (`mixedPort`, controller and daemon) for collisions. Version-0 files and removed version metadata migrate to canonical v1. Invalid or future-version files are never overwritten.
|
|
171
|
+
`sash.json` has explicit `schemaVersion: 1`. Loading validates the JSON root, every field type, nonblank control-character-free secrets, port range, loopback controller address, unknown keys and all three listener ports (`mixedPort`, controller and daemon) for collisions. Version-0 files and removed version metadata migrate to canonical v1. In 0.1.1, otherwise valid legacy files also migrate enabled TUN to false under the settings lock using the atomic writer. Invalid or future-version files are never overwritten.
|
|
164
172
|
|
|
165
173
|
After managed-state recovery, daemon and offline initialization first migrate a nonblank legacy `subscriptionUrl` into an active meta-only profile. That URL has priority over any pre-profile `config.yaml`. Only when `profiles/index.json` does not exist may Sash import `config.yaml` once as the active local `Imported config` profile. A present empty index is an explicit opt-out. The candidate must be bounded, regular, valid core-format YAML and contain non-default routing content after managed operational keys are stripped: nonempty proxies/providers, or nonempty rules/groups that differ from the Sash DIRECT-only default. Exact generated defaults are not imported. Invalid candidates fail initialization without changing `config.yaml`; successful import journals the profile YAML and index under `mutation.lock` while leaving `config.yaml` byte-for-byte in place. Later `ProfileService` preparation re-renders and, when Core is installed, validates the canonical profile-derived candidate before runtime use.
|
|
166
174
|
|
|
167
|
-
`SettingsService` snapshots committed settings, creates an immutable canonical candidate, then fetches/renders/Core-validates active profile configuration outside the mutation lock. Under the short commit boundary it rechecks settings/profile snapshots and journals settings plus generated config before publication. The daemon exposes only `committedSettings` to GET/status/auth handlers; Core spawn/restart can temporarily use `runtimeSettings` while a candidate transition is in progress. The committed in-memory snapshot changes only after the journaled transition succeeds; failure restores disk/config and the old runtime.
|
|
175
|
+
`SettingsService` snapshots committed settings, creates an immutable canonical candidate, then fetches/renders/Core-validates active profile configuration outside the mutation lock. Under the short commit boundary it rechecks settings/profile snapshots and journals settings plus generated config before publication. The daemon exposes only `committedSettings` to GET/status/auth handlers; Core spawn/restart can temporarily use `runtimeSettings` while a candidate transition is in progress. The committed in-memory snapshot changes only after the journaled transition succeeds; failure restores disk/config and the old runtime. Version 0.1.1 rejects TUN enable requests before configuration publication or runtime transitions. Generated configs explicitly disable the TUN listener; original profile DNS/provider options are preserved.
|
|
168
176
|
|
|
169
177
|
Prepared profile work is never exposed as a mutable internal transaction object. `ProfileService` issues WeakMap-backed, one-shot opaque capabilities that reject forgery, cross-instance use and repeated consumption. Settings publication deliberately binds a weak active-source snapshot (`activeId`, profile identity/URL and exact raw YAML digest), so unrelated metadata updates do not invalidate an otherwise safe settings change. A strict active reload instead binds the committed settings, complete active `ProfileMeta`, active selection and raw digest. Both paths prepare outside the mutation boundary and consume the capability only while rechecking and publishing; only a conflict raised before the publication callback is entered receives the single bounded automatic retry.
|
|
170
178
|
|
package/docs/frontend.md
CHANGED
|
@@ -13,6 +13,12 @@ The WebUI is a Vue 3 application built with Vite and bundled into `dist/ui/`. sa
|
|
|
13
13
|
- Vite 6 builds the bundled dashboard on the declared Node.js 24 baseline.
|
|
14
14
|
- CI runs lint, server/WebUI type checks, all tests, production builds and actual tarball pack/install/CLI/UI smoke on Windows, macOS and Linux with Node.js 24.
|
|
15
15
|
|
|
16
|
+
The `cn-font-split` dependency has a scoped `koffi: 2.16.3` override. The [Koffi changelog](https://koffi.dev/changelog) documents Node.js 24.14+ teardown fixes relevant to the native font splitter.
|
|
17
|
+
|
|
18
|
+
Workflows using `npm ci --ignore-scripts` explicitly initialize the tested native font subsetter with `node node_modules/cn-font-split/dist/cli.js i default@7.6.8` before building. The npm wrapper and native subsetter have separate versions; 7.6.8 is the native release used by the validated builds. This runs the selected build tool directly while keeping automatic dependency lifecycle hooks disabled.
|
|
19
|
+
|
|
20
|
+
`npx tsx scripts/web-auth-ui-verify.mts` checks the real private-file/browser/daemon authorization exchange in both engines, including bare URLs, refresh, replay, daemon restart, reauthorization and the Settings/Overview layouts without TUN controls. It uses isolated temporary roots and ports, a fake Core and system proxy. Screenshots and a report remain in the OS temporary directory.
|
|
21
|
+
|
|
16
22
|
---
|
|
17
23
|
|
|
18
24
|
## 2. Source Layout
|
|
@@ -20,6 +26,7 @@ The WebUI is a Vue 3 application built with Vite and bundled into `dist/ui/`. sa
|
|
|
20
26
|
```text
|
|
21
27
|
web/src/
|
|
22
28
|
├── api/index.ts typed REST client and WebSocket reconnect logic
|
|
29
|
+
├── api/session.ts bootstrap exchange and per-tab credential ownership
|
|
23
30
|
├── components/
|
|
24
31
|
│ ├── AppSidebar.vue
|
|
25
32
|
│ ├── ConfirmDialog.vue
|
|
@@ -34,6 +41,7 @@ web/src/
|
|
|
34
41
|
├── styles/main.css design tokens and shared utility/component styles
|
|
35
42
|
├── types/index.ts core-controller response types; daemon types are shared
|
|
36
43
|
├── views/
|
|
44
|
+
│ ├── ConnectionView.vue read-only browser authorization instructions
|
|
37
45
|
│ ├── OverviewView.vue status, traffic, modes and proxy groups
|
|
38
46
|
│ ├── ProfilesView.vue download/import/update/select/delete profiles
|
|
39
47
|
│ ├── LogsView.vue
|
|
@@ -66,9 +74,9 @@ Canonical actions include:
|
|
|
66
74
|
- runtime intent actions such as `setOutboundMode()` and `selectGroupProxy()`
|
|
67
75
|
- `startRuntimePolling()`
|
|
68
76
|
|
|
69
|
-
Polling is self-scheduling with `setTimeout` after the previous cycle completes. It slows to a 15-second interval while the page is hidden and refreshes immediately after returning to the foreground. Domain request generations discard responses made stale by a newer refresh or user mutation. Core-specific API calls
|
|
77
|
+
Polling is self-scheduling with `setTimeout` after the previous cycle completes. It slows to a 15-second interval while the page is hidden and refreshes immediately after returning to the foreground. Domain request generations discard both successful responses and resource errors made stale by a newer refresh or user mutation; old polls cannot overwrite a committed settings response. Core-specific API calls require an authorized browser session and status reporting `running && healthy`.
|
|
70
78
|
|
|
71
|
-
Daemon reachability, profile revision and Core snapshots have separate ownership. A successful `/sash/status` keeps the daemon online even when a downstream Core gateway request returns 502. Profiles track their last fetched daemon revision independently and refresh on revision changes even while Core is stopped. A daemon restart resets that revision comparison.
|
|
79
|
+
Daemon reachability, profile revision and Core snapshots have separate ownership. A successful `/sash/daemon/status` keeps the daemon online even when a downstream Core gateway request returns 502. Profiles track their last fetched daemon revision independently and refresh on revision changes even while Core is stopped. A daemon restart resets that revision comparison.
|
|
72
80
|
|
|
73
81
|
The Core owner is the daemon boot plus Core PID/start time. A stopped/unhealthy Core or unreachable daemon clears proxy groups, rules, connections/totals and traffic rates/history. A same-owner Core API failure preserves the last complete configs/proxies/rules/connections snapshot, marks it degraded and retries; a changed owner clears the old snapshot before fetching, so failed replacement data cannot be shown under the new owner. Profile revision changes request a new snapshot without prematurely discarding same-owner data.
|
|
74
82
|
|
|
@@ -98,6 +106,8 @@ System proxy controls are target-state based: enabling requires a running, healt
|
|
|
98
106
|
|
|
99
107
|
## 5. API and Streaming
|
|
100
108
|
|
|
109
|
+
`web/src/api/session.ts` consumes and immediately removes the one-time `sash web` fragment, then redeems it exactly once. Concurrent initialization shares that exchange. Private session credentials and their public daemon nonce are saved in `sessionStorage`, allowing a tab to reload without exposing credentials in ordinary URLs. Storage denial falls back to memory. Health remains public discovery only; a different boot nonce clears the stored session. Stale completions and old-token `401` responses cannot replace or revoke a newer authorization. Without authorization, the shell renders `ConnectionView`, continues public reachability/status polling and does not open Core streams or expose mutation controls.
|
|
110
|
+
|
|
101
111
|
`web/src/api/index.ts`:
|
|
102
112
|
|
|
103
113
|
- distinguishes JSON and `204 No Content` endpoints through overloads rather than `undefined as T`;
|
|
@@ -119,7 +129,7 @@ The controller secret is never available to browser code; sashd injects it serve
|
|
|
119
129
|
|
|
120
130
|
## 6. Interaction State
|
|
121
131
|
|
|
122
|
-
- Settings derive mixed-port dirty state
|
|
132
|
+
- Settings derive mixed-port dirty state from the committed value, preserve edited drafts across polling and offer reset. LAN and system-proxy mutations immediately adopt the committed response. If the subsequent runtime refresh fails, the UI reports that settings were saved and verification is temporarily unavailable. TUN controls and service administration are outside the 0.1.1 dashboard.
|
|
123
133
|
- Logs receive monotonic IDs before entering the capped 600-row buffer, providing stable Vue keys and an update sequence even when length remains constant.
|
|
124
134
|
- The global confirm service settles a previous pending Promise before opening another dialog; Escape, route changes and component unmount cancel the active confirmation.
|
|
125
135
|
- The global banner distinguishes an unreachable daemon from a degraded same-owner Core snapshot and an unavailable new-owner snapshot.
|
package/docs/usage.md
CHANGED
|
@@ -101,8 +101,10 @@ Sash does not blindly turn off an existing proxy. It stores a private ownership
|
|
|
101
101
|
|
|
102
102
|
| Command | Description |
|
|
103
103
|
| :--- | :--- |
|
|
104
|
-
| `sash web` |
|
|
105
|
-
| `sash web --no-open` | Print the dashboard URL without opening a browser. |
|
|
104
|
+
| `sash web` | Authorize this browser and open `http://127.0.0.1:19090/ui/`. |
|
|
105
|
+
| `sash web --no-open` | Print the dashboard URL without opening or authorizing a browser. |
|
|
106
|
+
|
|
107
|
+
Run `sash web` as the user who runs Sash, with the same `SASH_HOME`. It uses a private local handoff to authorize the browser automatically. A bare dashboard address shows a read-only connection page; it cannot obtain control access from public health information. The handoff expires after 90 seconds and works once. If it expires before the browser opens, rerun the command. An authorized tab can be refreshed, but a daemon restart requires a new `sash web` authorization. Browser storage restrictions keep the session in memory only, so those browsers also require authorization after a page reload.
|
|
106
108
|
|
|
107
109
|
### Profiles
|
|
108
110
|
|
|
@@ -110,7 +112,7 @@ Profiles are managed from the WebUI Profiles page: download from a subscription
|
|
|
110
112
|
|
|
111
113
|
### Settings
|
|
112
114
|
|
|
113
|
-
Runtime settings (`
|
|
115
|
+
Runtime settings (`mixedPort`, `allowLan`, `systemProxy`) are managed from the WebUI Settings page, and the entire `sash.json` can be edited as JSON from the same page ("Edit settings file"); invalid documents are rejected without touching the disk. `daemonSecret` changes apply immediately; `daemonPort` changes are saved but require a manual `sash restart` to rebind the listener.
|
|
114
116
|
|
|
115
117
|
### Maintenance & Upgrades
|
|
116
118
|
|
|
@@ -131,13 +133,13 @@ Core updates download and validate before shutdown, then use an authenticated ma
|
|
|
131
133
|
| Key | Default | Description |
|
|
132
134
|
| :--- | :--- | :--- |
|
|
133
135
|
| `schemaVersion` | `1` | On-disk settings schema; managed by Sash. |
|
|
134
|
-
| `mixedPort` | `7890` | Local HTTP/SOCKS5 mixed inbound port.
|
|
136
|
+
| `mixedPort` | `7890` | Local HTTP/SOCKS5 mixed inbound port. |
|
|
135
137
|
| `controller` | `127.0.0.1:9090` | Internal controller listen address; only loopback hosts are accepted. |
|
|
136
138
|
| `daemonPort` | `19090` | Daemon API and WebUI port. |
|
|
137
139
|
| `secret` | *(random)* | Internal controller secret. It is never returned by the public status API. |
|
|
138
140
|
| `daemonSecret` | *(random)* | CLI bearer secret for state-changing daemon requests. |
|
|
139
141
|
| `systemProxy` | `false` | Desired OS-level system proxy state. |
|
|
140
|
-
| `tun` | `false` |
|
|
142
|
+
| `tun` | `false` | Legacy compatibility field; always off in 0.1.1. Existing true values migrate to false on load; new true values are rejected. |
|
|
141
143
|
| `allowLan` | `false` | Accept proxy traffic from other devices. |
|
|
142
144
|
|
|
143
145
|
A legacy `subscriptionUrl` key is migrated once into `profiles/index.json` and then removed. It has priority over legacy `config.yaml` import. If no `profiles/index.json` has ever been created, startup/offline initialization may import an existing `config.yaml` once as the active local profile named `Imported config` (`url: ""`, updates disabled). A present empty index opts out. To avoid importing Sash's own generated default, the file must be valid core-format YAML and contain non-default routing content after managed keys are removed: nonempty proxies/providers, or nonempty rules/groups differing from the DIRECT-only default. The runtime `config.yaml` is kept unchanged during import; later profile application re-renders and validates it. Invalid YAML/config fails closed without overwriting the file.
|
|
@@ -150,39 +152,11 @@ Settings changes are prepared as an all-or-nothing candidate: active configurati
|
|
|
150
152
|
|
|
151
153
|
---
|
|
152
154
|
|
|
153
|
-
## 4.
|
|
154
|
-
|
|
155
|
-
TUN requires the whole Sash daemon to run with elevated privileges. Stop the current daemon and save the setting while Sash is offline — toggle it in the WebUI Settings page before stopping, or set `"tun": true` in `sash.json` directly:
|
|
156
|
-
|
|
157
|
-
```sh
|
|
158
|
-
sash stop
|
|
159
|
-
# ensure TUN is on (WebUI Settings page, or edit sash.json)
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
On Windows, open PowerShell as Administrator:
|
|
163
|
-
|
|
164
|
-
```powershell
|
|
165
|
-
sash start
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
The default `%LOCALAPPDATA%\Sash` data root remains the same when the current Windows user elevates. Only copy an explicitly customized `SASH_HOME` into the Administrator shell.
|
|
169
|
-
|
|
170
|
-
On macOS or Linux, `sudo` can change the default home directory. Read the current data root and pass it explicitly while starting Sash as root:
|
|
171
|
-
|
|
172
|
-
```sh
|
|
173
|
-
sash status # note the printed data root
|
|
174
|
-
sudo env SASH_HOME='<data root printed above>' "$(command -v sash)" start
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
While that elevated daemon is running on macOS or Linux, use the same `sudo` and `SASH_HOME` prefix for later lifecycle commands such as `status` or `stop`, so they target the same runtime and can read its protected state.
|
|
178
|
-
|
|
179
|
-
If TUN was already saved as on, just start Sash elevated. Because a full `sash restart` replaces the daemon itself, running it from the elevated shell is equivalent to `sash stop` + `sash start` here; restarting only the Core from the dashboard does not elevate `sashd`.
|
|
180
|
-
|
|
181
|
-
Sash distinguishes the desired setting from the Core's actual runtime state: `sash status` reports `on (active)`, `on (inactive)`, `on (unverified)` or `on (runtime unknown)`, and `sash status --json` reports `tun.desired` separately from `tun.active` (`true`, `false` or `null`). Privilege guidance is shown only after a responsive, healthy running Core explicitly reports inactive or unverified TUN state.
|
|
155
|
+
## 4. Network Scope in 0.1.1
|
|
182
156
|
|
|
183
|
-
|
|
157
|
+
This release supports local HTTP/SOCKS endpoints, profile rules and reversible system-proxy integration. TUN and Windows Service Mode are developed on the [feat/tun-service-mode branch](https://github.com/ming-kang/Sash/tree/feat/tun-service-mode).
|
|
184
158
|
|
|
185
|
-
|
|
159
|
+
The dashboard has no TUN switch or service installation controls. Settings updates reject `tun: true`; loading an otherwise valid older settings file migrates that value to `false` atomically. Generated configurations explicitly set `tun.enable: false`, including on reload. Profiles containing a separate TUN listener are rejected before publication. Original profile files and profile-owned DNS/provider settings remain unchanged. The JSON status contract retains its TUN observation fields for compatibility.
|
|
186
160
|
|
|
187
161
|
---
|
|
188
162
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astralyn/sash",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "A
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "A network toolbox for developers, learning, and research, with a command-line companion and built-in web dashboard.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "ming-kang",
|
|
7
7
|
"type": "module",
|
|
@@ -47,8 +47,7 @@
|
|
|
47
47
|
"network",
|
|
48
48
|
"toolbox",
|
|
49
49
|
"rules",
|
|
50
|
-
"dashboard"
|
|
51
|
-
"tun"
|
|
50
|
+
"dashboard"
|
|
52
51
|
],
|
|
53
52
|
"publishConfig": {
|
|
54
53
|
"access": "public"
|
|
@@ -60,6 +59,11 @@
|
|
|
60
59
|
"undici": "^7.29.0",
|
|
61
60
|
"yaml": "^2.7.0"
|
|
62
61
|
},
|
|
62
|
+
"overrides": {
|
|
63
|
+
"cn-font-split": {
|
|
64
|
+
"koffi": "2.16.3"
|
|
65
|
+
}
|
|
66
|
+
},
|
|
63
67
|
"devDependencies": {
|
|
64
68
|
"@biomejs/biome": "^2.5.5",
|
|
65
69
|
"@codemirror/legacy-modes": "^6.5.4",
|
package/dist/tun-guidance.js
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
function quotePosix(value) {
|
|
2
|
-
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
3
|
-
}
|
|
4
|
-
export function tunPrivilegeGuidance(context, options) {
|
|
5
|
-
const platform = options.platform ?? process.platform;
|
|
6
|
-
const prepare = context === "activation-rolled-back" ? 'Run "sash config set tun on" first. ' : "";
|
|
7
|
-
if (platform === "win32") {
|
|
8
|
-
return `${prepare}Open PowerShell as Administrator and run "sash restart". If SASH_HOME was explicitly customized, set the same value in that shell first. If Sash was already elevated, inspect the Core error log.`;
|
|
9
|
-
}
|
|
10
|
-
return `${prepare}Restart Sash with root privileges using the same data directory: sudo env SASH_HOME=${quotePosix(options.root)} "$(command -v sash)" restart. If Sash was already elevated, inspect the Core error log.`;
|
|
11
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{d as m,C as p,af as c,z as f,o as y,m as S,u as v,t as a,p as n,aj as _,y as o}from"./index-B61V60w_.js";import{C}from"./CodeEditorModal-CFEnWsyh.js";import"./theme-BNq4FkXS.js";const k=m({__name:"SettingsFileDialog",emits:["close"],setup(h,{emit:d}){const r=d,l=n(""),i=n(!0),s=n(!1),u=n(null);p(async()=>{try{const e=await c.getSettingsFile();l.value=e.content}catch(e){u.value=f(e)}finally{i.value=!1}});async function g(e){if(!s.value){s.value=!0;try{const t=await c.saveSettingsFile(e);await _(),t.restartRequired?o.success(a("toast.settingsSavedRestart")):o.success(a("toast.settingSaved")),r("close")}catch(t){o.error(a("toast.failed",{msg:f(t)}))}finally{s.value=!1}}}return(e,t)=>(y(),S(C,{title:v(a)("settings.fileTitle"),hint:v(a)("settings.fileHint"),content:l.value,language:"json",layout:"constrained",loading:i.value,"load-error":u.value,saving:s.value,onSave:g,onClose:t[0]||(t[0]=w=>r("close"))},null,8,["title","hint","content","loading","load-error","saving"]))}});export{k as default};
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["./SettingsFileDialog-CNFEAVs4.js","./index-B61V60w_.js","./index-bkyxJG8J.css","./CodeEditorModal-CFEnWsyh.js","./theme-BNq4FkXS.js","./CodeEditorModal-6m0TJWyF.css"])))=>i.map(i=>d[i]);
|
|
2
|
-
import{d as B,o as u,c as m,e as s,g as a,i as f,a7 as M,_ as U,n as _,u as t,t as e,p as k,s as o,w as J,a8 as K,a as r,b as C,I as x,j as Q,P as W,m as X,l as T,f as tt,v as st,a9 as $,aa as et,Y as at,Z as nt,ab as it,q as P,ac as ot,ad as lt,ae as dt,y as h,af as A,ag as F,z as S,ah as ct}from"./index-B61V60w_.js";import{t as b,s as rt}from"./theme-BNq4FkXS.js";const ut={class:"ui-card"},gt={key:0,class:"ui-card-head"},_t={class:"ui-card-heading"},mt={key:0,class:"ui-card-title"},vt={key:1,class:"ui-card-desc"},pt={key:0,class:"ui-card-actions"},ht={class:"ui-card-body"},bt=B({__name:"UiCard",props:{title:{},desc:{}},setup(l){return(v,g)=>(u(),m("section",ut,[l.title||l.desc||v.$slots.actions?(u(),m("header",gt,[s("div",_t,[l.title?(u(),m("h2",mt,a(l.title),1)):f("",!0),l.desc?(u(),m("p",vt,a(l.desc),1)):f("",!0)]),v.$slots.actions?(u(),m("div",pt,[M(v.$slots,"actions",{},void 0)])):f("",!0)])):f("",!0),s("div",ht,[M(v.$slots,"default",{},void 0)])]))}}),D=U(bt,[["__scopeId","data-v-042826e5"]]),ft=["aria-checked","aria-label","disabled"],yt=B({__name:"UiSwitch",props:{modelValue:{type:Boolean},disabled:{type:Boolean},label:{}},emits:["update:modelValue"],setup(l){return(v,g)=>(u(),m("button",{type:"button",role:"switch","aria-checked":l.modelValue,"aria-label":l.label??t(e)("common.toggle"),class:_(["switch",{on:l.modelValue}]),disabled:l.disabled,onClick:g[0]||(g[0]=y=>v.$emit("update:modelValue",!l.modelValue))},[...g[1]||(g[1]=[s("span",{class:"knob","aria-hidden":"true"},null,-1)])],10,ft))}}),O=U(yt,[["__scopeId","data-v-306af9c5"]]),wt={class:"settings-grid"},kt={class:"setting-row"},Ct={class:"setting-info"},Tt={class:"setting-name"},$t={class:"setting-desc"},Dt=["aria-label"],Vt=["aria-pressed"],xt=["aria-pressed"],Pt=["aria-pressed"],St={class:"setting-row"},Bt={class:"setting-info"},Ut={class:"setting-name"},zt={class:"setting-desc"},It=["aria-label"],Lt=["aria-pressed"],Nt=["aria-pressed"],Et={class:"setting-row interrupt-row"},Rt={class:"setting-info"},Mt={class:"setting-name",for:"mixed-port"},At={class:"setting-desc"},Ft={class:"setting-action port-action"},Ot=["aria-label","disabled"],jt=["disabled"],qt=["disabled"],Ht={class:"setting-row"},Yt={class:"setting-info"},Zt={class:"setting-name"},Gt={class:"setting-desc"},Jt={class:"setting-action"},Kt={class:"setting-row caution-row"},Qt={class:"setting-info"},Wt={class:"setting-name"},Xt={class:"setting-desc"},ts={class:"setting-action"},ss=["title"],es={class:"setting-row danger-row"},as={class:"setting-info"},ns={class:"setting-name"},is={class:"setting-desc"},os={class:"setting-action"},ls=["disabled"],ds={class:"setting-row"},cs={class:"setting-info"},rs={class:"setting-name"},us={class:"setting-desc"},gs={class:"setting-action"},_s=["disabled"],ms={class:"info-grid"},vs={class:"info-item"},ps={class:"mono"},hs={class:"info-item"},bs={class:"mono"},fs={class:"info-item"},ys={class:"mono"},ws=B({__name:"SettingsView",setup(l){const v=at(()=>nt(()=>import("./SettingsFileDialog-CNFEAVs4.js"),__vite__mapDeps([0,1,2,3,4,5]),import.meta.url)),g=k(!1),y=k(o.status?.settings.mixedPort??7890),c=k(y.value),p=k(!1),{restarting:z,restartCore:I}=it(),w=k(!1);J(()=>o.status?.settings.mixedPort,n=>{if(n===void 0)return;const i=y.value;y.value=n,c.value=K(c.value,i,n,p.value)});const L=P(()=>ot(c.value,y.value)),N=P(()=>Number.isInteger(c.value)&&c.value>=1&&c.value<=65535&&L.value),j=P(()=>{switch(lt.value){case"inactive":return e("settings.tunInactiveDesc");case"unverified":return e("settings.tunUnverifiedDesc");case"unexpected-active":return e("settings.tunUnexpectedDesc");default:return e("settings.tunDesc")}});function V(n){n!==b.value&&rt(n)}function E(n){n!==T.value&&(dt(n),h.success(e("toast.langSwitched")))}function q(){c.value=y.value}async function H(){if(!(!N.value||p.value)){p.value=!0;try{const n=await A.patchSettings({mixedPort:c.value});o.status&&(o.status={...o.status,settings:n.settings}),await F(),h.success(e("toast.portSaved"))}catch(n){h.error(e("toast.failed",{msg:S(n)}))}finally{p.value=!1}}}async function R(n,i){try{await ct(n,i),h.success(e("toast.settingSaved"))}catch(d){h.error(e("toast.failed",{msg:S(d)}))}}function Y(n){R("allow-lan",n)}function Z(n){R("tun",n)}async function G(){if(!w.value){w.value=!0;try{const n=await A.reloadCoreConfig();await F(),h.success(e("toast.configReloaded",{n:n.proxyCount}))}catch(n){h.error(e("toast.failed",{msg:S(n)}))}finally{w.value=!1}}}return(n,i)=>(u(),m("div",null,[r(W,{title:t(e)("page.settings.title"),desc:t(e)("page.settings.desc")},{default:C(()=>[s("button",{type:"button",class:"btn btn-secondary btn-sm",onClick:i[0]||(i[0]=d=>g.value=!0)},[r(x,{name:"code",size:14}),Q(" "+a(t(e)("settings.editFile")),1)])]),_:1},8,["title","desc"]),g.value?(u(),X(t(v),{key:0,onClose:i[1]||(i[1]=d=>g.value=!1)})):f("",!0),s("div",wt,[r(D,{title:t(e)("settings.appearanceTitle"),class:"settings-card"},{default:C(()=>[s("div",kt,[s("div",Ct,[s("span",Tt,a(t(e)("settings.themeTitle")),1),s("span",$t,a(t(e)("settings.appearanceDesc")),1)]),s("div",{class:"segmented preference-control",role:"group","aria-label":t(e)("settings.appearanceTitle")},[s("button",{type:"button",class:_(["segmented-item",{active:t(b)==="system"}]),"aria-pressed":t(b)==="system",onClick:i[2]||(i[2]=d=>V("system"))},a(t(e)("theme.system")),11,Vt),s("button",{type:"button",class:_(["segmented-item",{active:t(b)==="light"}]),"aria-pressed":t(b)==="light",onClick:i[3]||(i[3]=d=>V("light"))},a(t(e)("theme.light")),11,xt),s("button",{type:"button",class:_(["segmented-item",{active:t(b)==="dark"}]),"aria-pressed":t(b)==="dark",onClick:i[4]||(i[4]=d=>V("dark"))},a(t(e)("theme.dark")),11,Pt)],8,Dt)]),s("div",St,[s("div",Bt,[s("span",Ut,a(t(e)("settings.langTitle")),1),s("span",zt,a(t(e)("settings.langDesc")),1)]),s("div",{class:"segmented preference-control language-control",role:"group","aria-label":t(e)("settings.langTitle")},[s("button",{type:"button",class:_(["segmented-item",{active:t(T)==="zh"}]),"aria-pressed":t(T)==="zh",onClick:i[5]||(i[5]=d=>E("zh"))}," 中文 ",10,Lt),s("button",{type:"button",class:_(["segmented-item",{active:t(T)==="en"}]),"aria-pressed":t(T)==="en",onClick:i[6]||(i[6]=d=>E("en"))}," English ",10,Nt)],8,It)])]),_:1},8,["title"]),r(D,{title:t(e)("settings.networkTitle"),class:"settings-card"},{default:C(()=>[s("div",Et,[s("div",Rt,[s("label",Mt,a(t(e)("settings.mixedPortTitle")),1),s("span",At,a(t(e)("settings.mixedPortDesc")),1)]),s("div",Ft,[tt(s("input",{id:"mixed-port","onUpdate:modelValue":i[7]||(i[7]=d=>c.value=d),type:"number",min:"1",max:"65535",class:"input input-sm port-input","aria-label":t(e)("settings.mixedPortTitle"),disabled:p.value||!t(o).status},null,8,Ot),[[st,c.value,void 0,{number:!0}]]),L.value?(u(),m("button",{key:0,type:"button",class:"btn btn-secondary btn-sm",disabled:p.value||!t(o).status,onClick:q},a(t(e)("common.reset")),9,jt)):f("",!0),s("button",{type:"button",class:"btn btn-secondary btn-sm interrupt-save",disabled:p.value||!N.value||!t(o).status,onClick:H},a(p.value?t(e)("common.loading"):t(e)("common.save")),9,qt)])]),s("div",Ht,[s("div",Yt,[s("span",Zt,a(t(e)("settings.allowLanTitle")),1),s("span",Gt,a(t(e)("settings.allowLanDesc")),1)]),s("div",Jt,[r(O,{"model-value":t(o).status?.settings.allowLan??!1,label:t(e)("settings.allowLanTitle"),disabled:t(o).operations.networkSetting||!t(o).status,"onUpdate:modelValue":Y},null,8,["model-value","label","disabled"])])]),s("div",Kt,[s("div",Qt,[s("span",Wt,a(t(e)("settings.tunTitle")),1),s("span",Xt,a(j.value),1)]),s("div",ts,[t($)?(u(),m("span",{key:0,class:_(["badge",t($).className]),title:t($).title},a(t($).text),11,ss)):f("",!0),r(O,{"model-value":t(o).status?.settings.tun??!1,label:t(e)("settings.tunTitle"),disabled:t(o).operations.networkSetting||!t(o).status,"onUpdate:modelValue":Z},null,8,["model-value","label","disabled"])])])]),_:1},8,["title"]),r(D,{title:t(e)("settings.coreTitle"),class:"settings-card"},{default:C(()=>[s("div",es,[s("div",as,[s("span",ns,a(t(e)("settings.restartTitle")),1),s("span",is,a(t(e)("settings.restartDesc")),1)]),s("div",os,[s("button",{type:"button",class:"btn btn-sm danger-action",disabled:t(z),onClick:i[8]||(i[8]=(...d)=>t(I)&&t(I)(...d))},[r(x,{name:"power",size:13,class:_({spin:t(z)})},null,8,["class"]),s("span",null,a(t(e)("settings.restartBtn")),1)],8,ls)])]),s("div",ds,[s("div",cs,[s("span",rs,a(t(e)("settings.reloadTitle")),1),s("span",us,a(t(e)("settings.reloadDesc")),1)]),s("div",gs,[s("button",{type:"button",class:"btn btn-secondary btn-sm",disabled:w.value,onClick:G},[r(x,{name:"refresh",size:13,class:_({spin:w.value})},null,8,["class"]),s("span",null,a(t(e)("settings.reloadBtn")),1)],8,_s)])])]),_:1},8,["title"]),r(D,{title:t(e)("settings.aboutTitle"),class:"settings-card runtime-card"},{default:C(()=>[s("dl",ms,[s("div",vs,[s("dt",null,a(t(e)("overview.daemonPort")),1),s("dd",ps,"127.0.0.1:"+a(t(o).status?.settings.daemonPort??19090),1)]),s("div",hs,[s("dt",null,a(t(e)("overview.controller")),1),s("dd",bs,a(t(o).status?.settings.controller||"-"),1)]),s("div",fs,[s("dt",null,a(t(e)("settings.coreVersion")),1),s("dd",ys,a(t(et)||"-"),1)])])]),_:1},8,["title"])])]))}}),Ts=U(ws,[["__scopeId","data-v-34dbfd82"]]);export{Ts as default};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
.ui-card[data-v-042826e5]{min-width:0}.ui-card-head[data-v-042826e5]{display:flex;min-height:28px;align-items:flex-end;justify-content:space-between;gap:16px;margin-bottom:5px}.ui-card-heading[data-v-042826e5]{min-width:0}.ui-card-title[data-v-042826e5]{color:var(--text-primary);font-size:18px;font-weight:400;line-height:1.4}.ui-card-desc[data-v-042826e5]{margin-top:1px;color:var(--text-muted);font-size:14px;line-height:1.35}.ui-card-actions[data-v-042826e5]{display:flex;align-items:center;flex-shrink:0;flex-wrap:wrap;justify-content:flex-end;gap:7px}.ui-card-body[data-v-042826e5]{padding:6px 10px;background:var(--bg-panel);border-radius:3px}@media(max-width:520px){.ui-card-head[data-v-042826e5]{flex-wrap:wrap}.ui-card-body[data-v-042826e5]{padding:6px 8px}}.switch[data-v-306af9c5]{position:relative;width:34px;height:20px;flex-shrink:0;padding:0;border:0;border-radius:var(--radius-full);background:var(--switch-off);cursor:pointer;transition:background var(--motion-normal) var(--ease-standard),opacity var(--motion-fast) var(--ease-standard)}.switch[data-v-306af9c5]:hover:not(:disabled){background:color-mix(in srgb,var(--switch-off) 84%,var(--text-primary))}.switch.on[data-v-306af9c5]{background:var(--switch-on)}.switch.on[data-v-306af9c5]:hover:not(:disabled){background:color-mix(in srgb,var(--switch-on) 84%,#000000)}.switch[data-v-306af9c5]:disabled{cursor:not-allowed;opacity:.48}.knob[data-v-306af9c5]{position:absolute;top:3px;left:3px;width:14px;height:14px;border-radius:var(--radius-full);background:var(--switch-knob);box-shadow:var(--shadow-switch);transition:background var(--motion-normal) var(--ease-standard),transform var(--motion-normal) var(--ease-spring)}.switch.on .knob[data-v-306af9c5]{transform:translate(14px)}.settings-grid[data-v-34dbfd82]{display:grid;width:min(1046px,100%);grid-template-columns:1fr;gap:12px;margin:0 auto}.settings-card[data-v-34dbfd82]{min-width:0}.preference-control[data-v-34dbfd82]{display:grid;width:min(360px,48%);grid-template-columns:repeat(3,minmax(0,1fr));flex-shrink:0;gap:0}.language-control[data-v-34dbfd82]{grid-template-columns:repeat(2,minmax(0,1fr))}.preference-control .segmented-item[data-v-34dbfd82]{min-height:27px;border:0;border-radius:0;font-size:14px}.preference-control .segmented-item[data-v-34dbfd82]:first-child{border-radius:5px 0 0 5px}.preference-control .segmented-item[data-v-34dbfd82]:last-child{border-radius:0 5px 5px 0}.setting-row[data-v-34dbfd82]{display:flex;min-height:43px;align-items:center;justify-content:space-between;gap:24px;padding:6px 5px;border-bottom:0;transition:background var(--motion-fast) var(--ease-standard)}.setting-row[data-v-34dbfd82]:first-child{margin-top:-2px}.setting-row[data-v-34dbfd82]:last-child{border-bottom:0}.setting-row[data-v-34dbfd82]:hover{background:var(--general-row-hover);border-radius:3px}.caution-row .setting-name[data-v-34dbfd82]{color:var(--warning)}.danger-row .setting-name[data-v-34dbfd82]{color:var(--danger)}.setting-info[data-v-34dbfd82]{display:flex;min-width:0;flex-direction:column;gap:2px}.setting-name[data-v-34dbfd82]{color:var(--text-primary);font-size:16px;font-weight:400}.setting-desc[data-v-34dbfd82]{max-width:630px;color:var(--text-muted);font-size:14px;line-height:1.35}.setting-action[data-v-34dbfd82]{display:flex;align-items:center;flex-shrink:0;gap:8px}.port-input[data-v-34dbfd82]{width:96px;font-family:var(--font-mono);appearance:textfield;-moz-appearance:textfield}.port-input[data-v-34dbfd82]::-webkit-outer-spin-button,.port-input[data-v-34dbfd82]::-webkit-inner-spin-button{margin:0;-webkit-appearance:none}.interrupt-save[data-v-34dbfd82]{color:var(--warning);background:transparent;border-color:var(--warning-border)}.danger-action[data-v-34dbfd82]{color:var(--danger);background:transparent;border-color:var(--danger-border)}.interrupt-save[data-v-34dbfd82]:hover:not(:disabled){background:var(--warning-soft)}.danger-action[data-v-34dbfd82]:hover:not(:disabled){background:var(--danger-soft)}.info-grid[data-v-34dbfd82]{display:grid;grid-template-columns:1fr}.info-item[data-v-34dbfd82]{display:flex;min-width:0;min-height:39px;align-items:center;justify-content:space-between;gap:12px;padding:6px 5px;border-bottom:0}.info-item[data-v-34dbfd82]:last-child{border-bottom:0}.info-item dt[data-v-34dbfd82]{flex-shrink:0;color:var(--text-secondary);font-size:16px}.info-item dd[data-v-34dbfd82]{min-width:0;overflow:hidden;color:var(--text-primary);font-size:16px;font-weight:400;text-overflow:ellipsis;white-space:nowrap}@media(max-width:760px){.preference-control .segmented-item[data-v-34dbfd82],.setting-action .btn[data-v-34dbfd82]{min-height:40px}}@media(max-width:480px){.settings-grid[data-v-34dbfd82]{gap:12px}.preference-control[data-v-34dbfd82]{width:100%}.preference-control .segmented-item[data-v-34dbfd82]{min-height:40px;font-size:14px}.setting-row[data-v-34dbfd82]{min-height:0;align-items:stretch;flex-direction:column;gap:11px;padding:13px 8px}.setting-action[data-v-34dbfd82]{align-self:stretch;justify-content:flex-end}.port-action[data-v-34dbfd82]{display:grid;grid-template-columns:minmax(0,1fr) auto auto}.port-input[data-v-34dbfd82]{width:100%;min-height:44px;text-align:left}.setting-action .btn[data-v-34dbfd82]{min-height:44px}.info-item[data-v-34dbfd82]{min-height:0;align-items:flex-start;flex-direction:column;gap:3px;padding:10px 8px}.info-item dd[data-v-34dbfd82]{width:100%}}
|