import { scopeAlertHealth, summaryStatus } from '/assets/js/overview-health.js'; import { state } from '/assets/js/state.js'; import { $, esc, pluginText as text } from '/assets/js/shared.js'; import { get } from '/assets/js/navigation.js'; function managementLink(host) { try { const url = new URL(host.connection?.management_url || host.connection?.url || host.url); if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password) return ''; const label = text('打开 Frame Flow 管理', 'Open Frame Flow management'); return ``; } catch { return ''; } } const metricValue = metric => { if (metric.unit === 'percent') return `${metric.value}%`; if (metric.unit === 'count') return `${metric.value} ${text('个', 'items')}`; const values = { service_ready: [text('未就绪', 'Not ready'), text('就绪', 'Ready')], device_online: [text('离线', 'Offline'), text('在线', 'Online')], job_enabled: [text('停用', 'Disabled'), text('启用', 'Enabled')], last_run_success: [text('失败或待确认', 'Failed or unconfirmed'), text('成功', 'Succeeded')], }; return values[metric.key]?.[metric.value] ?? String(metric.value); }; export function hostCard(entry) { const snapshot = entry.snapshot || {}; const resources = snapshot.resources || []; const current = entry.dataCurrent === true; const label = item => item.label?.[state.LANG] || item.key || item.id; const name = id => resources.find(r => r.id === id)?.name || id; const alertRows = (snapshot.alerts || []).map(alert => { const separator = alert.id.indexOf(':'); const kind = alert.id.split(':')[0]; const resource = separator < 0 ? '' : `${kind.startsWith('JOB_') ? 'job' : 'device'}:${alert.id.slice(separator + 1)}`; return `
${esc(label(alert))}${esc(resource ? name(resource) : '')}
`; }).join(''); const metrics = snapshot.metrics || []; const value = (resource, key) => metrics.find(m => m.resource_id === resource.id && m.key === key)?.value; const card = (title, content, status = '') => `
${esc(title)}
${status}
${esc(content)}
`; const status = (semantic, title) => `${esc(title)}`; const cards = resources.flatMap(resource => { if (resource.type === 'job') { const enabled = value(resource, 'job_enabled') === 1; const result = value(resource, 'last_run_success'); const unknown = (snapshot.alerts || []).some(a => a.id === `JOB_UNKNOWN:${resource.id.slice(4)}`); const kind = !current || unknown || result === undefined ? 'unknown' : result === 1 ? 'normal' : 'error'; const stateLabel = kind === 'normal' ? text('正常', 'Normal') : kind === 'error' ? text('失败', 'Failed') : text('待确认', 'Unconfirmed'); const title = /轮播/.test(resource.name || '') ? text('轮播任务', 'Slideshow task') : resource.name || text('任务', 'Job'); return [card(title, `${resource.name || text('任务', 'Job')} · ${enabled ? text('启用', 'Enabled') : text('停用', 'Disabled')}`, status(kind, stateLabel))]; } return metrics.filter(m => m.resource_id === resource.id).map(metric => { const title = label(metric); return card(title, metricValue(metric)); }); }).join(''); const frames = resources.filter(resource => resource.type === 'device'); 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 = ``; $('configRows').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]) => `
`).join('')}
`; 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; } }; }