// SPDX-License-Identifier: GPL-2.0
// Online Download Update + Rescue Kernel
//
// Purpose: check / download / install plugin, kernel, and firmware updates online.
// Log tail is polled at 1 Hz; the last log line drives a state machine
// (idle → checking → button → installing → done). Terminal log lines contain
// raw HTML button strings which are parsed and rendered as DOM buttons.
// Backend RPC: /usr/share/rpcd/ucode/luci.amlogic (state, check_*, start_*, read_log_tail).
'use strict';
'require view';
'require rpc';
'require ui';
'require dom';
'require poll';
'require view.amlogic.shared as amlogicShared';
// ── RPCs ─────────────────────────────────────────────────────────────────────
// Query system state (firmware / plugin / kernel versions).
const callState = rpc.declare({ object: 'luci.amlogic', method: 'state' });
// Run plugin / kernel / firmware version check and write progress to the log.
const callCheckPlugin = rpc.declare({ object: 'luci.amlogic', method: 'check_plugin', params: ['options'] });
const callCheckKernel = rpc.declare({ object: 'luci.amlogic', method: 'check_kernel', params: ['options'] });
const callCheckFirmware = rpc.declare({ object: 'luci.amlogic', method: 'check_firmware', params: ['options'] });
const callStartPlugin = rpc.declare({ object: 'luci.amlogic', method: 'start_plugin' });
const callStartKernel = rpc.declare({ object: 'luci.amlogic', method: 'start_kernel' });
const callStartUpdate = rpc.declare({ object: 'luci.amlogic', method: 'start_update', params: ['amlogic_update_sel'] });
const callStartRescue = rpc.declare({ object: 'luci.amlogic', method: 'start_rescue' });
// Read the last line (up to 4096 bytes) of the named log file.
const callLogTail = rpc.declare({ object: 'luci.amlogic', method: 'read_log_tail', params: ['name'], expect: { line: '' } });
// ── Log-line HTML button parser ──────────────────────────────────────────────
// Parse an button string from the log; return null for plain-text lines.
// Returned object: { type, param, label } where type identifies the action.
function parseButtonLine(line) {
if (!line || line.indexOf('(.*)$/);
var label = labelMatch ? labelMatch[1].trim() : '';
// Plugin download: b_check_plugin(this, 'download_VERSION')
if (value === 'Download' && onclick.indexOf('b_check_plugin') !== -1) {
var m = onclick.match(/b_check_plugin\s*\(\s*this\s*,\s*'([^']*)'\s*\)/);
return { type: 'plugin-download', param: m ? m[1] : null, label: label };
}
// Firmware download: b_check_firmware(this, 'PARAM')
if (value === 'Download' && onclick.indexOf('b_check_firmware') !== -1) {
var m = onclick.match(/b_check_firmware\s*\(\s*this\s*,\s*'([^']*)'\s*\)/);
return { type: 'firmware-download', param: m ? m[1] : null, label: label };
}
// Kernel download: b_check_kernel(this, 'PARAM')
if (value === 'Download' && onclick.indexOf('b_check_kernel') !== -1) {
var m = onclick.match(/b_check_kernel\s*\(\s*this\s*,\s*'([^']*)'\s*\)/);
return { type: 'kernel-download', param: m ? m[1] : null, label: label };
}
// Firmware install: amlogic_update(this, 'PARAM')
if (value === 'Update' && onclick.indexOf('amlogic_update') !== -1) {
var m = onclick.match(/amlogic_update\s*\(\s*this\s*,\s*'([^']*)'\s*\)/);
return { type: 'firmware-update', param: m ? m[1] : null, label: label };
}
// Kernel install: amlogic_kernel(this)
if (value === 'Update' && onclick.indexOf('amlogic_kernel') !== -1) {
return { type: 'kernel-update', param: null, label: label };
}
// Plugin install: amlogic_plugin(this)
if (value === 'Update' && onclick.indexOf('amlogic_plugin') !== -1) {
return { type: 'plugin-update', param: null, label: label };
}
return null;
}
// ── Per-action state machine ──────────────────────────────────────────────────
// States: 'idle' | 'checking' | 'button' | 'installing' | 'done'
// Action object factory: { name, statusEl, state, subBtn }
function makeAction(name, statusEl) {
return { name: name, statusEl: statusEl, state: 'idle', subBtn: null };
}
// Update the status span with a text string (blue info style).
function showInfo(el, text) {
dom.content(el, E('span', { class: 'amlogic-status-info' }, text));
}
// Update the status span with a success string (green).
function showOk(el, text) {
dom.content(el, E('span', { class: 'amlogic-status-ok' }, text));
}
// Update the status span with an error string (red).
function showErr(el, text) {
dom.content(el, E('span', { class: 'amlogic-status-err' }, text));
}
// Replace statusEl contents with a real DOM button + optional label text.
// onClick is called when the user presses it.
function showSubButton(el, btnLabel, labelText, onClick) {
var btn = E('input', {
type: 'button',
class: 'cbi-button cbi-button-reload',
value: btnLabel,
click: onClick
});
var nodes = [btn];
if (labelText) nodes.push(E('span', { class: 'amlogic-status-info' }, ' ' + labelText));
dom.content(el, nodes);
return btn;
}
// ── Poll handler (called at 1 Hz) ────────────────────────────────────────────
// Inspect the last non-empty log line and advance the action state machine.
function handlePoll(action) {
if (action.state === 'done') return;
return callLogTail(action.name).then(function (raw) {
// Only inspect the last non-empty line to avoid false-positive
// 'Failed'/'error' matches from intermediate opkg/apk output.
var lines = (raw || '').split('\n');
var line = '';
for (var i = lines.length - 1; i >= 0; i--) {
var l = lines[i].trim();
if (l) { line = l; break; }
}
if (action.state === 'idle') {
return;
}
if (action.state === 'button') {
// Waiting for user to click sub-button; don't re-render on every poll.
return;
}
if (action.state === 'installing') {
// Show progress lines; detect terminal keywords.
if (!line) return;
// Skip if line is the same HTML button (stale log from check phase).
if (line.indexOf('