From 9cb5e7659302b78c428d86d2127551d854988c75 Mon Sep 17 00:00:00 2001 From: SandObserver <260779319+SandObserver@users.noreply.github.com> Date: Tue, 18 Aug 2026 17:20:02 -0400 Subject: [PATCH] Feature: read System Summary figures from Beszel Adds Beszel as a source alongside Glances and the local machine. The provider reads the chosen system's latest one-minute record for CPU, memory, temperatures by sensor name, root and named filesystems, and IO wait, and the system record for uptime. Process count is not offered, since Beszel does not report one. Beszel is a PocketBase app, so a login returns a token rather than accepting a key, and the account collection differs between releases. Both collections are tried and the working one is remembered with its session. An expired token is not refused by PocketBase: the read rule applies instead and an unauthenticated list is empty, which is indistinguishable from a hub with no systems. An empty answer is therefore retried once with a fresh session before it is believed. --- CHANGELOG.md | 2 +- README.md | 2 +- api/test/system-summary-beszel-data.test.js | 176 ++++++++++++++++++++ ui/widgets/system-summary/data.js | 158 +++++++++++++++++- ui/widgets/system-summary/i18n/de.json | 13 ++ ui/widgets/system-summary/i18n/en.json | 13 ++ ui/widgets/system-summary/i18n/es.json | 13 ++ ui/widgets/system-summary/i18n/fa.json | 13 ++ ui/widgets/system-summary/i18n/fr.json | 13 ++ ui/widgets/system-summary/i18n/zh-Hans.json | 13 ++ ui/widgets/system-summary/widget.json | 125 ++++++++++++++ 11 files changed, 538 insertions(+), 3 deletions(-) create mode 100644 api/test/system-summary-beszel-data.test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index c7185dc4..597c72d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- System Summary can read its figures from a host running Glances. +- System Summary can read its figures from a host running Glances or Beszel. ### Changed diff --git a/README.md b/README.md index c7822181..0e44c6b5 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ Widgets and the services they read: - **DNS**: AdGuard, Pi-hole, Technitium, NextDNS - **GitHub**: contribution graph and pull requests - **Books**: Audiobookshelf, Komga, Kavita -- **System summary**: CPU, memory, disk, throughput, uptime, and network speed from SpeedTest Tracker or MySpeed. Reports on this machine or on a host running Glances +- **System summary**: CPU, memory, disk, throughput, uptime, and network speed from SpeedTest Tracker or MySpeed. Reports on this machine, or on a host running Glances or Beszel - **Disk health**: TrueNAS, Scrutiny - **Backup**: Duplicati, Kopia - **Connections**: Gluetun, Psiphon Conduit, Netbird, Plausible, Umami diff --git a/api/test/system-summary-beszel-data.test.js b/api/test/system-summary-beszel-data.test.js new file mode 100644 index 00000000..17a97a3c --- /dev/null +++ b/api/test/system-summary-beszel-data.test.js @@ -0,0 +1,176 @@ +const { test } = require('node:test'); +const assert = require('node:assert/strict'); +const path = require('node:path'); +const { normalizeBase } = require('../src/widget-data'); +const { dispatchProvider } = require('../src/provider-dispatch'); +const { errorParts } = require('../test-support/widget-ctx'); + +const dataFn = require(path.join(__dirname, '..', '..', 'ui', 'widgets', 'system-summary', 'data.js')); + +const SYSTEM = { id: 'sys1', name: 'homelab', status: 'up' }; +const STATS = { + cpu: 31.58, + mp: 43.77, + d: 465.88, + du: 120.31, + dp: 26.98, + t: { coretemp_package_id_0: 69, iwlwifi_1: 48 }, + cpub: [20.36, 9.3, 45.52, 0, 22.9], + efs: { media: { d: 2000, du: 500 } }, +}; +const INFO = { info: { u: 267222, cpu: 31.58 } }; + +/* Stands in for a PocketBase hub: a login hands out a token, and a request + carrying anything else is answered the way PocketBase answers an + unauthenticated read, with an empty list rather than a refusal. */ +function hub({ collection = '_superusers', password = 'pw', tokens = ['t1', 't2'] } = {}) { + const state = { issued: 0, logins: [], reads: [] }; + const live = new Set(); + return { + state, + expire: () => live.clear(), + fetchJSON: async (url, opts = {}) => { + const login = /\/api\/collections\/(\w+)\/auth-with-password$/.exec(url); + if (login) { + state.logins.push(login[1]); + const body = JSON.parse(opts.body || '{}'); + if (login[1] !== collection || body.password !== password) return { status: 400, data: { message: 'Failed' } }; + const token = tokens[Math.min(state.issued++, tokens.length - 1)]; + live.add(token); + return { status: 200, data: { token } }; + } + state.reads.push(url); + const authed = live.has(opts.headers?.Authorization); + if (url.includes('/systems/records/')) { + return authed ? { status: 200, data: INFO } : { status: 404, data: null }; + } + if (url.includes('/systems/records')) { + return { status: 200, data: { items: authed ? [SYSTEM] : [] } }; + } + if (url.includes('/system_stats/records')) { + return { status: 200, data: { items: authed ? [{ stats: STATS }] : [] } }; + } + return { status: 404, data: null }; + }, + }; +} + +function ctxFor(config, h = hub(), endpoint = 'system') { + const ctx = { + endpoint, + config: { + statProvider: 'beszel', + beszelUrl: 'http://beszel:8090', + beszelUser: 'me@x', + beszelPass: 'pw', + ...config, + }, + normalizeBase, + fetchJSON: h.fetchJSON, + ...errorParts(), + }; + ctx.dispatchProvider = (handlers, opts) => dispatchProvider(ctx, handlers, opts); + return ctx; +} + +const SLOTS = [ + { type: 'cpu' }, + { type: 'temp', sensor: 'coretemp_package_id_0' }, + { type: 'disk', primary: '/', secondary: 'media' }, +]; + +test('a system maps onto the shape the widget already renders', async () => { + const r = await dataFn(ctxFor({ beszelSystem: 'sys1', slots: SLOTS })); + assert.equal(r.cpu, 31.58); + assert.equal(r.ram, 43.77); + assert.equal(r.iowait, 45.52); + assert.equal(r.uptime, 267222); + assert.equal(r.procs, null, 'Beszel reports no process count'); + assert.deepEqual(r.temps, { coretemp_package_id_0: 69, iwlwifi_1: 48 }); + assert.deepEqual(r.disks, [ + { mount: '/', usedPct: 26.98, totalGb: 465.88 }, + { mount: 'media', usedPct: 25, totalGb: 2000 }, + ]); +}); + +test('a disk slot left blank reads the root filesystem', async () => { + const r = await dataFn(ctxFor({ beszelSystem: 'sys1', slots: [{ type: 'disk' }] })); + assert.deepEqual(r.disks, [{ mount: '/', usedPct: 26.98, totalGb: 465.88 }]); +}); + +test('a filesystem the hub does not report reads as empty', async () => { + const r = await dataFn(ctxFor({ beszelSystem: 'sys1', slots: [{ type: 'disk', primary: 'nope' }] })); + assert.deepEqual(r.disks, [{ mount: 'nope', usedPct: 0, totalGb: 0 }]); +}); + +test('the account collection that answers is the one reused afterwards', async () => { + const h = hub({ collection: 'users' }); + await dataFn(ctxFor({ beszelUrl: 'http://legacy:8090', beszelSystem: 'sys1', slots: SLOTS }, h)); + assert.deepEqual(h.state.logins, ['_superusers', 'users'], 'both are tried, in order'); + const before = h.state.logins.length; + await dataFn(ctxFor({ beszelUrl: 'http://legacy:8090', beszelSystem: 'sys1', slots: SLOTS }, h)); + assert.equal(h.state.logins.length, before, 'a second poll reuses the session'); +}); + +/* The failure this widget would otherwise get wrong: PocketBase answers a stale + token with an empty list, which reads exactly like a hub with no systems. */ +test('an expired session is renewed rather than reported as an empty hub', async () => { + const h = hub({ tokens: ['first', 'second'] }); + const config = { beszelUrl: 'http://renew:8090', beszelSystem: 'sys1', slots: SLOTS }; + await dataFn(ctxFor(config, h)); + h.expire(); + const r = await dataFn(ctxFor(config, h)); + assert.equal(r.cpu, 31.58, 'the retry with a fresh token returns real figures'); + assert.equal(h.state.issued, 2, 'exactly one extra login'); +}); + +test('a hub that really has no systems says so instead of showing nothing', async () => { + const h = hub(); + h.fetchJSON = async url => + url.includes('auth-with-password') ? { status: 200, data: { token: 't' } } : { status: 200, data: { items: [] } }; + await assert.rejects(dataFn(ctxFor({ beszelUrl: 'http://bare:8090', beszelSystem: 'sys1', slots: SLOTS }, h)), { + message: 'Beszel is reporting no systems for this account', + }); +}); + +test('a rejected password is an auth failure, not a missing system', async () => { + const h = hub({ password: 'other' }); + await assert.rejects( + dataFn(ctxFor({ beszelUrl: 'http://wrongpw:8090', beszelSystem: 'sys1', slots: SLOTS }, h)), + e => { + assert.equal(e.kind, 'auth'); + return true; + }, + ); +}); + +test('a system that has gone away asks for a new choice', async () => { + const r = dataFn(ctxFor({ beszelUrl: 'http://gone:8090', beszelSystem: 'removed', slots: SLOTS })); + await assert.rejects(r, e => { + assert.equal(e.kind, 'invalid'); + assert.match(e.message, /Choose it again/); + return true; + }); +}); + +test('the connection details are required before anything is fetched', async () => { + for (const config of [{ beszelUrl: '' }, { beszelUser: '', beszelSystem: 'sys1' }, { beszelSystem: '' }]) { + await assert.rejects(dataFn(ctxFor({ slots: SLOTS, beszelSystem: 'sys1', ...config })), e => { + assert.equal(e.kind, 'invalid'); + return true; + }); + } +}); + +test('the pickers offer the hub systems and that system sensors', async () => { + const systems = await dataFn(ctxFor({ beszelUrl: 'http://pick:8090', slots: [] }, hub(), 'systems')); + assert.deepEqual(systems.options, [{ value: 'sys1', label: 'homelab' }]); + + const sensors = await dataFn( + ctxFor({ beszelUrl: 'http://pick:8090', beszelSystem: 'sys1', slots: [] }, hub(), 'sensors'), + ); + assert.deepEqual(sensors.options, [ + { value: 'coretemp_package_id_0', label: 'coretemp_package_id_0' }, + { value: 'iwlwifi_1', label: 'iwlwifi_1' }, + ]); +}); diff --git a/ui/widgets/system-summary/data.js b/ui/widgets/system-summary/data.js index ebf84e97..30c2eb18 100644 --- a/ui/widgets/system-summary/data.js +++ b/ui/widgets/system-summary/data.js @@ -1,6 +1,7 @@ module.exports = async function (ctx) { if (ctx.endpoint === 'speed') return speed(ctx); - if (ctx.endpoint === 'sensors') return glancesSensorOptions(ctx); + if (ctx.endpoint === 'sensors') return sensorOptions(ctx); + if (ctx.endpoint === 'systems') return beszelSystemOptions(ctx); return systemSummary(ctx); }; @@ -9,11 +10,21 @@ function systemSummary(ctx) { { system: systemSummaryLocal, glances: systemSummaryGlances, + beszel: systemSummaryBeszel, }, { field: 'statProvider', default: 'system' }, ); } +/* Only a source that names its sensors offers the picker, so the local machine + has no handler here. */ +function sensorOptions(ctx) { + return ctx.dispatchProvider( + { glances: glancesSensorOptions, beszel: beszelSensorOptions }, + { field: 'statProvider' }, + ); +} + /* Mount paths come from the widget's disk slots, then the global stats.diskMount setting, then '/'. */ async function systemSummaryLocal({ config, settings, metrics }) { @@ -188,3 +199,148 @@ async function systemSummaryGlances(ctx) { uptime, }; } + +/* Beszel is a PocketBase app: a login returns a token rather than accepting a + key. The account collection moved between releases, so both are tried and the + working one is remembered with its token. */ +const BESZEL_COLLECTIONS = ['_superusers', 'users']; +const _beszelSession = new Map(); + +async function beszelLogin(ctx, base) { + const { config, fetchJSON } = ctx; + if (!config.beszelUser || !config.beszelPass) + ctx.fail('Enter the Beszel account and password first.', { kind: ctx.KIND.INVALID }); + const body = JSON.stringify({ identity: config.beszelUser, password: config.beszelPass }); + const known = _beszelSession.get(base); + const tries = known ? [known.collection] : BESZEL_COLLECTIONS; + + for (const collection of tries) { + const r = await fetchJSON(`${base}/api/collections/${collection}/auth-with-password`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body, + timeout: 8000, + }); + if (r.status < 400 && r.data?.token) { + const session = { collection, token: r.data.token }; + _beszelSession.set(base, session); + return session; + } + } + _beszelSession.delete(base); + ctx.fail('Beszel rejected the account and password', { kind: ctx.KIND.AUTH }); +} + +/* An expired token is not refused: PocketBase applies the collection's read + rule instead, and an unauthenticated read of systems is an empty list. So an + empty answer is retried once with a fresh token before it is believed. */ +async function beszelGet(ctx, base, path) { + const { fetchJSON } = ctx; + let session = _beszelSession.get(base) || (await beszelLogin(ctx, base)); + for (let attempt = 0; attempt < 2; attempt++) { + const r = await fetchJSON(`${base}${path}`, { + headers: { Authorization: session.token }, + timeout: 8000, + }); + if (r.status === 401 || r.status === 403 || (Array.isArray(r.data?.items) && !r.data.items.length)) { + if (attempt === 0) { + _beszelSession.delete(base); + session = await beszelLogin(ctx, base); + continue; + } + return r.data; + } + if (r.status >= 400) ctx.fail('Beszel HTTP ' + r.status); + return r.data; + } +} + +function beszelBase(ctx) { + if (!ctx.config.beszelUrl) ctx.fail('Enter the Beszel URL first.', { kind: ctx.KIND.INVALID }); + return ctx.normalizeBase(ctx.config.beszelUrl); +} + +async function beszelSystems(ctx) { + const base = beszelBase(ctx); + const data = await beszelGet(ctx, base, '/api/collections/systems/records?perPage=200&fields=id,name,status'); + return Array.isArray(data?.items) ? data.items : []; +} + +async function beszelSystemOptions(ctx) { + const options = (await beszelSystems(ctx)).map(s => ({ value: s.id, label: s.name || s.id })); + return { options }; +} + +async function beszelLatest(ctx, base, systemId) { + const filter = encodeURIComponent(`system='${systemId}' && type='1m'`); + const data = await beszelGet( + ctx, + base, + `/api/collections/system_stats/records?filter=(${filter})&sort=-created&perPage=1`, + ); + return data?.items?.[0]?.stats || null; +} + +async function beszelSensorOptions(ctx) { + const base = beszelBase(ctx); + const id = ctx.config.beszelSystem; + if (!id) ctx.fail('Choose a system first.', { kind: ctx.KIND.INVALID }); + const stats = await beszelLatest(ctx, base, id); + const temps = stats && typeof stats.t === 'object' ? stats.t : {}; + return { options: Object.keys(temps).map(label => ({ value: label, label })) }; +} + +/* The root filesystem is the one Beszel reports directly. Every other one is + named, and a slot names it the way Beszel does. */ +function beszelDisk(stats, name) { + if (!name || name === '/') { + return { usedPct: Number(stats?.dp) || 0, totalGb: Number(stats?.d) || 0 }; + } + const fs = stats?.efs?.[name]; + const total = Number(fs?.d) || 0; + const used = Number(fs?.du) || 0; + return { usedPct: total > 0 ? (used / total) * 100 : 0, totalGb: total }; +} + +async function systemSummaryBeszel(ctx) { + const base = beszelBase(ctx); + const id = ctx.config.beszelSystem; + if (!id) ctx.fail('Choose a system first.', { kind: ctx.KIND.INVALID }); + + const systems = await beszelSystems(ctx); + if (!systems.length) ctx.fail('Beszel is reporting no systems for this account'); + const system = systems.find(s => s.id === id); + if (!system) ctx.fail('That system is no longer in Beszel. Choose it again.', { kind: ctx.KIND.INVALID }); + + const [record, stats] = await Promise.all([ + beszelGet(ctx, base, `/api/collections/systems/records/${encodeURIComponent(id)}?fields=info`), + beszelLatest(ctx, base, id), + ]); + if (!stats) ctx.fail('Beszel has no readings for that system yet'); + + const slots = ctx.config.slots || []; + const mounts = new Set(); + for (const s of slots) { + if (s.type !== 'disk') continue; + mounts.add(s.primary || '/'); + if (s.secondary) mounts.add(s.secondary); + } + const disks = [...mounts].map(mount => ({ mount, ...beszelDisk(stats, mount) })); + + const temps = {}; + for (const [label, value] of Object.entries(stats.t || {})) { + if (typeof value === 'number') temps[label] = value; + } + const iowait = Array.isArray(stats.cpub) && typeof stats.cpub[2] === 'number' ? stats.cpub[2] : null; + + return { + cpu: typeof stats.cpu === 'number' ? stats.cpu : null, + ram: typeof stats.mp === 'number' ? stats.mp : null, + temp: Object.values(temps)[0] ?? null, + temps, + disks, + iowait, + procs: null, + uptime: Number(record?.info?.u) || null, + }; +} diff --git a/ui/widgets/system-summary/i18n/de.json b/ui/widgets/system-summary/i18n/de.json index 0eef4810..addd633b 100644 --- a/ui/widgets/system-summary/i18n/de.json +++ b/ui/widgets/system-summary/i18n/de.json @@ -3,10 +3,18 @@ "statProvider.label": "Quelle", "statProvider.opt.system": "Dieser Rechner", "statProvider.opt.glances": "Glances", + "statProvider.opt.beszel": "Beszel", "glancesUrl.label": "Glances-URL", "glancesUrl.placeholder": "glances:61208", "glancesUser.label": "Benutzername", "glancesPass.label": "Passwort", + "beszelUrl.label": "Beszel-URL", + "beszelUrl.placeholder": "beszel:8090", + "beszelUser.label": "Konto", + "beszelUser.placeholder": "du@example.com", + "beszelPass.label": "Passwort", + "beszelSystem.label": "System", + "beszelSystem.hint": "Oben URL und Konto eintragen, dann die Liste laden.", "slots.label": "Slot", "slots.type.label": "Ressource", "slots.type.opt.cpu": "CPU", @@ -19,10 +27,15 @@ "slots.primary.placeholder": "/", "slots.secondary.label": "Zweiter Einhängepunkt", "slots.secondary.placeholder": "/mnt/data", + "slots.fsPrimary.label": "Erstes Dateisystem", + "slots.fsPrimary.placeholder": "root", + "slots.fsSecondary.label": "Zweites Dateisystem", + "slots.fsSecondary.placeholder": "media", "slots.thermalZone.label": "Thermalzone", "slots.sensor.label": "Sensor", "slots.sensor.hint": "Oben die URL eintragen, dann die Liste laden.", "slots.color.label": "Farbe", + "slots.beszelHint": "Beszel meldet keine Prozessanzahl.", "network.label": "Netzwerk", "network.enabled.label": "Netzwerkzeile anzeigen", "network.mode.label": "Modus", diff --git a/ui/widgets/system-summary/i18n/en.json b/ui/widgets/system-summary/i18n/en.json index 216d58e3..122976e3 100644 --- a/ui/widgets/system-summary/i18n/en.json +++ b/ui/widgets/system-summary/i18n/en.json @@ -3,10 +3,18 @@ "statProvider.label": "Source", "statProvider.opt.system": "This Machine", "statProvider.opt.glances": "Glances", + "statProvider.opt.beszel": "Beszel", "glancesUrl.label": "Glances URL", "glancesUrl.placeholder": "glances:61208", "glancesUser.label": "Username", "glancesPass.label": "Password", + "beszelUrl.label": "Beszel URL", + "beszelUrl.placeholder": "beszel:8090", + "beszelUser.label": "Account", + "beszelUser.placeholder": "you@example.com", + "beszelPass.label": "Password", + "beszelSystem.label": "System", + "beszelSystem.hint": "Enter the URL and account above, then load the list.", "slots.label": "Slot", "slots.type.label": "Resource", "slots.type.opt.cpu": "CPU", @@ -19,10 +27,15 @@ "slots.primary.placeholder": "/", "slots.secondary.label": "Second Mount Path", "slots.secondary.placeholder": "/mnt/data", + "slots.fsPrimary.label": "First Filesystem", + "slots.fsPrimary.placeholder": "root", + "slots.fsSecondary.label": "Second Filesystem", + "slots.fsSecondary.placeholder": "media", "slots.thermalZone.label": "Thermal Zone", "slots.sensor.label": "Sensor", "slots.sensor.hint": "Enter the URL above, then load the list.", "slots.color.label": "Color", + "slots.beszelHint": "Beszel does not report a process count.", "network.label": "Network", "network.enabled.label": "Show Network Row", "network.mode.label": "Mode", diff --git a/ui/widgets/system-summary/i18n/es.json b/ui/widgets/system-summary/i18n/es.json index ff45d411..d3bba69f 100644 --- a/ui/widgets/system-summary/i18n/es.json +++ b/ui/widgets/system-summary/i18n/es.json @@ -3,10 +3,18 @@ "statProvider.label": "Origen", "statProvider.opt.system": "Este equipo", "statProvider.opt.glances": "Glances", + "statProvider.opt.beszel": "Beszel", "glancesUrl.label": "URL de Glances", "glancesUrl.placeholder": "glances:61208", "glancesUser.label": "Usuario", "glancesPass.label": "Contraseña", + "beszelUrl.label": "URL de Beszel", + "beszelUrl.placeholder": "beszel:8090", + "beszelUser.label": "Cuenta", + "beszelUser.placeholder": "tu@example.com", + "beszelPass.label": "Contraseña", + "beszelSystem.label": "Sistema", + "beszelSystem.hint": "Introduce la URL y la cuenta de arriba y carga la lista.", "slots.label": "Ranura", "slots.type.label": "Recurso", "slots.type.opt.cpu": "CPU", @@ -19,10 +27,15 @@ "slots.primary.placeholder": "/", "slots.secondary.label": "Segunda ruta de montaje", "slots.secondary.placeholder": "/mnt/data", + "slots.fsPrimary.label": "Primer sistema de archivos", + "slots.fsPrimary.placeholder": "root", + "slots.fsSecondary.label": "Segundo sistema de archivos", + "slots.fsSecondary.placeholder": "media", "slots.thermalZone.label": "Zona térmica", "slots.sensor.label": "Sensor", "slots.sensor.hint": "Introduce la URL de arriba y carga la lista.", "slots.color.label": "Color", + "slots.beszelHint": "Beszel no informa del número de procesos.", "network.label": "Red", "network.enabled.label": "Mostrar la fila de red", "network.mode.label": "Modo", diff --git a/ui/widgets/system-summary/i18n/fa.json b/ui/widgets/system-summary/i18n/fa.json index 53001076..10b5ee7a 100644 --- a/ui/widgets/system-summary/i18n/fa.json +++ b/ui/widgets/system-summary/i18n/fa.json @@ -3,10 +3,18 @@ "statProvider.label": "منبع", "statProvider.opt.system": "همین دستگاه", "statProvider.opt.glances": "Glances", + "statProvider.opt.beszel": "Beszel", "glancesUrl.label": "نشانی Glances", "glancesUrl.placeholder": "glances:61208", "glancesUser.label": "نام کاربری", "glancesPass.label": "گذرواژه", + "beszelUrl.label": "نشانی Beszel", + "beszelUrl.placeholder": "beszel:8090", + "beszelUser.label": "حساب", + "beszelUser.placeholder": "you@example.com", + "beszelPass.label": "گذرواژه", + "beszelSystem.label": "سامانه", + "beszelSystem.hint": "نشانی و حساب بالا را وارد کنید، سپس فهرست را بارگیری کنید.", "slots.label": "جایگاه", "slots.type.label": "منبع", "slots.type.opt.cpu": "CPU", @@ -19,10 +27,15 @@ "slots.primary.placeholder": "/", "slots.secondary.label": "دومین مسیر اتصال", "slots.secondary.placeholder": "/mnt/data", + "slots.fsPrimary.label": "نخستین سامانهٔ پرونده", + "slots.fsPrimary.placeholder": "root", + "slots.fsSecondary.label": "دومین سامانهٔ پرونده", + "slots.fsSecondary.placeholder": "media", "slots.thermalZone.label": "ناحیهٔ حرارتی", "slots.sensor.label": "حسگر", "slots.sensor.hint": "نشانی بالا را وارد کنید، سپس فهرست را بارگیری کنید.", "slots.color.label": "رنگ", + "slots.beszelHint": "Beszel شمار فرایندها را گزارش نمی‌کند.", "network.label": "شبکه", "network.enabled.label": "نمایش ردیف شبکه", "network.mode.label": "حالت", diff --git a/ui/widgets/system-summary/i18n/fr.json b/ui/widgets/system-summary/i18n/fr.json index 3c44f6bd..f30ce3ac 100644 --- a/ui/widgets/system-summary/i18n/fr.json +++ b/ui/widgets/system-summary/i18n/fr.json @@ -3,10 +3,18 @@ "statProvider.label": "Source", "statProvider.opt.system": "Cette machine", "statProvider.opt.glances": "Glances", + "statProvider.opt.beszel": "Beszel", "glancesUrl.label": "URL Glances", "glancesUrl.placeholder": "glances:61208", "glancesUser.label": "Nom d'utilisateur", "glancesPass.label": "Mot de passe", + "beszelUrl.label": "URL Beszel", + "beszelUrl.placeholder": "beszel:8090", + "beszelUser.label": "Compte", + "beszelUser.placeholder": "vous@example.com", + "beszelPass.label": "Mot de passe", + "beszelSystem.label": "Système", + "beszelSystem.hint": "Saisissez l'URL et le compte ci-dessus, puis chargez la liste.", "slots.label": "Emplacement", "slots.type.label": "Ressource", "slots.type.opt.cpu": "CPU", @@ -19,10 +27,15 @@ "slots.primary.placeholder": "/", "slots.secondary.label": "Second point de montage", "slots.secondary.placeholder": "/mnt/data", + "slots.fsPrimary.label": "Premier système de fichiers", + "slots.fsPrimary.placeholder": "root", + "slots.fsSecondary.label": "Deuxième système de fichiers", + "slots.fsSecondary.placeholder": "media", "slots.thermalZone.label": "Zone thermique", "slots.sensor.label": "Capteur", "slots.sensor.hint": "Saisissez l'URL ci-dessus, puis chargez la liste.", "slots.color.label": "Couleur", + "slots.beszelHint": "Beszel ne rapporte pas le nombre de processus.", "network.label": "Réseau", "network.enabled.label": "Afficher la ligne réseau", "network.mode.label": "Mode", diff --git a/ui/widgets/system-summary/i18n/zh-Hans.json b/ui/widgets/system-summary/i18n/zh-Hans.json index 9f934786..425fb47a 100644 --- a/ui/widgets/system-summary/i18n/zh-Hans.json +++ b/ui/widgets/system-summary/i18n/zh-Hans.json @@ -3,10 +3,18 @@ "statProvider.label": "数据源", "statProvider.opt.system": "本机", "statProvider.opt.glances": "Glances", + "statProvider.opt.beszel": "Beszel", "glancesUrl.label": "Glances 地址", "glancesUrl.placeholder": "glances:61208", "glancesUser.label": "用户名", "glancesPass.label": "密码", + "beszelUrl.label": "Beszel 地址", + "beszelUrl.placeholder": "beszel:8090", + "beszelUser.label": "账户", + "beszelUser.placeholder": "you@example.com", + "beszelPass.label": "密码", + "beszelSystem.label": "系统", + "beszelSystem.hint": "先填写上方地址和账户,然后加载列表。", "slots.label": "插槽", "slots.type.label": "资源", "slots.type.opt.cpu": "CPU", @@ -19,10 +27,15 @@ "slots.primary.placeholder": "/", "slots.secondary.label": "第二个挂载路径", "slots.secondary.placeholder": "/mnt/data", + "slots.fsPrimary.label": "第一个文件系统", + "slots.fsPrimary.placeholder": "root", + "slots.fsSecondary.label": "第二个文件系统", + "slots.fsSecondary.placeholder": "media", "slots.thermalZone.label": "温度区域", "slots.sensor.label": "传感器", "slots.sensor.hint": "先填写上方地址,然后加载列表。", "slots.color.label": "颜色", + "slots.beszelHint": "Beszel 不报告进程数。", "network.label": "网络", "network.enabled.label": "显示网络行", "network.mode.label": "模式", diff --git a/ui/widgets/system-summary/widget.json b/ui/widgets/system-summary/widget.json index 666d8763..de61fb1e 100644 --- a/ui/widgets/system-summary/widget.json +++ b/ui/widgets/system-summary/widget.json @@ -20,6 +20,10 @@ { "value": "glances", "label": "statProvider.opt.glances" + }, + { + "value": "beszel", + "label": "statProvider.opt.beszel" } ] }, @@ -53,6 +57,46 @@ "equals": "glances" } }, + { + "key": "beszelUrl", + "type": "text", + "label": "beszelUrl.label", + "placeholder": "beszelUrl.placeholder", + "showIf": { + "field": "statProvider", + "equals": "beszel" + } + }, + { + "key": "beszelUser", + "type": "text", + "label": "beszelUser.label", + "placeholder": "beszelUser.placeholder", + "showIf": { + "field": "statProvider", + "equals": "beszel" + } + }, + { + "key": "beszelPass", + "type": "secret", + "label": "beszelPass.label", + "showIf": { + "field": "statProvider", + "equals": "beszel" + } + }, + { + "key": "beszelSystem", + "type": "select", + "label": "beszelSystem.label", + "optionsFrom": "systems", + "hint": "beszelSystem.hint", + "showIf": { + "field": "statProvider", + "equals": "beszel" + } + }, { "key": "slots", "type": "group", @@ -220,6 +264,87 @@ "equals": "glances" } }, + { + "key": "slots", + "type": "group", + "label": "slots.label", + "min": 3, + "max": 3, + "fields": [ + { + "key": "type", + "type": "select", + "label": "slots.type.label", + "default": "cpu", + "options": [ + { + "value": "cpu", + "label": "slots.type.opt.cpu" + }, + { + "value": "ram", + "label": "slots.type.opt.ram" + }, + { + "value": "temp", + "label": "slots.type.opt.temp" + }, + { + "value": "disk", + "label": "slots.type.opt.disk" + }, + { + "value": "iowait", + "label": "slots.type.opt.iowait" + } + ] + }, + { + "key": "primary", + "type": "text", + "label": "slots.fsPrimary.label", + "default": "/", + "placeholder": "slots.fsPrimary.placeholder", + "showIf": { + "field": "type", + "equals": "disk" + } + }, + { + "key": "secondary", + "type": "text", + "label": "slots.fsSecondary.label", + "placeholder": "slots.fsSecondary.placeholder", + "optional": true, + "showIf": { + "field": "type", + "equals": "disk" + } + }, + { + "key": "sensor", + "type": "select", + "label": "slots.sensor.label", + "optionsFrom": "sensors", + "hint": "slots.sensor.hint", + "showIf": { + "field": "type", + "equals": "temp" + } + }, + { + "key": "color", + "type": "color", + "label": "slots.color.label", + "default": "#0091FF" + } + ], + "showIf": { + "field": "statProvider", + "equals": "beszel" + }, + "hint": "slots.beszelHint" + }, { "key": "network", "type": "object",