init commit
Build / build (push) Failing after 1m50s

This commit is contained in:
Rene Kievits
2026-09-05 22:27:44 +02:00
commit b137bf9eda
21 changed files with 8371 additions and 0 deletions
+46
View File
@@ -0,0 +1,46 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=1920, initial-scale=1" />
<title>__TITLE__</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="screen">
<header class="header">
<div>
<h1>__TITLE__</h1>
<p class="subtitle">Stops the LG firmware update popup on rooted webOS TVs</p>
</div>
<div class="badges">
<span id="badge-root" class="badge badge-unknown">checking root…</span>
<span id="badge-version" class="badge">v__VERSION__</span>
</div>
</header>
<div class="columns">
<main class="panel">
<h2>Protection layers</h2>
<div id="toggles" class="toggles"></div>
<h2>Actions</h2>
<div id="actions" class="actions"></div>
</main>
<aside class="panel">
<h2>Status</h2>
<dl id="status" class="status"></dl>
<h2>Log</h2>
<pre id="log" class="log"></pre>
</aside>
</div>
<footer class="footer">
<span>▲▼ move · OK select · BACK exit</span>
<span id="hint"></span>
</footer>
</div>
<script src="index.js"></script>
</body>
</html>
+407
View File
@@ -0,0 +1,407 @@
/**
* LG Update Blocker - frontend.
*
* No frameworks and no build step: Luna calls go straight through
* PalmServiceBridge, which every webOS webapp gets injected for free.
*/
(function () {
'use strict';
var SERVICE = 'luna://__SERVICE_ID__';
var HBCHANNEL = 'luna://org.webosbrew.hbchannel.service';
var HBCHANNEL_ELEVATE =
'/media/developer/apps/usr/palm/services/org.webosbrew.hbchannel.service/elevate-service';
var TOGGLES = [
{
key: 'blockHosts',
title: 'Block LG update servers',
desc: 'Points snu/su/nsu.lge.com at 127.0.0.1 in /etc/hosts, re-applied on every boot'
},
{
key: 'purgeCache',
title: 'Delete staged firmware',
desc: 'Wipes an already downloaded update - the usual reason the popup keeps returning'
},
{
key: 'lockCache',
title: 'Lock the staging folder',
desc: 'Mounts an empty read-only folder over it so no update can be staged again'
},
{
key: 'disableSettings',
title: 'Turn off auto-update settings',
desc: 'Switches off every update-related key com.webos.settingsservice exposes'
},
{
key: 'stopServices',
title: 'Stop update services at boot',
desc: 'Advanced: stops the update upstart jobs found on this TV after every boot'
}
];
var ACTIONS = [
{ key: 'apply', label: 'Apply protection', primary: true },
{ key: 'purge', label: 'Purge staged update' },
{ key: 'revert', label: 'Remove protection' },
{ key: 'refresh', label: 'Refresh status' },
{ key: 'diagnostics', label: 'Diagnostics' }
];
var config = {
blockHosts: true,
purgeCache: true,
lockCache: false,
disableSettings: true,
stopServices: false
};
var focusables = [];
var focusIndex = 0;
var busy = false;
var bridges = [];
/* ----------------------------------------------------------- luna calls */
function luna(uri, params, onSuccess, onFailure) {
if (typeof PalmServiceBridge === 'undefined') {
if (onFailure) onFailure({ returnValue: false, errorText: 'no Luna bus (not running on a TV)' });
return;
}
var bridge = new PalmServiceBridge();
bridges.push(bridge);
bridge.onservicecallback = function (raw) {
var index = bridges.indexOf(bridge);
if (index !== -1) bridges.splice(index, 1);
var response;
try {
response = JSON.parse(raw);
} catch (err) {
response = { returnValue: false, errorText: 'malformed response: ' + raw };
}
if (response.returnValue === false || response.errorText) {
if (onFailure) onFailure(response);
} else if (onSuccess) {
onSuccess(response);
}
};
bridge.call(uri, JSON.stringify(params || {}));
}
/* ------------------------------------------------------------------- ui */
function el(id) {
return document.getElementById(id);
}
function log(text) {
var pane = el('log');
var stamp = new Date().toTimeString().slice(0, 8);
pane.textContent += '[' + stamp + '] ' + text + '\n';
pane.scrollTop = pane.scrollHeight;
}
function logLines(lines) {
(lines || []).forEach(function (line) {
log(line);
});
}
function setHint(text, isBusy) {
var hint = el('hint');
hint.textContent = text || '';
hint.className = isBusy ? 'busy' : '';
}
function setBusy(state, text) {
busy = state;
setHint(text || (state ? 'working…' : ''), state);
}
function bytes(value) {
if (!value) return '0 B';
var units = ['B', 'kB', 'MB', 'GB'];
var index = 0;
var size = value;
while (size >= 1024 && index < units.length - 1) {
size /= 1024;
index += 1;
}
return (index === 0 ? size : size.toFixed(1)) + ' ' + units[index];
}
function buildUi() {
var toggles = el('toggles');
TOGGLES.forEach(function (toggle) {
var row = document.createElement('div');
row.className = 'row';
row.innerHTML =
'<div class="text"><div class="title"></div><div class="desc"></div></div>' +
'<div class="state"></div>';
row.querySelector('.title').textContent = toggle.title;
row.querySelector('.desc').textContent = toggle.desc;
row.dataset.toggle = toggle.key;
row.onActivate = function () {
config[toggle.key] = !config[toggle.key];
renderToggles();
log((config[toggle.key] ? 'enabled: ' : 'disabled: ') + toggle.title +
' (press Apply protection to write it to the TV)');
};
toggles.appendChild(row);
focusables.push(row);
});
var actions = el('actions');
ACTIONS.forEach(function (action) {
var button = document.createElement('div');
button.className = 'button' + (action.primary ? ' primary' : '');
button.textContent = action.label;
button.onActivate = function () {
runAction(action.key);
};
actions.appendChild(button);
focusables.push(button);
});
renderToggles();
renderFocus();
}
function renderToggles() {
TOGGLES.forEach(function (toggle, index) {
var row = focusables[index];
var on = !!config[toggle.key];
row.className = 'row' + (on ? ' on' : '') + (index === focusIndex ? ' focused' : '');
row.querySelector('.state').textContent = on ? 'ON' : 'OFF';
});
}
function renderFocus() {
focusables.forEach(function (node, index) {
var focused = index === focusIndex;
if (focused) {
if (node.className.indexOf('focused') === -1) node.className += ' focused';
if (node.scrollIntoView) node.scrollIntoView(false);
} else {
node.className = node.className.replace(/\s*focused/g, '');
}
});
}
function move(delta) {
focusIndex = (focusIndex + delta + focusables.length) % focusables.length;
renderFocus();
}
function badge(id, text, kind) {
var node = el(id);
node.textContent = text;
node.className = 'badge' + (kind ? ' badge-' + kind : '');
}
function renderStatus(status) {
var list = el('status');
list.innerHTML = '';
function row(label, value) {
var wrapper = document.createElement('div');
var dt = document.createElement('dt');
var dd = document.createElement('dd');
dt.textContent = label;
dd.textContent = value;
wrapper.appendChild(dt);
wrapper.appendChild(dd);
list.appendChild(wrapper);
}
var staged = 0;
var stagedDirs = 0;
var locked = 0;
(status.cache || []).forEach(function (entry) {
stagedDirs += 1;
staged += entry.bytes || 0;
if (entry.locked) locked += 1;
});
row('Service user', status.root ? 'root' : 'uid ' + status.uid + ' (not elevated)');
row('Hosts entries', status.hosts.blockedDomains + ' / ' + status.hosts.totalDomains +
(status.hosts.bindMounted ? ' (bind-mounted)' : ''));
row('Staging folders', stagedDirs ? stagedDirs + ' found' : 'none on this TV');
row('Staged firmware', stagedDirs ? bytes(staged) : '-');
row('Staging locked', stagedDirs ? locked + ' / ' + stagedDirs : '-');
row('Update jobs found', (status.jobs || []).length ? status.jobs.join(', ') : 'none');
row('Boot hook', status.bootHook.installed ? 'installed' : 'not installed');
var settings = status.settings || [];
if (settings.length) {
settings.forEach(function (entry) {
row(entry.category + '/' + entry.key, JSON.stringify(entry.value));
});
} else {
row('Update settings', 'none exposed');
}
badge('badge-root', status.root ? 'root' : 'no root', status.root ? 'ok' : 'bad');
badge('badge-version', 'v' + status.version);
if (status.config) {
Object.keys(config).forEach(function (key) {
if (typeof status.config[key] === 'boolean') config[key] = status.config[key];
});
renderToggles();
}
}
/* -------------------------------------------------------------- actions */
function refresh(onDone) {
luna(SERVICE + '/status', {}, function (status) {
renderStatus(status);
if (onDone) onDone(status);
}, function (err) {
log('status failed: ' + (err.errorText || JSON.stringify(err)));
badge('badge-root', 'service error', 'bad');
if (onDone) onDone(null);
});
}
function runAction(key) {
if (busy) return;
if (key === 'refresh') {
setBusy(true, 'refreshing…');
refresh(function () {
setBusy(false);
log('status refreshed');
});
return;
}
if (key === 'diagnostics') {
setBusy(true, 'collecting diagnostics…');
luna(SERVICE + '/diagnostics', {}, function (res) {
setBusy(false);
log('--- diagnostics ---');
log('os: ' + JSON.stringify(res.osInfo));
log('mounts: ' + JSON.stringify(res.mounts));
log('settings: ' + JSON.stringify(res.settings));
log('hosts (tail):\n' + res.hostsFile);
log('boot hook log (tail):\n' + res.bootLog);
log('--- end ---');
}, function (err) {
setBusy(false);
log('diagnostics failed: ' + (err.errorText || JSON.stringify(err)));
});
return;
}
var uri = SERVICE + '/' + key;
var params = key === 'apply' ? config : {};
setBusy(true, key === 'revert' ? 'removing…' : 'applying…');
luna(uri, params, function (res) {
setBusy(false);
logLines(res.log);
if (res.status) renderStatus(res.status);
else refresh();
}, function (err) {
setBusy(false);
logLines(err.log);
log('failed: ' + (err.errorText || JSON.stringify(err)));
});
}
/* ------------------------------------------------------------ elevation */
function elevate(onDone) {
log('service is not running as root, asking the Homebrew Channel to elevate it…');
function afterElevation() {
// The running instance keeps its old (unprivileged) process; quitting it
// makes the bus start a fresh, elevated one on the next call.
luna(SERVICE + '/quit', {}, function () {
window.setTimeout(recheck, 2500);
}, function () {
window.setTimeout(recheck, 2500);
});
}
function recheck() {
refresh(function (status) {
if (status && status.root) {
log('service elevated - ready');
} else {
log('still not elevated. Open the Homebrew Channel, make sure "Root status" says ' +
'"ok", then relaunch this app.');
}
setBusy(false);
if (onDone) onDone();
});
}
luna(HBCHANNEL + '/elevateService', { id: '__SERVICE_ID__' }, function () {
log('elevateService succeeded, restarting our service…');
afterElevation();
}, function (err) {
log('elevateService unavailable (' + (err.errorText || 'unknown') + '), trying exec fallback…');
luna(HBCHANNEL + '/exec', { command: HBCHANNEL_ELEVATE + ' __SERVICE_ID__' }, function () {
log('elevate-service executed, restarting our service…');
afterElevation();
}, function (execErr) {
log('could not elevate: ' + (execErr.errorText || JSON.stringify(execErr)));
log('Is the Homebrew Channel installed and rooted? Its service must be elevated first.');
setBusy(false);
if (onDone) onDone();
});
});
}
/* ---------------------------------------------------------------- input */
document.addEventListener('keydown', function (event) {
switch (event.keyCode) {
case 38: // up
move(-1);
break;
case 40: // down
move(1);
break;
case 37: // left
move(-1);
break;
case 39: // right
move(1);
break;
case 13: // OK
case 32: // space (keyboard/dev)
if (!busy && focusables[focusIndex] && focusables[focusIndex].onActivate) {
focusables[focusIndex].onActivate();
}
break;
case 461: // back
case 27: // esc
window.close();
break;
default:
return;
}
event.preventDefault();
});
/* ----------------------------------------------------------------- boot */
buildUi();
log('LG Update Blocker v__VERSION__');
setBusy(true, 'checking service…');
refresh(function (status) {
if (!status) {
setBusy(false);
return;
}
if (status.root) {
setBusy(false);
log('service is running as root - ready');
} else {
elevate();
}
});
})();
+234
View File
@@ -0,0 +1,234 @@
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html,
body {
width: 1920px;
height: 1080px;
overflow: hidden;
background: #0d1014;
color: #e6e9ee;
font-family: "LG Smart UI", "Museo Sans", Helvetica, Arial, sans-serif;
font-size: 24px;
}
.screen {
/* TV overscan safe area */
padding: 54px 90px 40px;
height: 100%;
display: flex;
flex-direction: column;
}
.header {
display: flex;
align-items: flex-start;
justify-content: space-between;
padding-bottom: 24px;
border-bottom: 2px solid #232a33;
}
h1 {
font-size: 46px;
font-weight: 600;
letter-spacing: 0.5px;
}
h2 {
font-size: 24px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 2px;
color: #7c8899;
margin: 26px 0 14px;
}
h2:first-child {
margin-top: 0;
}
.subtitle {
color: #8b95a5;
font-size: 22px;
margin-top: 6px;
}
.badges {
display: flex;
gap: 12px;
}
.badge {
padding: 8px 18px;
border-radius: 999px;
background: #1a2029;
color: #9aa5b5;
font-size: 20px;
white-space: nowrap;
}
.badge-ok {
background: #123021;
color: #4ade80;
}
.badge-bad {
background: #35161a;
color: #f87171;
}
.badge-unknown {
background: #2a2413;
color: #fbbf24;
}
.columns {
display: flex;
gap: 40px;
flex: 1;
min-height: 0;
padding-top: 26px;
}
.panel {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
}
.panel:last-child {
max-width: 720px;
}
.toggles,
.actions {
display: flex;
flex-direction: column;
gap: 10px;
}
.actions {
flex-direction: row;
flex-wrap: wrap;
}
.row {
display: flex;
align-items: center;
gap: 20px;
padding: 16px 22px;
border: 3px solid transparent;
border-radius: 12px;
background: #151b23;
}
.row .text {
flex: 1;
min-width: 0;
}
.row .title {
font-size: 25px;
}
.row .desc {
font-size: 19px;
color: #8b95a5;
margin-top: 4px;
}
.row .state {
font-size: 22px;
font-weight: 600;
padding: 6px 16px;
border-radius: 8px;
background: #232b36;
color: #94a3b8;
min-width: 92px;
text-align: center;
}
.row.on .state {
background: #14361f;
color: #4ade80;
}
.button {
padding: 16px 28px;
border: 3px solid transparent;
border-radius: 12px;
background: #1d2530;
font-size: 24px;
}
.button.primary {
background: #7f1d24;
color: #ffe4e6;
}
.focused {
border-color: #f43f5e;
background: #222c38;
}
.button.primary.focused {
background: #b91c2c;
}
.status {
font-size: 21px;
line-height: 1.5;
}
.status div {
display: flex;
justify-content: space-between;
gap: 16px;
padding: 5px 0;
border-bottom: 1px solid #1b2029;
}
.status dt {
color: #8b95a5;
}
.status dd {
text-align: right;
color: #dbe2ea;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 60%;
}
.log {
flex: 1;
min-height: 120px;
overflow-y: auto;
background: #0a0d11;
border: 1px solid #1b2029;
border-radius: 10px;
padding: 14px 18px;
font-family: "Andale Mono", monospace;
font-size: 18px;
line-height: 1.45;
color: #9fb0c3;
white-space: pre-wrap;
word-break: break-word;
}
.footer {
display: flex;
justify-content: space-between;
padding-top: 18px;
color: #6b7686;
font-size: 20px;
}
.busy {
color: #fbbf24;
}