This is an automated email from the ASF dual-hosted git repository. hsluoyz pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/casbin-gateway.git
commit 0e5383378c275d1224c95d68e40d30342d9ed314 Author: Yang Luo <[email protected]> AuthorDate: Fri Sep 4 16:14:41 2026 +0800 feat(settings): add a start-at-login toggle --- autostart/autostart.go | 111 ++++++++++++++++++++++++++ autostart/autostart_darwin.go | 90 +++++++++++++++++++++ autostart/autostart_unix.go | 79 ++++++++++++++++++ autostart/autostart_windows.go | 67 ++++++++++++++++ controllers/autostart.go | 84 +++++++++++++++++++ desktop/tray.go | 15 ++++ routers/router.go | 2 + web/src/backend/MiscBackend.ts | 11 ++- web/src/components/settings/startup-panel.tsx | 92 +++++++++++++++++++++ web/src/locales/en/data.json | 8 ++ web/src/locales/zh/data.json | 8 ++ web/src/nav.ts | 1 + web/src/pages/SettingPage.tsx | 5 ++ web/src/types.ts | 9 +++ 14 files changed, 581 insertions(+), 1 deletion(-) diff --git a/autostart/autostart.go b/autostart/autostart.go new file mode 100644 index 0000000..660b860 --- /dev/null +++ b/autostart/autostart.go @@ -0,0 +1,111 @@ +// Copyright 2026 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package autostart turns the login entry that starts Casbin Gateway on and +// off. +// +// The entry starts the desktop launcher rather than this server, because the +// launcher is what starts the server and the tray icon together. It is the same +// entry the launcher writes for its own "Start at Login" checkbox and for the +// "autostart on" the installers call, so the Settings page and the tray icon +// show one setting rather than two. +package autostart + +import ( + "errors" + "os" + "path/filepath" + "runtime" +) + +// appId matches the desktop launcher's, because both write the same entry. +const appId = "org.apache.casbin-gateway" + +// macOsBundleLauncher is the copy inside the application bundle, which is the +// one the Dock starts and therefore the one to start at login. +const macOsBundleLauncher = "Applications/Casbin Gateway.app/Contents/MacOS/casbin-gateway-desktop" + +// ErrNoLauncher is returned where there is no desktop launcher to start: a +// container or a headless host, where a login entry has nothing to open. +var ErrNoLauncher = errors.New("this installation has no desktop launcher to start at login") + +// Supported reports whether a login entry can be written on this host. +func Supported() bool { + return LauncherPath() != "" +} + +// Enabled reports whether Gateway starts at login. +func Enabled() (bool, error) { + return enabled() +} + +// Set writes or removes the login entry. Removing one needs no launcher, so an +// entry left behind by an installation that has since been stripped down can +// still be turned off. +func Set(enable bool) error { + if !enable { + return set("", false) + } + + launcher := LauncherPath() + if launcher == "" { + return ErrNoLauncher + } + return set(launcher, true) +} + +func launcherName() string { + if runtime.GOOS == "windows" { + return "casbin-gateway-desktop.exe" + } + return "casbin-gateway-desktop" +} + +// LauncherPath is the windowed executable beside the server, or "" where the +// server was installed without one. +func LauncherPath() string { + if bundled := bundleLauncher(); bundled != "" { + return bundled + } + + executable, err := os.Executable() + if err != nil { + return "" + } + if resolved, err := filepath.EvalSymlinks(executable); err == nil { + executable = resolved + } + + launcher := filepath.Join(filepath.Dir(executable), launcherName()) + if info, err := os.Stat(launcher); err != nil || info.IsDir() { + return "" + } + return launcher +} + +func bundleLauncher() string { + if runtime.GOOS != "darwin" { + return "" + } + + home, err := os.UserHomeDir() + if err != nil { + return "" + } + bundled := filepath.Join(home, filepath.FromSlash(macOsBundleLauncher)) + if _, err := os.Stat(bundled); err != nil { + return "" + } + return bundled +} diff --git a/autostart/autostart_darwin.go b/autostart/autostart_darwin.go new file mode 100644 index 0000000..1c28042 --- /dev/null +++ b/autostart/autostart_darwin.go @@ -0,0 +1,90 @@ +// Copyright 2026 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package autostart + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" +) + +const launchAgentLabel = appId + ".desktop" + +func launchAgentPath() string { + home, err := os.UserHomeDir() + if err != nil { + return "" + } + return filepath.Join(home, "Library", "LaunchAgents", launchAgentLabel+".plist") +} + +func enabled() (bool, error) { + path := launchAgentPath() + if path == "" { + return false, nil + } + + _, err := os.Stat(path) + if os.IsNotExist(err) { + return false, nil + } + return err == nil, err +} + +func set(launcher string, enable bool) error { + path := launchAgentPath() + if path == "" { + return fmt.Errorf("cannot find the home directory") + } + + if !enable { + _ = exec.Command("launchctl", "unload", path).Run() + if err := os.Remove(path); err != nil && !os.IsNotExist(err) { + return err + } + return nil + } + + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + return err + } + + // The launcher starts the server in its own working directory, which is + // where Gateway keeps its database. + home, err := os.Getwd() + if err != nil { + return err + } + + plist := fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>Label</key><string>%s</string> + <key>ProgramArguments</key> + <array><string>%s</string></array> + <key>WorkingDirectory</key><string>%s</string> + <key>RunAtLoad</key><true/> +</dict> +</plist> +`, launchAgentLabel, launcher, home) + + if err := os.WriteFile(path, []byte(plist), 0o644); err != nil { + return err + } + _ = exec.Command("launchctl", "load", path).Run() + return nil +} diff --git a/autostart/autostart_unix.go b/autostart/autostart_unix.go new file mode 100644 index 0000000..d544dab --- /dev/null +++ b/autostart/autostart_unix.go @@ -0,0 +1,79 @@ +// Copyright 2026 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !windows && !darwin + +package autostart + +import ( + "fmt" + "os" + "path/filepath" +) + +// XDG autostart rather than a systemd user unit: this one starts a tray icon, +// which needs the desktop session that a unit does not wait for. +func autostartPath() string { + dir := os.Getenv("XDG_CONFIG_HOME") + if dir == "" { + home, err := os.UserHomeDir() + if err != nil { + return "" + } + dir = filepath.Join(home, ".config") + } + return filepath.Join(dir, "autostart", appId+".desktop") +} + +func enabled() (bool, error) { + path := autostartPath() + if path == "" { + return false, nil + } + + _, err := os.Stat(path) + if os.IsNotExist(err) { + return false, nil + } + return err == nil, err +} + +func set(launcher string, enable bool) error { + path := autostartPath() + if path == "" { + return fmt.Errorf("cannot find the config directory") + } + + if !enable { + if err := os.Remove(path); err != nil && !os.IsNotExist(err) { + return err + } + return nil + } + + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + return err + } + + entry := fmt.Sprintf(`[Desktop Entry] +Type=Application +Name=Casbin Gateway +Exec=%s +Icon=%s +Terminal=false +X-GNOME-Autostart-enabled=true +`, launcher, appId) + + return os.WriteFile(path, []byte(entry), 0o644) +} diff --git a/autostart/autostart_windows.go b/autostart/autostart_windows.go new file mode 100644 index 0000000..2829e48 --- /dev/null +++ b/autostart/autostart_windows.go @@ -0,0 +1,67 @@ +// Copyright 2026 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package autostart + +import ( + "errors" + "os" + + "golang.org/x/sys/windows/registry" +) + +// The per-user Run key, holding the value the launcher's tray checkbox writes: +// it needs no elevation, and Task Manager's Startup tab lists it. +const ( + runKeyPath = `Software\Microsoft\Windows\CurrentVersion\Run` + runKeyName = "CasbinGateway" +) + +func enabled() (bool, error) { + key, err := registry.OpenKey(registry.CURRENT_USER, runKeyPath, registry.QUERY_VALUE) + if err != nil { + if errors.Is(err, registry.ErrNotExist) { + return false, nil + } + return false, err + } + defer key.Close() + + value, _, err := key.GetStringValue(runKeyName) + if errors.Is(err, registry.ErrNotExist) { + return false, nil + } + if err != nil { + return false, err + } + return value != "", nil +} + +func set(launcher string, enable bool) error { + key, err := registry.OpenKey(registry.CURRENT_USER, runKeyPath, registry.SET_VALUE) + if err != nil { + return err + } + defer key.Close() + + if !enable { + err := key.DeleteValue(runKeyName) + if errors.Is(err, registry.ErrNotExist) || errors.Is(err, os.ErrNotExist) { + return nil + } + return err + } + + return key.SetStringValue(runKeyName, `"`+launcher+`"`) +} diff --git a/controllers/autostart.go b/controllers/autostart.go new file mode 100644 index 0000000..2df6c43 --- /dev/null +++ b/controllers/autostart.go @@ -0,0 +1,84 @@ +// Copyright 2026 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package controllers + +import ( + "encoding/json" + + "github.com/apache/casbin-gateway/autostart" +) + +// AutostartState is the login entry as the Settings page shows it. It is host +// state rather than a stored setting: the machine holds it, so a restored +// backup does not bring another machine's answer with it. +type AutostartState struct { + Supported bool `json:"supported"` + Enabled bool `json:"enabled"` + Launcher string `json:"launcher"` +} + +func getAutostartState() (*AutostartState, error) { + on, err := autostart.Enabled() + if err != nil { + return nil, err + } + + return &AutostartState{ + Supported: autostart.Supported(), + Enabled: on, + Launcher: autostart.LauncherPath(), + }, nil +} + +func (c *ApiController) GetAutostart() { + if c.RequireAdmin() { + return + } + + state, err := getAutostartState() + if err != nil { + c.ResponseError(err.Error()) + return + } + + c.ResponseOk(state) +} + +func (c *ApiController) UpdateAutostart() { + if c.RequireAdmin() { + return + } + + var body struct { + Enabled bool `json:"enabled"` + } + if err := json.Unmarshal(c.Ctx.Input.RequestBody, &body); err != nil { + c.ResponseError(err.Error()) + return + } + + if err := autostart.Set(body.Enabled); err != nil { + c.ResponseError(err.Error()) + return + } + + state, err := getAutostartState() + if err != nil { + c.ResponseError(err.Error()) + return + } + + c.ResponseOk(state) +} diff --git a/desktop/tray.go b/desktop/tray.go index e86a3cd..45bff82 100644 --- a/desktop/tray.go +++ b/desktop/tray.go @@ -83,6 +83,10 @@ func onTrayReady() { systray.AddSeparator() mQuit := systray.AddMenuItem("Quit", "Close the window and leave the tray") + // The Settings page writes the same login entry, so the checkbox follows it + // rather than showing what was true when the menu was built. + go watchAutostart(mAutostart) + port := httpPort() // The window is opened from a goroutine so that the tray answers clicks @@ -142,6 +146,17 @@ func watchStatus(item *systray.MenuItem, port int) { } } +func watchAutostart(item *systray.MenuItem) { + for { + if autostartEnabled() { + item.Check() + } else { + item.Uncheck() + } + time.Sleep(probeInterval) + } +} + func toggleAutostart(item *systray.MenuItem) { if item.Checked() { if err := setAutostart(false); err != nil { diff --git a/routers/router.go b/routers/router.go index 5d6d2d8..36f7c38 100644 --- a/routers/router.go +++ b/routers/router.go @@ -43,6 +43,8 @@ func initAPI() { beego.Router("/api/get-update-status", &controllers.ApiController{}, "GET:GetUpdateStatus") beego.Router("/api/get-setting", &controllers.ApiController{}, "GET:GetSetting") beego.Router("/api/update-setting", &controllers.ApiController{}, "POST:UpdateSetting") + beego.Router("/api/get-autostart", &controllers.ApiController{}, "GET:GetAutostart") + beego.Router("/api/update-autostart", &controllers.ApiController{}, "POST:UpdateAutostart") beego.Router("/api/export-snapshot", &controllers.ApiController{}, "POST:ExportSnapshot") beego.Router("/api/import-snapshot", &controllers.ApiController{}, "POST:ImportSnapshot") diff --git a/web/src/backend/MiscBackend.ts b/web/src/backend/MiscBackend.ts index 30a5e7c..1d57e9e 100644 --- a/web/src/backend/MiscBackend.ts +++ b/web/src/backend/MiscBackend.ts @@ -13,7 +13,7 @@ // limitations under the License. import {query, request} from "@/backend/request"; -import type {UpdateStatus, VersionInfo} from "@/types"; +import type {AutostartState, UpdateStatus, VersionInfo} from "@/types"; /** The token an agent has to send to the relay, and whether it is needed here. */ export function getRelayToken() { @@ -33,3 +33,12 @@ export function updateGateway() { export function getUpdateStatus() { return request<UpdateStatus>("/api/get-update-status"); } + +/** Whether this machine starts Gateway at login. */ +export function getAutostart() { + return request<AutostartState>("/api/get-autostart"); +} + +export function updateAutostart(enabled: boolean) { + return request<AutostartState>("/api/update-autostart", "POST", {enabled: enabled}); +} diff --git a/web/src/components/settings/startup-panel.tsx b/web/src/components/settings/startup-panel.tsx new file mode 100644 index 0000000..12c301a --- /dev/null +++ b/web/src/components/settings/startup-panel.tsx @@ -0,0 +1,92 @@ +// Copyright 2026 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as React from "react"; +import i18next from "i18next"; + +import * as MiscBackend from "@/backend/MiscBackend"; +import * as Setting from "@/Setting"; +import {Card, CardContent, CardDescription, CardHeader, CardTitle} from "@/components/ui/card"; +import {Switch} from "@/components/ui/switch"; +import type {AutostartState} from "@/types"; + +/** + * Whether Gateway comes up when this account logs in. The switch writes the + * same login entry as the tray icon's "Start at Login", and applies as it is + * switched rather than on Save: this one is the machine's state, not a row in + * the database the rest of the page edits. + */ +export function StartupPanel() { + const [state, setState] = React.useState<AutostartState | null>(null); + const [busy, setBusy] = React.useState(false); + + React.useEffect(() => { + MiscBackend.getAutostart() + .then(res => { + if (res.status === "ok") { + setState(res.data); + } + }) + .catch(() => undefined); + }, []); + + if (state === null) { + return null; + } + + const toggle = (enabled: boolean) => { + setBusy(true); + MiscBackend.updateAutostart(enabled) + .then(res => { + if (res.status === "ok" && res.data) { + setState(res.data); + Setting.showMessage("success", i18next.t("general:Successfully saved")); + } else { + Setting.showMessage("error", `${i18next.t("general:Failed to save")}: ${res.msg}`); + } + }) + .catch(failure => Setting.showMessage("error", failure.message || String(failure))) + .then(() => setBusy(false)); + }; + + return ( + <Card className="gap-4 py-5"> + <CardHeader className="px-5"> + <div className="flex flex-wrap items-start justify-between gap-3"> + <div className="min-w-0"> + <CardTitle className="text-[15px]">{i18next.t("setting:Startup")}</CardTitle> + <CardDescription>{i18next.t("setting:Startup description")}</CardDescription> + </div> + <div className="flex shrink-0 items-center gap-2"> + <span className="text-muted-foreground text-xs"> + {i18next.t(state.enabled ? "setting:Autostart is on" : "setting:Autostart is off")} + </span> + <Switch + checked={state.enabled} + disabled={busy || (!state.supported && !state.enabled)} + aria-label={i18next.t("setting:Start at login")} + onCheckedChange={toggle} + /> + </div> + </div> + </CardHeader> + + {state.supported ? null : ( + <CardContent className="px-5"> + <p className="text-muted-foreground text-xs">{i18next.t("setting:No launcher to start")}</p> + </CardContent> + )} + </Card> + ); +} diff --git a/web/src/locales/en/data.json b/web/src/locales/en/data.json index 76b1c5d..848a40f 100644 --- a/web/src/locales/en/data.json +++ b/web/src/locales/en/data.json @@ -768,6 +768,7 @@ "Go to writable demo site?": "Go to writable demo site?", "Home": "Home", "ID": "ID", + "Indigo": "Indigo", "Installing": "Installing", "Latest version": "Latest version", "Leave this page open, it reloads itself when the new version is up": "Leave this page open, it reloads itself when the new version is up", @@ -804,6 +805,7 @@ "Sorry, you do not have permission to access this page or logged in status invalid.": "Sorry, you do not have permission to access this page or logged in status invalid.", "Successfully saved": "Successfully saved", "Sure to delete {name} ?": "Sure to delete {name} ?", + "Terminal": "Terminal", "The new version did not start, check the Gateway log": "The new version did not start, check the Gateway log", "This Gateway cannot find its own executable, so the update has to be done by hand": "This Gateway cannot find its own executable, so the update has to be done by hand", "This executable sits where it cannot be replaced, so the update has to be done by hand": "This executable sits where it cannot be replaced, so the update has to be done by hand", @@ -1095,6 +1097,8 @@ "Allowed hosts hint": "Extra Host names Gateway answers to, comma-separated. Its own addresses, \"localhost\" and this machine's hostname always work; add a name only when a reverse proxy or another domain points here.", "Allowed origins": "Allowed origins", "Allowed origins hint": "Browser origins allowed to call the management API from another site, comma-separated and written in full, e.g. \"https://app.example.com\". The web UI needs none of these, and neither does a browser extension calling /v1.", + "Autostart is off": "Autostart is off", + "Autostart is on": "Autostart is on", "Back up now": "Back up now", "Backups": "Backups", "Backups are off": "Backups are off", @@ -1159,6 +1163,7 @@ "Next backup": "Next backup", "No backups yet": "No backups yet", "No keys": "No keys", + "No launcher to start": "This installation has no desktop launcher, so there is nothing to start at login.", "Not in this file": "Not in this file", "Nothing in it": "Nothing in it", "Nothing is scheduled": "Nothing is scheduled", @@ -1204,6 +1209,9 @@ "Sign-in": "Sign-in", "Sign-in description": "Casdoor is optional: leave the endpoint empty to sign in against Gateway's own user table.", "Skipped": "Skipped", + "Start at login": "Start at login", + "Startup": "Startup", + "Startup description": "Whether Casbin Gateway starts when you log in to this machine. The tray icon and the server behind it come up together, so this machine's agents are routed from the moment you sign in. The tray menu's Start at Login item is the same entry.", "Storage": "Storage", "Sync now": "Sync now", "Syncing is off": "Syncing is off", diff --git a/web/src/locales/zh/data.json b/web/src/locales/zh/data.json index baf24df..3267a42 100644 --- a/web/src/locales/zh/data.json +++ b/web/src/locales/zh/data.json @@ -840,6 +840,7 @@ "Go to writable demo site?": "跳转至可写演示站点?", "Home": "首页", "ID": "ID", + "Indigo": "靛蓝", "Installing": "正在安装", "Latest version": "最新版本", "Leave this page open, it reloads itself when the new version is up": "请保持本页面打开,新版本启动后会自动刷新", @@ -876,6 +877,7 @@ "Sorry, you do not have permission to access this page or logged in status invalid.": "抱歉,您无权访问该页面或登录状态无效。", "Successfully saved": "保存成功", "Sure to delete {name} ?": "确定删除 {name} ?", + "Terminal": "终端", "The new version did not start, check the Gateway log": "新版本没有启动,请查看 Gateway 日志", "This Gateway cannot find its own executable, so the update has to be done by hand": "Gateway 找不到自己的可执行文件,无法替换,需要手动升级", "This executable sits where it cannot be replaced, so the update has to be done by hand": "当前可执行文件所在目录不可写,无法被替换,需要手动升级", @@ -1167,6 +1169,8 @@ "Allowed hosts hint": "网关额外响应的 Host 名称,用逗号分隔。本机地址、\"localhost\" 和本机主机名始终可用;仅当有反向代理或其他域名指向这里时才需要添加。", "Allowed origins": "允许的来源", "Allowed origins hint": "允许从其他站点调用管理 API 的浏览器来源,用逗号分隔并写全,例如 \"https://app.example.com\"。网页控制台无需配置,浏览器扩展调用 /v1 也无需配置。", + "Autostart is off": "自启动已关闭", + "Autostart is on": "自启动已开启", "Back up now": "立即备份", "Backups": "备份", "Backups are off": "自动备份已关闭", @@ -1231,6 +1235,7 @@ "Next backup": "下次备份", "No backups yet": "还没有备份", "No keys": "不含密钥", + "No launcher to start": "本次安装没有桌面启动器,登录时没有可以启动的程序。", "Not in this file": "文件里没有", "Nothing in it": "里面什么都没有", "Nothing is scheduled": "没有排期", @@ -1276,6 +1281,9 @@ "Sign-in": "登录", "Sign-in description": "Casdoor 是可选的:地址留空则使用 Gateway 自带的用户表登录。", "Skipped": "跳过", + "Start at login": "开机自启动", + "Startup": "开机自启动", + "Startup description": "登录本机时是否自动启动 Casbin Gateway。托盘图标会和它背后的服务一起启动,登录之后本机 Agent 的请求立刻就有网关接管。托盘菜单里的 “Start at Login” 改的是同一处设置。", "Storage": "存储", "Sync now": "立即同步", "Syncing is off": "同步已关闭", diff --git a/web/src/nav.ts b/web/src/nav.ts index 2e9e7e7..2ab339a 100644 --- a/web/src/nav.ts +++ b/web/src/nav.ts @@ -123,6 +123,7 @@ export const navGroups: NavGroup[] = [ adminOnly: true, // One long page, so the children are its sections rather than tabs. children: [ + {key: "/settings#startup", label: "setting:Startup", path: "/settings#startup"}, {key: "/settings#llm-records", label: "setting:LLM records", path: "/settings#llm-records"}, {key: "/settings#probes", label: "setting:Channel probes", path: "/settings#probes"}, {key: "/settings#agents", label: "setting:Agents", path: "/settings#agents"}, diff --git a/web/src/pages/SettingPage.tsx b/web/src/pages/SettingPage.tsx index f9630b0..a4fc634 100644 --- a/web/src/pages/SettingPage.tsx +++ b/web/src/pages/SettingPage.tsx @@ -21,6 +21,7 @@ import * as Setting from "@/Setting"; import {BackupPanel} from "@/components/settings/backup-panel"; import {CloudSyncPanel} from "@/components/settings/cloud-sync-panel"; import {ImportExportPanel} from "@/components/settings/import-export-panel"; +import {StartupPanel} from "@/components/settings/startup-panel"; import {Field} from "@/components/shared/form-dialog"; import {Loading} from "@/components/shared/loading"; import {UnauthorizedResult} from "@/components/shared/misc"; @@ -149,6 +150,10 @@ export default function SettingPage({account}: {account: Account}) { } /> + <div id="startup" className="scroll-mt-20"> + <StartupPanel /> + </div> + <Section id="llm-records" columns={2} title={i18next.t("setting:LLM records")} description={i18next.t("setting:LLM records description")}> <Field label={i18next.t("setting:Record mode")}> <SimpleSelect diff --git a/web/src/types.ts b/web/src/types.ts index bda14f9..bf58c0b 100644 --- a/web/src/types.ts +++ b/web/src/types.ts @@ -1316,3 +1316,12 @@ export interface VersionInfo { releaseUrl: string; installCommand: string; } + +/** Whether this machine starts Gateway at login. Host state, not a setting: it + * lives in the login entry the desktop launcher and the tray share. */ +export interface AutostartState { + /** False where there is no desktop launcher to start, as in a container. */ + supported: boolean; + enabled: boolean; + launcher: string; +} --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
