mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-10-19 05:57:39 +02:00
feat: move the one-click update into the system settings menu
The update console stops being a page of its own and becomes the last section of 系统设置 (data-section="update", gated by update:read): the sidebar entry and page-system-update are gone, the console markup lives in settings-section-update, and the settings menu is where an operator now finds it. #system-update keeps working as an alias - both hash parsers normalize it to settings + the update section - so existing deep links and docs keep landing. Polling follows the new home: leaving the settings page (router) or switching to another section (settings.js) stops it, selecting the section loads the console, and returning to the settings page with the section still selected re-reads the install tree and re-arms the poller (a frozen progress read is the "nothing is happening" picture an operator must never be shown). The guard suite follows the move: the old nav/page assertions become "must not exist", the new ones pin the section, the alias, the poller handoff and the retired nav.systemUpdate key (settings.nav.update replaces it in zh/EN/RU). README and both developer/deployment guides say 系统设置 → 一键更新.
This commit is contained in:
1 parent
27c1ff6573
commit
cbd8073beb
16 files changed
+140
-73
No files matched your search
@@ -269,7 +269,7 @@ If server logs show `client sent an HTTP request to an HTTPS server`, a client i
|
||||
|
||||
### Upgrade and Compatibility
|
||||
|
||||
**Prefer the platform's own one-click update** (console Platform management -> One-click update, or
|
||||
**Prefer the platform's own one-click update** (console System settings -> One-click update, or
|
||||
`./cyberstrike-ai -update` / `-check-update` / `-update-rollback`): it pulls **the remote this
|
||||
installation directory already tracks**, fast-forwards, rebuilds and swaps the binary atomically, keeps
|
||||
your `roles/skills/tools/agents/data/config.yaml` untouched (the result names every file it
|
||||
|
||||
+1
-1
@@ -268,7 +268,7 @@ go build -o cyberstrike-ai ./cmd/server
|
||||
|
||||
### 版本升级与兼容性
|
||||
|
||||
**优先用平台自带的「一键更新」**(控制台「平台管理 → 一键更新」,或 `./cyberstrike-ai -update` /
|
||||
**优先用平台自带的「一键更新」**(控制台「系统设置 → 一键更新」,或 `./cyberstrike-ai -update` /
|
||||
`-check-update` / `-update-rollback`):它拉的是**本安装目录自己跟踪的远端**,快进后重编译并原子换二进制,
|
||||
你的 `roles/skills/tools/agents/data/config.yaml` 会被原样保留(结果里逐个点名保留了什么),
|
||||
本地源码有改动或分支已分叉时它会拒绝而不是覆盖。详见 [部署指南](docs/zh-CN/deployment.md)。
|
||||
|
||||
@@ -77,7 +77,7 @@ proxy_set_header Connection "upgrade";
|
||||
|
||||
## Upgrading
|
||||
|
||||
**Prefer the platform's own one-click update** (console Platform management -> One-click update,
|
||||
**Prefer the platform's own one-click update** (console System settings -> One-click update,
|
||||
`POST /api/system/update/apply`, or `./cyberstrike-ai -update` at a keyboard). It pulls **the remote
|
||||
this install directory already tracks**: fetch, fast-forward, `go build`, atomic binary swap (the old
|
||||
binary is kept as `cyberstrike-ai.prev`), while operator content - `roles/ skills/ tools/ agents/
|
||||
|
||||
@@ -48,7 +48,7 @@ disagree about why an update was refused.
|
||||
|
||||
| entry | how it is used |
|
||||
|---|---|
|
||||
| console | Platform management -> One-click update (`#system-update`). Opening the page reads the local tree only and does not go online; **Check** is what performs the fetch; **Update** starts a job the page polls; the page also carries the "exit after updating" option and a rollback button |
|
||||
| console | System settings -> One-click update (`#system-update`, the old deep link still works). Opening this section reads the local tree only and does not go online; **Check** is what performs the fetch; **Update** starts a job the page polls; the page also carries the "exit after updating" option and a rollback button |
|
||||
| REST | `GET /api/system/update` (state on disk, no network), `POST /api/system/update/check` (fetch, then report the gap), `POST /api/system/update/apply` (`202` with `job_id`, polled through `GET /api/system/update/job`), `POST /api/system/update/rollback` |
|
||||
| CLI | `./cyberstrike-ai -check-update`, `./cyberstrike-ai -update`, `./cyberstrike-ai -update-rollback`. The install root is the directory holding `--config`, or the current directory when `--config` was not given |
|
||||
|
||||
|
||||
@@ -179,7 +179,7 @@ SQLite 热备份时最好先停止服务,或至少复制 `*.db`、`*.db-wal`
|
||||
|
||||
## 升级
|
||||
|
||||
**优先用平台自带的「一键更新」**(控制台「平台管理 → 一键更新」、`POST /api/system/update/apply`、
|
||||
**优先用平台自带的「一键更新」**(控制台「系统设置 → 一键更新」、`POST /api/system/update/apply`、
|
||||
或键盘上的 `./cyberstrike-ai -update`)。它拉的是**这个安装目录自己跟踪的远端**:fetch → 快进合并 →
|
||||
`go build` → 原子换二进制(旧的留作 `cyberstrike-ai.prev`),并把 `roles/ skills/ tools/ agents/
|
||||
knowledge_base/ data/ config.yaml` 等运维者内容先暂存再放回,结果里点名保留了哪些文件。
|
||||
|
||||
@@ -54,7 +54,7 @@ go run ./cmd/server --config config.yaml
|
||||
|
||||
| 入口 | 怎么用 |
|
||||
|---|---|
|
||||
| 控制台 | 「平台管理 → 一键更新」(`#system-update`)。打开页面只读本机状态、不联网;点「检查更新」才去 fetch;「一键更新」发起任务并轮询进度;页面另有"更新完成后退出进程"勾选与回滚按钮 |
|
||||
| 控制台 | 「系统设置 → 一键更新」(`#system-update` 老深链仍可用)。打开这一区只读本机状态、不联网;点「检查更新」才去 fetch;「一键更新」发起任务并轮询进度;页面另有"更新完成后退出进程"勾选与回滚按钮 |
|
||||
| REST | `GET /api/system/update`(磁盘现状,不联网)、`POST /api/system/update/check`(fetch 后报告差集)、`POST /api/system/update/apply`(`202` 返回 `job_id`,用 `GET /api/system/update/job` 轮询)、`POST /api/system/update/rollback` |
|
||||
| CLI | `./cyberstrike-ai -check-update`、`./cyberstrike-ai -update`、`./cyberstrike-ai -update-rollback`。安装目录 = `--config` 所在目录,未给 `--config` 时是当前目录 |
|
||||
|
||||
|
||||
@@ -46718,7 +46718,7 @@ html[data-theme="dark"] { --tool-blocked-color: #f4bc62; }
|
||||
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
一键更新控制台(system-update):这里说的每一句状态都要能被眼睛分区——
|
||||
一键更新控制台(系统设置页的 settings-section-update 分区):这里说的每一句状态都要能被眼睛分区——
|
||||
本机事实、远端比较、能不能点、正在跑的进度、以及跑完的结论。
|
||||
ok/warn/danger 三色是给"说谎"留的对照组:查不了必须是 warn/danger,不能长得像 ok。
|
||||
--------------------------------------------------------------------------- */
|
||||
@@ -46728,6 +46728,15 @@ html[data-theme="dark"] { --tool-blocked-color: #f4bc62; }
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
/* 分区标题行:标题在左、刷新在右;窄屏放不下就换行,别把标题压扁 */
|
||||
.update-section-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.update-intro {
|
||||
color: var(--text-secondary);
|
||||
margin: 0 0 12px;
|
||||
|
||||
@@ -202,7 +202,6 @@
|
||||
"rollbackItems": "Files that were not rolled back"
|
||||
},
|
||||
"nav": {
|
||||
"systemUpdate": "One-Click Update",
|
||||
"security": "Security",
|
||||
"toolGuard": "Call blocking",
|
||||
"dashboard": "Dashboard",
|
||||
@@ -1920,6 +1919,7 @@
|
||||
"rbac": "Platform permissions",
|
||||
"audit": "Audit logs",
|
||||
"storage": "Storage cleanup",
|
||||
"update": "One-Click Update",
|
||||
"infocollect": "Asset management"
|
||||
},
|
||||
"infocollect": {
|
||||
|
||||
@@ -202,7 +202,6 @@
|
||||
"rollbackItems": "Файлы, которые не были откачены"
|
||||
},
|
||||
"nav": {
|
||||
"systemUpdate": "Обновление в один клик",
|
||||
"security": "Безопасность",
|
||||
"toolGuard": "Блокировка вызовов",
|
||||
"dashboard": "Обзор",
|
||||
@@ -1933,6 +1932,7 @@
|
||||
"rbac": "Права платформы",
|
||||
"audit": "Журнал аудита",
|
||||
"storage": "Очистка хранилища",
|
||||
"update": "Обновление в один клик",
|
||||
"infocollect": "Управление активами"
|
||||
},
|
||||
"infocollect": {
|
||||
|
||||
@@ -202,7 +202,6 @@
|
||||
"rollbackItems": "未能回滚的文件"
|
||||
},
|
||||
"nav": {
|
||||
"systemUpdate": "一键更新",
|
||||
"security": "安全防护",
|
||||
"toolGuard": "调用拦截",
|
||||
"dashboard": "仪表盘",
|
||||
@@ -1908,6 +1907,7 @@
|
||||
"rbac": "平台权限",
|
||||
"audit": "日志审计",
|
||||
"storage": "存储清理",
|
||||
"update": "一键更新",
|
||||
"infocollect": "资产管理"
|
||||
},
|
||||
"infocollect": {
|
||||
|
||||
@@ -443,7 +443,6 @@ const PAGE_PERMISSION_MAP = {
|
||||
roles: 'roles:read',
|
||||
'roles-management': 'roles:read',
|
||||
'platform-rbac': 'rbac:read',
|
||||
'system-update': 'update:read',
|
||||
settings: 'config:read',
|
||||
};
|
||||
|
||||
|
||||
+23
-7
@@ -110,8 +110,14 @@ function initRouter() {
|
||||
const hashParts = hash.split('?');
|
||||
let pageId = hashParts[0].replace(/^#?\//, '');
|
||||
if (pageId === 'c2') pageId = 'c2-listeners';
|
||||
if (pageId && ['dashboard', 'chat', 'hitl', 'tool-guard', 'asset-overview', 'asset-library', 'info-collect', 'projects', 'vulnerabilities', 'webshell', 'chat-files', 'mcp-monitor', 'mcp-management', 'knowledge-management', 'knowledge-retrieval-logs', 'roles-management', 'platform-rbac', 'workflows', 'skills-monitor', 'skills-management', 'agents-management', 'system-update', 'settings', 'tasks', 'c2-listeners', 'c2-sessions', 'c2-tasks', 'c2-payloads', 'c2-events', 'c2-profiles'].includes(pageId)) {
|
||||
// 一键更新已并进系统设置页的分区菜单:#system-update 这条老深链落到设置页的那一区。
|
||||
const settingsSection = pageId === 'system-update' ? 'update' : '';
|
||||
if (settingsSection) pageId = 'settings';
|
||||
if (pageId && ['dashboard', 'chat', 'hitl', 'tool-guard', 'asset-overview', 'asset-library', 'info-collect', 'projects', 'vulnerabilities', 'webshell', 'chat-files', 'mcp-monitor', 'mcp-management', 'knowledge-management', 'knowledge-retrieval-logs', 'roles-management', 'platform-rbac', 'workflows', 'skills-monitor', 'skills-management', 'agents-management', 'settings', 'tasks', 'c2-listeners', 'c2-sessions', 'c2-tasks', 'c2-payloads', 'c2-events', 'c2-profiles'].includes(pageId)) {
|
||||
switchPage(pageId);
|
||||
if (settingsSection && typeof switchSettingsSection === 'function') {
|
||||
switchSettingsSection(settingsSection);
|
||||
}
|
||||
if (pageId === 'chat') {
|
||||
scheduleChatConversationFromHash(0);
|
||||
}
|
||||
@@ -524,14 +530,17 @@ async function initPage(pageId) {
|
||||
initChatFilesPage();
|
||||
}
|
||||
break;
|
||||
case 'system-update':
|
||||
if (typeof loadUpdateConsole === 'function') loadUpdateConsole();
|
||||
break;
|
||||
case 'settings':
|
||||
// 初始化设置页面(不需要加载工具列表)
|
||||
if (typeof loadConfig === 'function') {
|
||||
loadConfig(false);
|
||||
}
|
||||
// 回到设置页时若停在「一键更新」分区,重新读一遍安装状态并把进度轮询接上——
|
||||
// 离开设置页时轮询已被停掉,不接的话画面上会留一份不再动的进度。
|
||||
const updateSection = document.getElementById('settings-section-update');
|
||||
if (updateSection && updateSection.classList.contains('active') && typeof loadUpdateConsole === 'function') {
|
||||
loadUpdateConsole();
|
||||
}
|
||||
break;
|
||||
case 'roles-management':
|
||||
// 初始化角色管理页面
|
||||
@@ -608,8 +617,9 @@ async function initPage(pageId) {
|
||||
if (pageId !== 'tasks' && typeof cleanupTasksPage === 'function') {
|
||||
cleanupTasksPage();
|
||||
}
|
||||
// 一键更新的进度轮询同理:离开那一页就没必要每 1.5 秒读一次任务了。
|
||||
if (pageId !== 'system-update' && typeof stopUpdatePolling === 'function') {
|
||||
// 一键更新的进度轮询同理:它现在住在系统设置页里,离开设置页就没必要每 1.5 秒读一次任务了
|
||||
// (同一页内切到别的分区由 settings.js 的 switchSettingsSection 负责停)。
|
||||
if (pageId !== 'settings' && typeof stopUpdatePolling === 'function') {
|
||||
stopUpdatePolling();
|
||||
}
|
||||
}
|
||||
@@ -628,8 +638,14 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
let pageId = hashParts[0].replace(/^#?\//, '');
|
||||
|
||||
if (pageId === 'c2') pageId = 'c2-listeners';
|
||||
if (pageId && ['dashboard', 'chat', 'hitl', 'tool-guard', 'asset-overview', 'asset-library', 'info-collect', 'projects', 'tasks', 'workflows', 'vulnerabilities', 'webshell', 'chat-files', 'mcp-monitor', 'mcp-management', 'knowledge-management', 'knowledge-retrieval-logs', 'roles-management', 'platform-rbac', 'skills-monitor', 'skills-management', 'agents-management', 'system-update', 'settings', 'c2-listeners', 'c2-sessions', 'c2-tasks', 'c2-payloads', 'c2-events', 'c2-profiles'].includes(pageId)) {
|
||||
// 与首屏解析同一套别名:#system-update 落到系统设置页的「一键更新」分区。
|
||||
const settingsSection = pageId === 'system-update' ? 'update' : '';
|
||||
if (settingsSection) pageId = 'settings';
|
||||
if (pageId && ['dashboard', 'chat', 'hitl', 'tool-guard', 'asset-overview', 'asset-library', 'info-collect', 'projects', 'tasks', 'workflows', 'vulnerabilities', 'webshell', 'chat-files', 'mcp-monitor', 'mcp-management', 'knowledge-management', 'knowledge-retrieval-logs', 'roles-management', 'platform-rbac', 'skills-monitor', 'skills-management', 'agents-management', 'settings', 'c2-listeners', 'c2-sessions', 'c2-tasks', 'c2-payloads', 'c2-events', 'c2-profiles'].includes(pageId)) {
|
||||
switchPage(pageId);
|
||||
if (settingsSection && typeof switchSettingsSection === 'function') {
|
||||
switchSettingsSection(settingsSection);
|
||||
}
|
||||
if (pageId === 'chat') {
|
||||
scheduleChatConversationFromHash(0);
|
||||
}
|
||||
|
||||
@@ -691,6 +691,14 @@ function switchSettingsSection(section) {
|
||||
if (section === 'storage' && typeof initStorageSection === 'function') {
|
||||
setTimeout(initStorageSection, 0);
|
||||
}
|
||||
if (section === 'update') {
|
||||
if (typeof loadUpdateConsole === 'function') {
|
||||
loadUpdateConsole();
|
||||
}
|
||||
} else if (typeof stopUpdatePolling === 'function') {
|
||||
// 离开更新分区就停掉进度轮询;服务端的任务照跑,回来时 loadUpdateConsole 会接着读。
|
||||
stopUpdatePolling();
|
||||
}
|
||||
}
|
||||
|
||||
// 打开设置
|
||||
|
||||
@@ -6,6 +6,7 @@ const assert = require('node:assert/strict');
|
||||
const source = fs.readFileSync('web/static/js/update.js', 'utf8');
|
||||
const template = fs.readFileSync('web/templates/index.html', 'utf8');
|
||||
const router = fs.readFileSync('web/static/js/router.js', 'utf8');
|
||||
const settings = fs.readFileSync('web/static/js/settings.js', 'utf8');
|
||||
const sheet = fs.readFileSync('web/static/css/style.css', 'utf8');
|
||||
const zh = JSON.parse(fs.readFileSync('web/static/i18n/zh-CN.json', 'utf8'));
|
||||
const en = JSON.parse(fs.readFileSync('web/static/i18n/en-US.json', 'utf8'));
|
||||
@@ -160,8 +161,12 @@ function harness(options) {
|
||||
checked: false,
|
||||
insertAdjacentHTML(_pos, html) { this.innerHTML = html + this.innerHTML; },
|
||||
};
|
||||
// 控制台住在系统设置页的「一键更新」分区里:页面和分区都得是 active,它才算在前台。
|
||||
let seeded = ['page'];
|
||||
if (id === 'page-settings') seeded = ['page'].concat(active ? ['active'] : []);
|
||||
if (id === 'settings-section-update') seeded = ['settings-section-content'].concat(active ? ['active'] : []);
|
||||
el.classList = {
|
||||
_set: new Set(id === 'page-system-update' && active ? ['page', 'active'] : ['page']),
|
||||
_set: new Set(seeded),
|
||||
contains(c) { return this._set.has(c); },
|
||||
add(c) { this._set.add(c); },
|
||||
remove(c) { this._set.delete(c); },
|
||||
@@ -325,11 +330,15 @@ function harness(options) {
|
||||
if (!text) throw new Error('handler ' + name + ' is not present in the rendered markup');
|
||||
return runHandler(text, thisArg);
|
||||
},
|
||||
// Flipping the router's page-active class is all that leaving a page means for this console.
|
||||
// Flipping the router's active classes (the settings page and its update section) is all
|
||||
// that leaving means for this console.
|
||||
setActive(v) {
|
||||
active = v;
|
||||
const node = nodes.get('page-system-update');
|
||||
if (v) node.classList.add('active'); else node.classList.remove('active');
|
||||
['page-settings', 'settings-section-update'].forEach(id => {
|
||||
const node = nodes.get(id);
|
||||
if (!node) return;
|
||||
if (v) node.classList.add('active'); else node.classList.remove('active');
|
||||
});
|
||||
},
|
||||
setConfirm(v) { confirmAnswer = v; },
|
||||
liveTimers() { return timers.filter(t => !t.cleared && !t.deferred); },
|
||||
@@ -343,38 +352,64 @@ function harness(options) {
|
||||
return Promise.resolve(sandbox.api.loadUpdateConsole()).then(flush).then(() => h);
|
||||
}
|
||||
|
||||
test('the page is wired in everywhere a page has to be registered', () => {
|
||||
assert.match(template, /<div class="nav-item" data-page="system-update">/);
|
||||
assert.match(template, /<div id="page-system-update" class="page">/);
|
||||
test('the console lives inside the settings page as its own section', () => {
|
||||
// The entry is no longer a page of its own: it is one section of 系统设置, so the main sidebar
|
||||
// must not advertise a sibling item for it any more.
|
||||
assert.doesNotMatch(template, /data-page="system-update"/,
|
||||
'the standalone sidebar entry must be gone: the console is a settings section now');
|
||||
assert.doesNotMatch(template, /id="page-system-update"/,
|
||||
'the standalone page must be gone: its content moved into 系统设置');
|
||||
|
||||
const settingsNavItem = template.indexOf('<div class="settings-nav-item" data-section="update" data-require-permission="update:read" onclick="switchSettingsSection(\'update\')">');
|
||||
assert.ok(settingsNavItem > -1, 'the settings menu must carry the 一键更新 entry');
|
||||
assert.match(template, /<span data-i18n="settings\.nav\.update">一键更新<\/span>/);
|
||||
const storageItem = template.indexOf('data-section="storage"');
|
||||
assert.ok(storageItem > -1 && storageItem < settingsNavItem, 'the entry must follow 存储清理 in the settings menu');
|
||||
|
||||
// The console markup sits inside page-settings, wrapped in its own section.
|
||||
const pageSettings = template.indexOf('<div id="page-settings" class="page">');
|
||||
const section = template.indexOf('<div id="settings-section-update" class="settings-section-content" data-require-permission="update:read">');
|
||||
const nextPage = template.indexOf('<!-- 平台权限页面 -->');
|
||||
assert.ok(pageSettings > -1 && section > pageSettings && nextPage > section,
|
||||
'settings-section-update must live inside page-settings');
|
||||
assert.match(template, /id="settings-section-update"[\s\S]{0,700}?id="update-console"/);
|
||||
assert.match(template, /id="update-console"/);
|
||||
assert.match(template, /<script src="\/static\/js\/update\.js\?v=\{\{\.Version\}\}"><\/script>/);
|
||||
assert.match(template, /<div class="settings-section-header update-section-head">/);
|
||||
assert.match(template, /<h3 data-i18n="update\.title">/);
|
||||
|
||||
const group = template.indexOf('<span data-i18n="navGroups.administration">平台管理</span>');
|
||||
const rbac = template.indexOf('<div class="nav-item" data-page="platform-rbac">');
|
||||
const update = template.indexOf('<div class="nav-item" data-page="system-update">');
|
||||
const settings = template.indexOf('<div class="nav-item" data-page="settings">');
|
||||
assert.ok(group > -1 && rbac > group && update > rbac && settings > update,
|
||||
'the nav item must sit in 平台管理, after 平台权限 and before 系统设置');
|
||||
assert.match(template, /data-page="system-update"[\s\S]{0,400}?onclick="switchPage\('system-update'\)"/);
|
||||
assert.match(template, /<div class="nav-item-content" data-title="一键更新" onclick="switchPage\('system-update'\)" data-i18n="nav\.systemUpdate" data-i18n-attr="data-title"/);
|
||||
assert.match(template, /<div class="page-header">\s*\n\s*<h2 data-i18n="update\.title">/);
|
||||
// Both hash entry points carry the alias, and the retired page id is gone from the whitelists:
|
||||
// #system-update has to land on 系统设置 with its update section selected.
|
||||
const aliases = [...router.matchAll(/const settingsSection = pageId === 'system-update' \? 'update' : '';/g)];
|
||||
assert.equal(aliases.length, 2, 'both hash parsers must normalize #system-update into the settings section');
|
||||
const selects = [...router.matchAll(/if \(settingsSection && typeof switchSettingsSection === 'function'\) \{[\s\S]{0,80}?switchSettingsSection\(settingsSection\);/g)];
|
||||
assert.equal(selects.length, 2, 'both hash paths must open the update section after switching the page');
|
||||
const lists = router.split('\n').filter(l => l.includes("['dashboard'") && l.includes("'settings'"));
|
||||
assert.equal(lists.length, 2, 'settings must stay in both router page lists');
|
||||
assert.ok(lists.every(l => !l.includes("'system-update'")), 'no whitelist may still name the retired page id');
|
||||
assert.doesNotMatch(router, /case 'system-update':/, 'the page init case must be gone with the page');
|
||||
// Leaving the settings page stops the poller; switching sections within it is settings.js's job.
|
||||
assert.match(router, /if \(pageId !== 'settings' && typeof stopUpdatePolling === 'function'\)/);
|
||||
// Coming back to the settings page while the update section is still selected must re-read the
|
||||
// install state and re-arm the poller: leaving the page stopped it, and a frozen progress
|
||||
// read is exactly the "nothing is happening" picture an operator must never be shown.
|
||||
assert.match(router, /case 'settings':[\s\S]{0,450}?getElementById\('settings-section-update'\)[\s\S]{0,250}?loadUpdateConsole\(\)/);
|
||||
|
||||
// Registered in the router twice on purpose: the hash whitelist on boot, and the one used when
|
||||
// a page builds its own hash. A page missing from either is silently unreachable.
|
||||
const lists = router.split('\n').filter(l => l.includes("['dashboard'") && l.includes("'system-update'"));
|
||||
assert.equal(lists.length, 2, 'system-update must appear in both router page lists');
|
||||
assert.match(router, /case 'system-update':\s*\n\s*if \(typeof loadUpdateConsole === 'function'\) loadUpdateConsole\(\);/);
|
||||
// And the leave-page cleanup: the router only calls initPage on entry, so the progress poller
|
||||
// has to be stopped from the same tail that already does this for the tasks page.
|
||||
assert.match(router, /if \(pageId !== 'system-update' && typeof stopUpdatePolling === 'function'\)/);
|
||||
// Selecting the section runs the console; selecting anything else stops its poller.
|
||||
assert.match(settings, /if \(section === 'update'\) \{[\s\S]{0,160}?loadUpdateConsole\(\)/);
|
||||
assert.match(settings, /else if \(typeof stopUpdatePolling === 'function'\) \{[\s\S]{0,120}?stopUpdatePolling\(\);/);
|
||||
|
||||
// And the console's own idea of "on screen" is the settings page plus that section.
|
||||
assert.match(source, /getElementById\('page-settings'\)/);
|
||||
assert.match(source, /getElementById\('settings-section-update'\)/);
|
||||
});
|
||||
|
||||
test('the update console has its own stylesheet section', () => {
|
||||
const header = sheet.indexOf('一键更新控制台(system-update)');
|
||||
const header = sheet.indexOf('一键更新控制台(系统设置页的 settings-section-update 分区)');
|
||||
assert.ok(header > -1, 'style.css must carry the update console section header');
|
||||
const block = sheet.slice(header);
|
||||
assert.ok(block.includes('.update-console'), 'the .update-* section must follow its own section header');
|
||||
['.update-console', '.update-section-title', '.update-facts', '.update-chip-ok', '.update-chip-warn',
|
||||
['.update-console', '.update-section-head', '.update-section-title', '.update-facts', '.update-chip-ok', '.update-chip-warn',
|
||||
'.update-chip-danger', '.update-error', '.update-check-failed', '.update-step-list', '.update-table',
|
||||
'.update-restart-choice', '.update-blocker-list', '.update-apply-btn:disabled'].forEach(sel => {
|
||||
assert.ok(sheet.includes(sel), sel + ' must be defined');
|
||||
@@ -398,10 +433,16 @@ test('all locales carry the same update keys, and everything the page asks for e
|
||||
assert.deepEqual([...enKeys].filter(k => !ruKeys.has(k)), [], 'keys missing from ru-RU');
|
||||
assert.deepEqual([...ruKeys].filter(k => !enKeys.has(k)), [], 'ru-RU has keys other locales do not');
|
||||
assert.ok(zhKeys.size >= 60, 'the update namespace has gone thin: ' + zhKeys.size);
|
||||
assert.ok(zh.nav.systemUpdate && en.nav.systemUpdate && ru.nav.systemUpdate, 'nav.systemUpdate must exist in every locale');
|
||||
assert.equal(zh.nav.systemUpdate, '一键更新');
|
||||
assert.equal(en.nav.systemUpdate, 'One-Click Update');
|
||||
assert.equal(ru.nav.systemUpdate, 'Обновление в один клик');
|
||||
// The settings menu is where this feature's entry lives now; the old nav key must be retired
|
||||
// everywhere rather than left behind as a second, drifting label.
|
||||
assert.ok(zh.settings.nav.update && en.settings.nav.update && ru.settings.nav.update,
|
||||
'settings.nav.update must exist in every locale');
|
||||
assert.equal(zh.settings.nav.update, '一键更新');
|
||||
assert.equal(en.settings.nav.update, 'One-Click Update');
|
||||
assert.equal(ru.settings.nav.update, 'Обновление в один клик');
|
||||
[['zh-CN', zh], ['en-US', en], ['ru-RU', ru]].forEach(([label, dict]) => {
|
||||
assert.ok(!dict.nav.systemUpdate, label + ' still carries the retired nav.systemUpdate key');
|
||||
});
|
||||
|
||||
// The non-Chinese dictionaries must not be holding Chinese text.
|
||||
[...enKeys].forEach(k => {
|
||||
@@ -416,7 +457,7 @@ test('all locales carry the same update keys, and everything the page asks for e
|
||||
});
|
||||
|
||||
const htmlKeys = [...template.matchAll(/data-i18n="([^"]+)"/g)].map(m => m[1])
|
||||
.filter(k => k.startsWith('update.') || k === 'nav.systemUpdate');
|
||||
.filter(k => k.startsWith('update.') || k === 'settings.nav.update');
|
||||
assert.ok(htmlKeys.length >= 3, 'the page must carry its own translated titles: ' + htmlKeys);
|
||||
htmlKeys.forEach(k => {
|
||||
assert.ok(resolve(zh, k), 'zh-CN missing ' + k);
|
||||
|
||||
@@ -71,8 +71,11 @@ function scheduleUpdateAutoCheck() {
|
||||
}
|
||||
|
||||
function isUpdateConsoleActive() {
|
||||
const page = document.getElementById('page-system-update');
|
||||
return !!(page && page.classList.contains('active'));
|
||||
// 控制台住在系统设置页的「一键更新」分区里:页面激活还不够,分区也得是当前选中的那个。
|
||||
const page = document.getElementById('page-settings');
|
||||
const section = document.getElementById('settings-section-update');
|
||||
return !!(page && page.classList.contains('active') &&
|
||||
section && section.classList.contains('active'));
|
||||
}
|
||||
|
||||
function updateStatusOf() {
|
||||
|
||||
+13
-22
@@ -445,16 +445,6 @@
|
||||
<span data-i18n="nav.platformRbac">平台权限</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav-item" data-page="system-update">
|
||||
<div class="nav-item-content" data-title="一键更新" onclick="switchPage('system-update')" data-i18n="nav.systemUpdate" data-i18n-attr="data-title" data-i18n-skip-text="true">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M4 14.899A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.5 8.242"></path>
|
||||
<path d="M12 12v9"></path>
|
||||
<path d="m8 17 4 4 4-4"></path>
|
||||
</svg>
|
||||
<span data-i18n="nav.systemUpdate">一键更新</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav-item" data-page="settings">
|
||||
<div class="nav-item-content" data-title="系统设置" onclick="switchPage('settings')" data-i18n="nav.settings" data-i18n-attr="data-title" data-i18n-skip-text="true">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
@@ -3582,18 +3572,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="page-system-update" class="page">
|
||||
<div class="page-header">
|
||||
<h2 data-i18n="update.title">一键更新</h2>
|
||||
<div class="page-header-actions">
|
||||
<button class="btn-secondary" onclick="loadUpdateConsole()" data-i18n="common.refresh">刷新</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="page-content">
|
||||
<p class="update-intro" data-i18n="update.intro">这一页改的就是当前正在应答请求的这套安装:拉取它自己跟踪的远端、重新编译、换掉二进制。你本机维护的 roles/skills/tools 与配置不会被覆盖。</p>
|
||||
<div id="update-console" class="update-console"></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 系统设置页面 -->
|
||||
<div id="page-settings" class="page">
|
||||
<div class="page-header">
|
||||
@@ -3633,6 +3611,9 @@
|
||||
<div class="settings-nav-item" data-section="storage" onclick="switchSettingsSection('storage')">
|
||||
<span data-i18n="settings.nav.storage">存储清理</span>
|
||||
</div>
|
||||
<div class="settings-nav-item" data-section="update" data-require-permission="update:read" onclick="switchSettingsSection('update')">
|
||||
<span data-i18n="settings.nav.update">一键更新</span>
|
||||
</div>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
@@ -5202,6 +5183,16 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 一键更新 -->
|
||||
<div id="settings-section-update" class="settings-section-content" data-require-permission="update:read">
|
||||
<div class="settings-section-header update-section-head">
|
||||
<h3 data-i18n="update.title">一键更新</h3>
|
||||
<button class="btn-secondary" type="button" onclick="loadUpdateConsole()" data-i18n="common.refresh">刷新</button>
|
||||
</div>
|
||||
<p class="update-intro" data-i18n="update.intro">这一页改的就是当前正在应答请求的这套安装:拉取它自己跟踪的远端、重新编译、换掉二进制。你本机维护的 roles/skills/tools 与配置不会被覆盖。</p>
|
||||
<div id="update-console" class="update-console"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user