${esc(entry.host.name || 'Frame Flow')} ${frames.map(frame => `${esc(frame.name || text('画框', 'Frame'))} `).join('')}
${current ? text('在线', 'Online') : text('状态未确认', 'Unconfirmed')} ${!current ? `${text('以下为上次采集数据', 'Showing the last collected data')}
` : ''}${cards}
${(snapshot.alerts || []).length || !current || snapshot.alerts_complete !== true ? `${text('已读取范围内的告警', 'Alerts within monitored scope')} ${summaryStatus(scopeAlertHealth(snapshot.alerts, snapshot.alerts_complete, current), state.LANG)}${alertRows || (current && snapshot.alerts_complete ? text('无告警', 'No alerts') : text('状态未确认', 'Unconfirmed'))}
` : ""} `;
+ return `
${esc(entry.host.name || 'Frame Flow')} ${managementLink(entry.host)}${frames.map(frame => `${esc(frame.name || text('画框', 'Frame'))} `).join('')}
${current ? text('在线', 'Online') : text('状态未确认', 'Unconfirmed')} ${!current ? `${text('以下为上次采集数据', 'Showing the last collected data')}
` : ''}${cards}
${(snapshot.alerts || []).length || !current || snapshot.alerts_complete !== true ? `${text('已读取范围内的告警', 'Alerts within monitored scope')} ${summaryStatus(scopeAlertHealth(snapshot.alerts, snapshot.alerts_complete, current), state.LANG)}${alertRows || (current && snapshot.alerts_complete ? text('无告警', 'No alerts') : text('状态未确认', 'Unconfirmed'))}
` : ""} `;
+}
+
+
+export function renderConfig() {
+ const host = state.fleetHosts.find(h => h.id === state.currentConfigHost);
+ const connection = host.connection || {};
+ $('mainSavebar').classList.add('hidden');
+ $('configMenu').innerHTML = `${text('Frame Flow 接入', 'Frame Flow connection')} ${text('配置保存在 Console;管理入口留空时,首页链接使用 Frame Flow 服务地址。API 令牌留空保留现有值。', 'Settings are stored in Console. Leave the management URL blank to use the Frame Flow service URL on the overview. A blank API token keeps the existing value.')}
${[
+ ['url', text('Frame Flow 地址', 'Frame Flow URL'), connection.url || host.url || ''],
+ ['token', text('API 令牌(留空保留)', 'API token (blank keeps existing)'), ''],
+ ['management_url', text('管理入口地址(留空使用服务地址)', 'Management URL (blank uses service URL)'), connection.management_url || ''],
+ ].map(([key, label, value]) => `
${label}
`).join('')}
${text('保存接入配置', 'Save connection')}
`;
+ const page = $('configRows').querySelector('[data-config-page="frame-flow"]');
+ $('configMenu').onclick = event => {
+ if (!event.target.closest('[data-config-section="frame-flow"]')) return;
+ $('configMenu').querySelectorAll('button').forEach(b => b.classList.toggle('active', b.dataset.configSection === 'frame-flow'));
+ $('configRows').querySelectorAll('[data-config-page]').forEach(p => p.classList.toggle('active', p === page));
+ $('mainSavebar').classList.add('hidden');
+ };
+ $('saveFrameFlowConnection').onclick = async () => {
+ const button = $('saveFrameFlowConnection'), message = $('frameFlowConnectionMessage');
+ const values = Object.fromEntries([...page.querySelectorAll('[data-frame-flow-connection]')].map(input => [input.dataset.frameFlowConnection, input.value.trim()]));
+ for (const [key, value] of Object.entries(values)) {
+ if (key === 'token' || (key === 'management_url' && !value)) continue;
+ try {
+ const url = new URL(value);
+ if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password || url.pathname !== '/' || url.search || url.hash) throw Error();
+ } catch {
+ message.textContent = text('请使用不含路径或账号密码的 HTTP(S) 根地址', 'Use an HTTP(S) origin without paths or credentials');
+ message.className = 'error'; return;
+ }
+ }
+ button.disabled = true;
+ try {
+ const result = await get('/api/v1/hosts', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ operation: 'save', id: host.id, adapter_id: host.adapter_id, connection: values }) });
+ state.fleetHosts = result.hosts;
+ page.querySelector('[data-frame-flow-connection="token"]').value = '';
+ message.textContent = text('已保存', 'Saved'); message.className = 'success';
+ } catch (error) { message.textContent = error.message; message.className = 'error'; }
+ finally { button.disabled = false; }
+ };
}
diff --git a/pyproject.toml b/pyproject.toml
index b2fdcc4..9c840bc 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "pigway-console-adapter-frame-flow"
-version = "1.0.3"
+version = "1.0.4"
requires-python = ">=3.12"
dependencies = []
readme = "README.md"
diff --git a/tests/ui.cjs b/tests/ui.cjs
index 4f52aa6..83ab44e 100644
--- a/tests/ui.cjs
+++ b/tests/ui.cjs
@@ -1,35 +1,73 @@
-const {chromium}=require('playwright');
+const {chromium}=require(process.env.PLAYWRIGHT_MODULE||'playwright');
const fs=require('node:fs'),path=require('node:path'),assert=require('node:assert/strict');
+const root=path.resolve(__dirname,'..'),consoleRoot=path.resolve(root,'../pigway-device-console');
+const manifest=JSON.parse(fs.readFileSync(path.join(root,'plugin/manifest.json')));
(async()=>{
-const browser=await chromium.launch({headless:true,channel:'chrome'});
-try {
-const source=fs.readFileSync(path.join(__dirname,'../plugin/ui/index.js'),'utf8').replace(/^import .*;\n/gm,'').replace('export function hostCard','function hostCard');
-const labels={service_ready:{zh:'服务就绪',en:'Service ready'},device_online:{zh:'画框在线',en:'Frame online'},battery_percent:{zh:'电量',en:'Battery'}};
-const entry={host:{name:'Frame Flow'},dataCurrent:true,snapshot:{resources:[{id:'service',name:'Frame Flow',type:'service'},{id:'device:living-room',name:'渡口浪人的相框',type:'device'}],metrics:[{resource_id:'service',key:'service_ready',value:1,unit:'boolean',label:labels.service_ready},{resource_id:'device:living-room',key:'device_online',value:1,unit:'boolean',label:labels.device_online},{resource_id:'device:living-room',key:'battery_percent',value:28,unit:'percent',label:labels.battery_percent}],alerts:[]}};
-entry.snapshot.resources.push({id:'job:slideshow',name:'家庭照片轮播',type:'job'});
-entry.snapshot.metrics.push({resource_id:'job:slideshow',key:'job_enabled',value:1,unit:'boolean'},{resource_id:'job:slideshow',key:'last_run_success',value:1,unit:'boolean'});
-for(const lang of ['zh','en'])for(const width of [1440,820,390]) {
-const page=await browser.newPage({bypassCSP:true,viewport:{width,height:1000}});
-await page.goto('http://192.168.89.19/');
-const result=await page.evaluate(async ({source,entry,lang})=>{
- const {scopeAlertHealth,summaryStatus}=await import('/assets/js/overview-health.js');
- const state={LANG:lang};const text=(zh,en)=>lang==='zh'?zh:en;
- const esc=v=>String(v??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
- const html=eval(source+'\nhostCard(entry)');
- document.body.innerHTML=`