Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .astrbot-plugin/i18n/en-US.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"pages": {
"dashboard": {
"title": "Dashboard"
}
}
}
7 changes: 7 additions & 0 deletions .astrbot-plugin/i18n/zh-CN.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"pages": {
"dashboard": {
"title": "Dashboard"
}
}
}
187 changes: 187 additions & 0 deletions pages/dashboard/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Self Learning Dashboard</title>
<script src="/api/plugin/page/bridge-sdk.js"></script>
<style>
:root {
color-scheme: light;
--bg: #eef3f8;
--panel: #ffffff;
--text: #102033;
--muted: #64748b;
--border: #d8e1ec;
--accent: #2563eb;
--accent-soft: rgba(37, 99, 235, 0.12);
--danger: #dc2626;
--shadow: 0 16px 40px rgba(15, 23, 42, 0.10);
}

[data-theme="dark"] {
color-scheme: dark;
--bg: #0b111c;
--panel: #121c2b;
--text: #e5edf7;
--muted: #9aa8bc;
--border: #273449;
--accent: #60a5fa;
--accent-soft: rgba(96, 165, 250, 0.16);
--danger: #f87171;
--shadow: 0 16px 40px rgba(0, 0, 0, 0.36);
}

* {
box-sizing: border-box;
}

html,
body {
margin: 0;
width: 100%;
height: 100%;
background: var(--bg);
color: var(--text);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Microsoft YaHei", sans-serif;
}

body {
overflow: hidden;
}

.shell {
width: 100%;
height: 100%;
min-height: 100vh;
}

.frame-wrap {
position: relative;
width: 100%;
height: 100%;
min-height: 0;
}

iframe {
display: block;
width: 100%;
height: 100%;
border: 0;
background: var(--bg);
}

.status {
position: absolute;
inset: 0;
display: grid;
place-items: center;
padding: 20px;
background: var(--bg);
}

.status-card {
width: min(520px, 100%);
padding: 24px;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--panel);
box-shadow: var(--shadow);
}

.status-title {
margin: 0;
font-size: 20px;
letter-spacing: 0;
line-height: 1.3;
}

.status-text {
margin: 10px 0 0;
color: var(--muted);
font-size: 14px;
line-height: 1.7;
}

.status.error .status-title {
color: var(--danger);
}

.hidden {
display: none;
}

</style>
</head>
<body>
<div class="shell">
<main class="frame-wrap">
<iframe id="dashboardFrame" title="Self Learning Dashboard"></iframe>
<section class="status" id="status">
<div class="status-card">
<h2 class="status-title" id="statusTitle">正在加载 Dashboard</h2>
<p class="status-text" id="statusText">正在通过 AstrBot Plugin Page bridge 读取 Self Learning WebUI 地址。</p>
</div>
</section>
</main>
</div>

<script>
(function () {
const statusEl = document.getElementById('status');
const statusTitleEl = document.getElementById('statusTitle');
const statusTextEl = document.getElementById('statusText');
const frameEl = document.getElementById('dashboardFrame');

function fallbackDashboardUrl() {
const host = window.location.hostname || '127.0.0.1';
const safeHost = host.includes(':') && !host.startsWith('[') ? `[${host}]` : host;
return `http://${safeHost}:7833/static/html/dashboard.html`;
Comment on lines +136 to +139

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Consider deriving the fallback URL scheme from the current page to avoid mixed-content issues.

fallbackDashboardUrl always uses http://. When AstrBot runs over HTTPS, this will trigger mixed-content blocking and the iframe won’t load. Please derive the scheme from window.location.protocol (or the current origin) so the dashboard URL matches the page’s protocol.

For example:

const scheme = window.location.protocol === 'https:' ? 'https' : 'http';
return `${scheme}://${safeHost}:7833/static/html/dashboard.html`;

}

function setStatus(title, message, tone) {
statusTitleEl.textContent = title;
statusTextEl.textContent = message;
statusEl.className = `status${tone ? ` ${tone}` : ''}`;
}

function hideStatus() {
statusEl.classList.add('hidden');
}

function setDashboardUrl(url) {
frameEl.src = url;
}

frameEl.addEventListener('load', hideStatus);

if (!window.AstrBotPluginPage) {
setDashboardUrl(fallbackDashboardUrl());
return;
}

window.AstrBotPluginPage.ready().then(async () => {
try {
const payload = await window.AstrBotPluginPage.apiGet('dashboard_url');
if (!payload || !payload.url) {
throw new Error('dashboard_url 未返回 url');
}
setDashboardUrl(payload.url);
} catch (error) {
setStatus(
'无法加载 Dashboard',
`已进入插件 Page,但读取独立 WebUI 地址失败:${error.message || error}`,
'error',
);
}
}).catch((error) => {
setStatus(
'插件页面上下文读取失败',
error.message || String(error),
'error',
);
});
})();
</script>
</body>
</html>
99 changes: 99 additions & 0 deletions tests/integration/test_webui_static_assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

