summaryrefslogtreecommitdiffhomepage
path: root/applications/luci-app-attendedsysupgrade/htdocs/luci-static/resources/view/attendedsysupgrade/overview.js
diff options
context:
space:
mode:
authorPaul Spooren <mail@aparcar.org>2022-03-31 16:01:31 +0100
committerPaul Spooren <mail@aparcar.org>2023-05-12 20:19:58 +0200
commitfa9fb2f9552034f7e4229bbb35eb3524e5557185 (patch)
tree2d3a93670ad6193506bbd06dffa720e4c3eb3d52 /applications/luci-app-attendedsysupgrade/htdocs/luci-static/resources/view/attendedsysupgrade/overview.js
parenta97ff279ceb7e15c3278a1db7e062ba35352c1f6 (diff)
luci-app-attendedsysupgrade: introduce rebuilders
This adds automatic verification builds to shift trust on multiple server and multiple entities. Signed-off-by: Paul Spooren <mail@aparcar.org>
Diffstat (limited to 'applications/luci-app-attendedsysupgrade/htdocs/luci-static/resources/view/attendedsysupgrade/overview.js')
-rw-r--r--applications/luci-app-attendedsysupgrade/htdocs/luci-static/resources/view/attendedsysupgrade/overview.js586
1 files changed, 388 insertions, 198 deletions
diff --git a/applications/luci-app-attendedsysupgrade/htdocs/luci-static/resources/view/attendedsysupgrade/overview.js b/applications/luci-app-attendedsysupgrade/htdocs/luci-static/resources/view/attendedsysupgrade/overview.js
index 55ed0509e1..76e504086e 100644
--- a/applications/luci-app-attendedsysupgrade/htdocs/luci-static/resources/view/attendedsysupgrade/overview.js
+++ b/applications/luci-app-attendedsysupgrade/htdocs/luci-static/resources/view/attendedsysupgrade/overview.js
@@ -9,17 +9,17 @@
'require dom';
'require fs';
-var callPackagelist = rpc.declare({
+let callPackagelist = rpc.declare({
object: 'rpc-sys',
method: 'packagelist',
});
-var callSystemBoard = rpc.declare({
+let callSystemBoard = rpc.declare({
object: 'system',
method: 'board',
});
-var callUpgradeStart = rpc.declare({
+let callUpgradeStart = rpc.declare({
object: 'rpc-sys',
method: 'upgrade_start',
params: ['keep'],
@@ -28,14 +28,14 @@ var callUpgradeStart = rpc.declare({
/**
* Returns the branch of a given version. This helps to offer upgrades
* for point releases (aka within the branch).
- *
+ *
* Logic:
* SNAPSHOT -> SNAPSHOT
* 21.02-SNAPSHOT -> 21.02
* 21.02.0-rc1 -> 21.02
* 19.07.8 -> 19.07
- *
- * @param {string} version
+ *
+ * @param {string} version
* Input version from which to determine the branch
* @returns {string}
* The determined branch
@@ -45,10 +45,10 @@ function get_branch(version) {
}
/**
- * The OpenWrt revision string contains both a hash as well as the number
- * commits since the OpenWrt/LEDE reboot. It helps to determine if a
+ * The OpenWrt revision string contains both a hash as well as the number
+ * commits since the OpenWrt/LEDE reboot. It helps to determine if a
* snapshot is newer than another.
- *
+ *
* @param {string} revision
* Revision string of a OpenWrt device
* @returns {integer}
@@ -60,17 +60,19 @@ function get_revision_count(revision) {
return view.extend({
steps: {
- init: _('10% Received build request'),
- download_imagebuilder: _('20% Downloading ImageBuilder archive'),
- unpack_imagebuilder: _('40% Setup ImageBuilder'),
- calculate_packages_hash: _('60% Validate package selection'),
- building_image: _('80% Generating firmware image')
+ init: [10, _('Received build request')],
+ download_imagebuilder: [20, _('Downloading ImageBuilder archive')],
+ unpack_imagebuilder: [40, _('Setup ImageBuilder')],
+ calculate_packages_hash: [60, _('Validate package selection')],
+ building_image: [80, _('Generating firmware image')],
},
data: {
url: '',
revision: '',
advanced_mode: 0,
+ rebuilder: [],
+ sha256_unsigned: '',
},
firmware: {
@@ -82,74 +84,107 @@ return view.extend({
filesystem: '',
},
- handle200: function (response) {
- response = response.json();
- var image;
- for (image of response.images) {
+ selectImage: function (images) {
+ let image;
+ for (image of images) {
if (this.firmware.filesystem == image.filesystem) {
if (this.data.efi) {
if (image.type == 'combined-efi') {
- break;
+ return image;
}
} else {
if (image.type == 'sysupgrade' || image.type == 'combined') {
- break;
+ return image;
}
}
}
}
+ return null;
+ },
+
+ handle200: function (response) {
+ response = response.json();
+ let image = this.selectImage(response.images);
if (image.name != undefined) {
- var sysupgrade_url = `${this.data.url}/store/${response.bin_dir}/${image.name}`;
+ this.data.sha256_unsigned = image.sha256_unsigned;
+ let sysupgrade_url = `${this.data.url}/store/${response.bin_dir}/${image.name}`;
- var keep = E('input', { type: 'checkbox' });
+ let keep = E('input', { type: 'checkbox' });
keep.checked = true;
- var fields = [
- _('Version'), `${response.version_number} ${response.version_code}`,
- _('SHA256'), image.sha256,
+ let fields = [
+ _('Version'),
+ `${response.version_number} ${response.version_code}`,
+ _('SHA256'),
+ image.sha256,
];
if (this.data.advanced_mode == 1) {
fields.push(
- _('Profile'), response.id,
- _('Target'), response.target,
- _('Build Date'), response.build_at,
- _('Filename'), image.name,
- _('Filesystem'), image.filesystem,
- )
+ _('Profile'),
+ response.id,
+ _('Target'),
+ response.target,
+ _('Build Date'),
+ response.build_at,
+ _('Filename'),
+ image.name,
+ _('Filesystem'),
+ image.filesystem
+ );
}
- fields.push('', E('a', { href: sysupgrade_url }, _('Download firmware image')))
+ fields.push(
+ '',
+ E('a', { href: sysupgrade_url }, _('Download firmware image'))
+ );
+ if (this.data.rebuilder) {
+ fields.push(_('Rebuilds'), E('div', { id: 'rebuilder_status' }));
+ }
- var table = E('div', { class: 'table' });
+ let table = E('div', { class: 'table' });
- for (var i = 0; i < fields.length; i += 2) {
- table.appendChild(E('tr', { class: 'tr' }, [
- E('td', { class: 'td left', width: '33%' }, [fields[i]]),
- E('td', { class: 'td left' }, [fields[i + 1]]),
- ]));
+ for (let i = 0; i < fields.length; i += 2) {
+ table.appendChild(
+ E('tr', { class: 'tr' }, [
+ E('td', { class: 'td left', width: '33%' }, [fields[i]]),
+ E('td', { class: 'td left' }, [fields[i + 1]]),
+ ])
+ );
}
- var modal_body = [
+ let modal_body = [
table,
- E('p', { class: 'mt-2' },
+ E(
+ 'p',
+ { class: 'mt-2' },
E('label', { class: 'btn' }, [
- keep, ' ',
- _('Keep settings and retain the current configuration')
- ])),
+ keep,
+ ' ',
+ _('Keep settings and retain the current configuration'),
+ ])
+ ),
E('div', { class: 'right' }, [
- E('div', { class: 'btn', click: ui.hideModal }, _('Cancel')), ' ',
- E('button', {
- 'class': 'btn cbi-button cbi-button-positive important',
- 'click': ui.createHandlerFn(this, function () {
- this.handleInstall(sysupgrade_url, keep.checked, image.sha256)
- })
- }, _('Install firmware image')),
+ E('div', { class: 'btn', click: ui.hideModal }, _('Cancel')),
+ ' ',
+ E(
+ 'button',
+ {
+ class: 'btn cbi-button cbi-button-positive important',
+ click: ui.createHandlerFn(this, function () {
+ this.handleInstall(sysupgrade_url, keep.checked, image.sha256);
+ }),
+ },
+ _('Install firmware image')
+ ),
]),
];
ui.showModal(_('Successfully created firmware image'), modal_body);
+ if (this.data.rebuilder) {
+ this.handleRebuilder();
+ }
}
},
@@ -159,20 +194,37 @@ return view.extend({
if ('queue_position' in response) {
ui.showModal(_('Queued...'), [
- E('p', { 'class': 'spinning' }, _('Request in build queue position %s').format(response.queue_position))
+ E(
+ 'p',
+ { class: 'spinning' },
+ _('Request in build queue position %s').format(
+ response.queue_position
+ )
+ ),
]);
} else {
ui.showModal(_('Building Firmware...'), [
- E('p', { 'class': 'spinning' }, _('Progress: %s').format(this.steps[response.imagebuilder_status]))
+ E(
+ 'p',
+ { class: 'spinning' },
+ _('Progress: %s%% %s').format(
+ this.steps[response.imagebuilder_status][0],
+ this.steps[response.imagebuilder_status][1]
+ )
+ ),
]);
}
},
handleError: function (response) {
response = response.json();
- var body = [
+ let body = [
E('p', {}, _('Server response: %s').format(response.detail)),
- E('a', { href: 'https://github.com/openwrt/asu/issues' }, _('Please report the error message and request')),
+ E(
+ 'a',
+ { href: 'https://github.com/openwrt/asu/issues' },
+ _('Please report the error message and request')
+ ),
E('p', {}, _('Request Data:')),
E('pre', {}, JSON.stringify({ ...this.data, ...this.firmware }, null, 4)),
];
@@ -196,61 +248,118 @@ return view.extend({
ui.showModal(_('Error building the firmware image'), body);
},
- handleRequest: function () {
- var request_url = `${this.data.url}/api/v1/build`;
- var method = "POST"
- var content = this.firmware;
+ handleRequest: function (server, main) {
+ let request_url = `${server}/api/v1/build`;
+ let method = 'POST';
+ let content = this.firmware;
/**
- * If `request_hash` is available use a GET request instead of
+ * If `request_hash` is available use a GET request instead of
* sending the entire object.
*/
- if (this.data.request_hash) {
+ if (this.data.request_hash && main == true) {
request_url += `/${this.data.request_hash}`;
content = {};
- method = "GET"
+ method = 'GET';
}
- request.request(request_url, { method: method, content: content })
+ request
+ .request(request_url, { method: method, content: content })
.then((response) => {
switch (response.status) {
case 202:
- this.handle202(response);
+ if (main) {
+ this.handle202(response);
+ } else {
+ response = response.json();
+
+ let view = document.getElementById(server);
+ view.innerText = `⏳ (${
+ this.steps[response.imagebuilder_status][0]
+ }%) ${server}`;
+ }
break;
case 200:
- poll.stop();
- this.handle200(response);
+ if (main == true) {
+ poll.remove(this.pollFn);
+ this.handle200(response);
+ } else {
+ poll.remove(this.rebuilder_polls[server]);
+ response = response.json();
+ let view = document.getElementById(server);
+ let image = this.selectImage(response.images);
+ if (image.sha256_unsigned == this.data.sha256_unsigned) {
+ view.innerText = '✅ %s'.format(server);
+ } else {
+ view.innerHTML = `⚠️ ${server} (<a href="${server}/store/${
+ response.bin_dir
+ }/${image.name}">${_('Download')}</a>)`;
+ }
+ }
break;
case 400: // bad request
case 422: // bad package
case 500: // build failed
- poll.stop();
- this.handleError(response);
- break;
+ if (main == true) {
+ poll.remove(this.pollFn);
+ this.handleError(response);
+ break;
+ } else {
+ poll.remove(this.rebuilder_polls[server]);
+ document.getElementById(server).innerText = '🚫 %s'.format(
+ server
+ );
+ }
}
});
},
+ handleRebuilder: function () {
+ this.rebuilder_polls = {};
+ for (let rebuilder of this.data.rebuilder) {
+ this.rebuilder_polls[rebuilder] = L.bind(
+ this.handleRequest,
+ this,
+ rebuilder,
+ false
+ );
+ poll.add(this.rebuilder_polls[rebuilder], 5);
+ document.getElementById(
+ 'rebuilder_status'
+ ).innerHTML += `<p id="${rebuilder}">⏳ ${rebuilder}</p>`;
+ }
+ poll.start();
+ },
+
handleInstall: function (url, keep, sha256) {
ui.showModal(_('Downloading...'), [
- E('p', { 'class': 'spinning' }, _('Downloading firmware from server to browser'))
+ E(
+ 'p',
+ { class: 'spinning' },
+ _('Downloading firmware from server to browser')
+ ),
]);
- request.get(url, {
- headers: {
- 'Content-Type': 'application/x-www-form-urlencoded',
- },
- responseType: 'blob',
- })
+ request
+ .get(url, {
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ },
+ responseType: 'blob',
+ })
.then((response) => {
- var form_data = new FormData();
+ let form_data = new FormData();
form_data.append('sessionid', rpc.getSessionID());
form_data.append('filename', '/tmp/firmware.bin');
form_data.append('filemode', 600);
form_data.append('filedata', response.blob());
ui.showModal(_('Uploading...'), [
- E('p', { 'class': 'spinning' }, _('Uploading firmware from browser to device'))
+ E(
+ 'p',
+ { class: 'spinning' },
+ _('Uploading firmware from browser to device')
+ ),
]);
request
@@ -261,164 +370,219 @@ return view.extend({
.then((response) => response.json())
.then((response) => {
if (response.sha256sum != sha256) {
-
ui.showModal(_('Wrong checksum'), [
- E('p', _('Error during download of firmware. Please try again')),
- E('div', { class: 'btn', click: ui.hideModal }, _('Close'))
+ E(
+ 'p',
+ _('Error during download of firmware. Please try again')
+ ),
+ E('div', { class: 'btn', click: ui.hideModal }, _('Close')),
]);
} else {
ui.showModal(_('Installing...'), [
- E('p', { class: 'spinning' }, _('Installing the sysupgrade. Do not unpower device!'))
+ E(
+ 'p',
+ { class: 'spinning' },
+ _('Installing the sysupgrade. Do not unpower device!')
+ ),
]);
- L.resolveDefault(callUpgradeStart(keep), {})
- .then((response) => {
- if (keep) {
- ui.awaitReconnect(window.location.host);
- } else {
- ui.awaitReconnect('192.168.1.1', 'openwrt.lan');
- }
- });
+ L.resolveDefault(callUpgradeStart(keep), {}).then((response) => {
+ if (keep) {
+ ui.awaitReconnect(window.location.host);
+ } else {
+ ui.awaitReconnect('192.168.1.1', 'openwrt.lan');
+ }
+ });
}
});
});
},
handleCheck: function () {
- var { url, revision } = this.data
- var { version, target } = this.firmware
- var candidates = [];
- var response;
- var request_url = `${url}/api/overview`;
+ let { url, revision } = this.data;
+ let { version, target } = this.firmware;
+ let candidates = [];
+ let request_url = `${url}/api/overview`;
if (version.endsWith('SNAPSHOT')) {
request_url = `${url}/api/v1/revision/${version}/${target}`;
}
ui.showModal(_('Searching...'), [
- E('p', { 'class': 'spinning' },
- _('Searching for an available sysupgrade of %s - %s').format(version, revision))
+ E(
+ 'p',
+ { class: 'spinning' },
+ _('Searching for an available sysupgrade of %s - %s').format(
+ version,
+ revision
+ )
+ ),
]);
- L.resolveDefault(request.get(request_url))
- .then(response => {
- if (!response.ok) {
- ui.showModal(_('Error connecting to upgrade server'), [
- E('p', {}, _('Could not reach API at "%s". Please try again later.').format(response.url)),
- E('pre', {}, response.responseText),
- E('div', { class: 'right' }, [
- E('div', { class: 'btn', click: ui.hideModal }, _('Close'))
- ]),
- ]);
- return;
+ L.resolveDefault(request.get(request_url)).then((response) => {
+ if (!response.ok) {
+ ui.showModal(_('Error connecting to upgrade server'), [
+ E(
+ 'p',
+ {},
+ _('Could not reach API at "%s". Please try again later.').format(
+ response.url
+ )
+ ),
+ E('pre', {}, response.responseText),
+ E('div', { class: 'right' }, [
+ E('div', { class: 'btn', click: ui.hideModal }, _('Close')),
+ ]),
+ ]);
+ return;
+ }
+ if (version.endsWith('SNAPSHOT')) {
+ const remote_revision = response.json().revision;
+ if (
+ get_revision_count(revision) < get_revision_count(remote_revision)
+ ) {
+ candidates.push([version, remote_revision]);
}
- if (version.endsWith('SNAPSHOT')) {
- const remote_revision = response.json().revision;
- if (get_revision_count(revision) < get_revision_count(remote_revision)) {
- candidates.push([version, remote_revision]);
- }
- } else {
- const latest = response.json().latest;
+ } else {
+ const latest = response.json().latest;
- for (let remote_version of latest) {
- var remote_branch = get_branch(remote_version);
+ for (let remote_version of latest) {
+ let remote_branch = get_branch(remote_version);
- // already latest version installed
- if (version == remote_version) {
- break;
- }
+ // already latest version installed
+ if (version == remote_version) {
+ break;
+ }
- // skip branch upgrades outside the advanced mode
- if (this.data.branch != remote_branch && this.data.advanced_mode == 0) {
- continue;
- }
+ // skip branch upgrades outside the advanced mode
+ if (
+ this.data.branch != remote_branch &&
+ this.data.advanced_mode == 0
+ ) {
+ continue;
+ }
- candidates.unshift([remote_version, null]);
+ candidates.unshift([remote_version, null]);
- // don't offer branches older than the current
- if (this.data.branch == remote_branch) {
- break;
- }
+ // don't offer branches older than the current
+ if (this.data.branch == remote_branch) {
+ break;
}
}
+ }
- // allow to re-install running firmware in advanced mode
- if (this.data.advanced_mode == 1) {
- candidates.unshift([version, revision])
- }
-
- if (candidates.length) {
- var m, s, o;
-
- var mapdata = {
- request: {
- profile: this.firmware.profile,
- version: candidates[0][0],
- packages: Object.keys(this.firmware.packages).sort(),
- },
- };
-
- var map = new form.JSONMap(mapdata, '');
+ // allow to re-install running firmware in advanced mode
+ if (this.data.advanced_mode == 1) {
+ candidates.unshift([version, revision]);
+ }
- s = map.section(form.NamedSection, 'request', '', '', 'Use defaults for the safest update');
- o = s.option(form.ListValue, 'version', 'Select firmware version');
- for (let candidate of candidates) {
- if (candidate[0] == version && candidate[1] == revision) {
- o.value(candidate[0], _('[installed] %s')
- .format(candidate[1] ? `${candidate[0]} - ${candidate[1]}` : candidate[0]));
- } else {
- o.value(candidate[0], candidate[1] ? `${candidate[0]} - ${candidate[1]}` : candidate[0]);
- }
+ if (candidates.length) {
+ let s, o;
+
+ let mapdata = {
+ request: {
+ profile: this.firmware.profile,
+ version: candidates[0][0],
+ packages: Object.keys(this.firmware.packages).sort(),
+ },
+ };
+
+ let map = new form.JSONMap(mapdata, '');
+
+ s = map.section(
+ form.NamedSection,
+ 'request',
+ '',
+ '',
+ 'Use defaults for the safest update'
+ );
+ o = s.option(form.ListValue, 'version', 'Select firmware version');
+ for (let candidate of candidates) {
+ if (candidate[0] == version && candidate[1] == revision) {
+ o.value(
+ candidate[0],
+ _('[installed] %s').format(
+ candidate[1]
+ ? `${candidate[0]} - ${candidate[1]}`
+ : candidate[0]
+ )
+ );
+ } else {
+ o.value(
+ candidate[0],
+ candidate[1] ? `${candidate[0]} - ${candidate[1]}` : candidate[0]
+ );
}
+ }
- if (this.data.advanced_mode == 1) {
- o = s.option(form.Value, 'profile', _('Board Name / Profile'));
- o = s.option(form.DynamicList, 'packages', _('Packages'));
- }
+ if (this.data.advanced_mode == 1) {
+ o = s.option(form.Value, 'profile', _('Board Name / Profile'));
+ o = s.option(form.DynamicList, 'packages', _('Packages'));
+ }
- L.resolveDefault(map.render()).
- then(form_rendered => {
- ui.showModal(_('New firmware upgrade available'), [
- E('p', _('Currently running: %s - %s').format(this.firmware.version, this.data.revision)),
- form_rendered,
- E('div', { class: 'right' }, [
- E('div', { class: 'btn', click: ui.hideModal }, _('Cancel')), ' ',
- E('button', {
- 'class': 'btn cbi-button cbi-button-positive important',
- 'click': ui.createHandlerFn(this, function () {
- map.save().then(() => {
- this.firmware.packages = mapdata.request.packages;
- this.firmware.version = mapdata.request.version;
- this.firmware.profile = mapdata.request.profile;
- poll.add(L.bind(this.handleRequest, this), 5);
- });
- })
- }, _('Request firmware image')),
- ]),
- ]);
- });
- } else {
- ui.showModal(_('No upgrade available'), [
- E('p', _('The device runs the latest firmware version %s - %s').format(version, revision)),
+ L.resolveDefault(map.render()).then((form_rendered) => {
+ ui.showModal(_('New firmware upgrade available'), [
+ E(
+ 'p',
+ _('Currently running: %s - %s').format(
+ this.firmware.version,
+ this.data.revision
+ )
+ ),
+ form_rendered,
E('div', { class: 'right' }, [
- E('div', { class: 'btn', click: ui.hideModal }, _('Close')),
+ E('div', { class: 'btn', click: ui.hideModal }, _('Cancel')),
+ ' ',
+ E(
+ 'button',
+ {
+ class: 'btn cbi-button cbi-button-positive important',
+ click: ui.createHandlerFn(this, function () {
+ map.save().then(() => {
+ this.firmware.packages = mapdata.request.packages;
+ this.firmware.version = mapdata.request.version;
+ this.firmware.profile = mapdata.request.profile;
+ this.pollFn = L.bind(function () {
+ this.handleRequest(this.data.url, true);
+ }, this);
+ poll.add(this.pollFn, 5);
+ poll.start();
+ });
+ }),
+ },
+ _('Request firmware image')
+ ),
]),
]);
- }
-
- });
+ });
+ } else {
+ ui.showModal(_('No upgrade available'), [
+ E(
+ 'p',
+ _('The device runs the latest firmware version %s - %s').format(
+ version,
+ revision
+ )
+ ),
+ E('div', { class: 'right' }, [
+ E('div', { class: 'btn', click: ui.hideModal }, _('Close')),
+ ]),
+ ]);
+ }
+ });
},
load: function () {
return Promise.all([
L.resolveDefault(callPackagelist(), {}),
L.resolveDefault(callSystemBoard(), {}),
- L.resolveDefault(fs.stat("/sys/firmware/efi"), null),
+ L.resolveDefault(fs.stat('/sys/firmware/efi'), null),
uci.load('attendedsysupgrade'),
]);
},
render: function (response) {
- this.firmware.client = 'luci/' + response[0].packages['luci-app-attendedsysupgrade'];
+ this.firmware.client =
+ 'luci/' + response[0].packages['luci-app-attendedsysupgrade'];
this.firmware.packages = response[0].packages;
this.firmware.profile = response[1].board_name;
@@ -431,20 +595,46 @@ return view.extend({
this.data.efi = response[2];
this.data.url = uci.get_first('attendedsysupgrade', 'server', 'url');
- this.data.advanced_mode = uci.get_first('attendedsysupgrade', 'client', 'advanced_mode') || 0
+ this.data.advanced_mode =
+ uci.get_first('attendedsysupgrade', 'client', 'advanced_mode') || 0;
+ this.data.rebuilder = uci.get_first(
+ 'attendedsysupgrade',
+ 'server',
+ 'rebuilder'
+ );
return E('p', [
E('h2', _('Attended Sysupgrade')),
- E('p', _('The attended sysupgrade service allows to easily upgrade vanilla and custom firmware images.')),
- E('p', _('This is done by building a new firmware on demand via an online service.')),
- E('p', _('Currently running: %s - %s').format(this.firmware.version, this.data.revision)),
- E('button', {
- 'class': 'btn cbi-button cbi-button-positive important',
- 'click': ui.createHandlerFn(this, this.handleCheck)
- }, _('Search for firmware upgrade'))
+ E(
+ 'p',
+ _(
+ 'The attended sysupgrade service allows to easily upgrade vanilla and custom firmware images.'
+ )
+ ),
+ E(
+ 'p',
+ _(
+ 'This is done by building a new firmware on demand via an online service.'
+ )
+ ),
+ E(
+ 'p',
+ _('Currently running: %s - %s').format(
+ this.firmware.version,
+ this.data.revision
+ )
+ ),
+ E(
+ 'button',
+ {
+ class: 'btn cbi-button cbi-button-positive important',
+ click: ui.createHandlerFn(this, this.handleCheck),
+ },
+ _('Search for firmware upgrade')
+ ),
]);
},
handleSaveApply: null,
handleSave: null,
- handleReset: null
+ handleReset: null,
});