PLUGIN_ROOT = Path(__file__).resolve().parents[2]
HTML_FILES = [
PLUGIN_ROOT / "pages" / "dashboard" / "index.html",
PLUGIN_ROOT / "web_res" / "static" / "html" / "change_password.html",
PLUGIN_ROOT / "web_res" / "static" / "html" / "dashboard.html",
PLUGIN_ROOT / "web_res" / "static" / "html" / "graph_share.html",
Expand Down Expand Up @@ -41,6 +42,26 @@ def test_webui_frontend_vendor_assets_exist():
assert path.exists(), f"Missing vendored frontend asset: {path}"


def test_astrbot_plugin_pages_dashboard_entry_exists():
text = (PLUGIN_ROOT / "pages" / "dashboard" / "index.html").read_text(encoding="utf-8")
zh_i18n = (PLUGIN_ROOT / ".astrbot-plugin" / "i18n" / "zh-CN.json").read_text(encoding="utf-8")
en_i18n = (PLUGIN_ROOT / ".astrbot-plugin" / "i18n" / "en-US.json").read_text(encoding="utf-8")

assert "AstrBotPluginPage" in text
assert "/api/plugin/page/bridge-sdk.js" in text
assert "apiGet('dashboard_url')" in text
assert 'id="dashboardFrame"' in text
assert "setDashboardUrl(payload.url)" in text
assert "toolbar" not in text
assert "brand-title" not in text
assert "brand-meta" not in text
assert "pageLabel" not in text
assert "openDashboard" not in text
assert "reloadDashboard" not in text
assert '"dashboard"' in zh_i18n
assert '"dashboard"' in en_i18n


def test_dashboard_exposes_learning_content_browser():
text = (PLUGIN_ROOT / "web_res" / "static" / "html" / "dashboard.html").read_text(encoding="utf-8")

Expand Down Expand Up @@ -72,6 +93,84 @@ def test_dashboard_review_details_use_backend_structured_fields():
assert "renderContextExamples(item)" in text


def test_dashboard_review_queue_uses_sidebar_categories():
text = (PLUGIN_ROOT / "web_res" / "static" / "html" / "dashboard.html").read_text(encoding="utf-8")

assert "review-sidebar" in text
assert "review-workspace" in text
assert 'data-review-tab="persona"' in text
assert 'data-review-tab="style"' in text
assert 'data-review-tab="jargon"' in text
assert 'data-review-tab="persona-state"' in text
assert 'data-review-tab="reviewed"' in text
assert 'data-review-tab="batches"' in text
assert "setReviewTab" in text
assert "updateReviewNavCounts" in text


def test_dashboard_settings_uses_sidebar_categories():
text = (PLUGIN_ROOT / "web_res" / "static" / "html" / "dashboard.html").read_text(encoding="utf-8")

assert "settings-sidebar" in text
assert "settings-workspace" in text
assert "settingsSidebarSummary" in text
assert "settingsNav" in text
assert 'data-config-group="all"' in text
assert "dependencyInstallPanel" in text
assert "renderSettingsNav" in text
assert "setConfigGroup" in text
assert "configGroupIcon" in text
assert "SETTINGS_NAV_SECTIONS" in text
assert "常用入口" in text
assert "学习链路" in text
assert "人格与关系" in text
assert "数据与运行" in text
assert "settings-nav::-webkit-scrollbar" in text
assert "scrollbar-width: thin" in text
assert "Dependency_Install_Guide: 'extension'" in text
assert "deployed_code" not in text
Comment on lines +108 to +131

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Avoid over-coupling dashboard settings tests to exact copy and raw substrings

These assertions validate the new sidebar structure and key labels, but relying on long exact strings (full Chinese labels, settings-nav::-webkit-scrollbar, exact CSS declarations) and negative substring checks (like ensuring "deployed_code" is absent) makes the test brittle to harmless copy or CSS changes. Prefer asserting on stable structural markers (ids, data attributes, function names), and loosen or narrow text/style checks so they confirm the intended layout/behavior without depending on exact CSS serialization or full label text.

Suggested change
assert "updateReviewNavCounts" in text
def test_dashboard_settings_uses_sidebar_categories():
text = (PLUGIN_ROOT / "web_res" / "static" / "html" / "dashboard.html").read_text(encoding="utf-8")
assert "settings-sidebar" in text
assert "settings-workspace" in text
assert "settingsSidebarSummary" in text
assert "settingsNav" in text
assert 'data-config-group="all"' in text
assert "dependencyInstallPanel" in text
assert "renderSettingsNav" in text
assert "setConfigGroup" in text
assert "configGroupIcon" in text
assert "SETTINGS_NAV_SECTIONS" in text
assert "常用入口" in text
assert "学习链路" in text
assert "人格与关系" in text
assert "数据与运行" in text
assert "settings-nav::-webkit-scrollbar" in text
assert "scrollbar-width: thin" in text
assert "Dependency_Install_Guide: 'extension'" in text
assert "deployed_code" not in text
assert "updateReviewNavCounts" in text
+
+
+def test_dashboard_settings_uses_sidebar_categories():
+ text = (PLUGIN_ROOT / "web_res" / "static" / "html" / "dashboard.html").read_text(encoding="utf-8")
+
+ # Structural hooks for the settings sidebar and workspace layout
+ assert "settings-sidebar" in text
+ assert "settings-workspace" in text
+ assert "settingsSidebarSummary" in text
+ assert "settingsNav" in text
+
+ # Config grouping and navigation rendering hooks
+ assert 'data-config-group="all"' in text
+ assert "dependencyInstallPanel" in text
+ assert "renderSettingsNav" in text
+ assert "setConfigGroup" in text
+ assert "configGroupIcon" in text
+ assert "SETTINGS_NAV_SECTIONS" in text



def test_dashboard_settings_right_pane_uses_dense_controls():
text = (PLUGIN_ROOT / "web_res" / "static" / "html" / "dashboard.html").read_text(encoding="utf-8")

assert "grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));" in text
assert "function configFieldLayoutClasses" in text
assert "is-bool" in text
assert "is-list" in text
assert "is-wide" in text
assert "function renderConfigSwitch" in text
assert 'class="config-switch-input"' in text
assert "switch-track" in text
assert "switch-thumb" in text
assert "bool-state" in text
assert "field.type === 'bool'" in text
assert "renderConfigSwitch(field, fieldId, Boolean(displayValue))" in text
assert "function syncConfigSwitchState" in text


def test_dashboard_global_header_uses_compact_brand_copy():
text = (PLUGIN_ROOT / "web_res" / "static" / "html" / "dashboard.html").read_text(encoding="utf-8")

assert '<div class="app-brand">Self Learning</div>' in text
assert "SELF LEARNING / DASHBOARD" not in text
assert '<div class="brand">' not in text
assert 'id="summaryText"' not in text
assert 'id="refreshBtn"' in text
assert 'id="settingsJumpBtn"' in text
assert 'id="themeBtn"' in text


def test_dashboard_jargon_sort_does_not_expose_misleading_occurrences_order():
text = (PLUGIN_ROOT / "web_res" / "static" / "html" / "dashboard.html").read_text(encoding="utf-8")

assert 'value="occurrences"' not in text
assert "按出现次数" not in text
assert "sort === 'occurrences'" not in text
assert "样本 ${formatCount(item.occurrences)}" in text
assert "['样本', jargonStats.total_occurrences]" in text


def test_dashboard_review_deletes_use_inline_confirmation():
text = (PLUGIN_ROOT / "web_res" / "static" / "html" / "dashboard.html").read_text(encoding="utf-8")

Expand Down
43 changes: 43 additions & 0 deletions tests/unit/test_webui_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,46 @@ async def start(self):
assert database_manager.start_calls == 1
assert manager._database_degraded is True
assert manager._database_start_error == "connection was closed in the middle of operation"


def test_webui_manager_registers_astrbot_plugin_page_dashboard_url_api():
calls = []
context = SimpleNamespace(
register_web_api=lambda route, handler, methods, desc: calls.append(
(route, handler, methods, desc)
)
)
config = SimpleNamespace(
enable_web_interface=True,
web_interface_host="0.0.0.0",
web_interface_port=7833,
)

WebUIManager(
plugin_config=config,
context=context,
factory_manager=object(),
perf_tracker="perf",
group_id_to_unified_origin={},
plugin_instance=SimpleNamespace(name="astrbot_plugin_self_learning"),
)

routes = {item[0] for item in calls}
assert "astrbot_plugin_self_learning/dashboard_url" in routes
assert any(item[2] == ["GET"] for item in calls)


def test_webui_manager_public_webui_url_uses_configured_host():
manager = WebUIManager(
plugin_config=SimpleNamespace(
enable_web_interface=True,
web_interface_host="192.168.114.2",
web_interface_port=7833,
),
context=SimpleNamespace(),
factory_manager=object(),
perf_tracker="perf",
group_id_to_unified_origin={},
)

assert manager._public_webui_base_url() == "http://192.168.114.2:7833"
Loading
Loading