Merge remote-tracking branch 'origin/260201-readonly' into beta

# Conflicts:
#	README.md
#	www/index.html
This commit is contained in:
Sergey Krashevich
2026-03-10 23:58:57 +03:00
28 changed files with 550 additions and 9 deletions
+17 -1
View File
@@ -52,8 +52,24 @@
drawTable(table, await r.json());
}
</script>
<script>
window.addEventListener('DOMContentLoaded', () => {
if (!window.go2rtcReady) return;
window.go2rtcReady.then(data => {
if (!data || !data.read_only) return;
const banner = document.getElementById('readonly-banner');
if (banner) banner.style.display = 'block';
document.querySelectorAll('main input, main select, main button, main textarea').forEach(el => {
el.disabled = true;
});
});
});
</script>
<main>
<div id="readonly-banner" style="display: none; padding: 10px; background: #ffe9e9; border: 1px solid #f0b4b4;">
Read-only mode: add actions are disabled.
</div>
<button id="stream">Temporary stream</button>
<div>
<form id="stream-form">
@@ -566,4 +582,4 @@
</main>
</body>
</html>
</html>
+46 -3
View File
@@ -1182,18 +1182,61 @@
let dump;
document.getElementById('save').addEventListener('click', async () => {
const saveButton = document.getElementById('save');
const readOnlyWarning = 'Enabling read_only: true cannot be reverted remotely. To disable it you must edit the config file on the server manually. Continue?';
const hasReadOnlyTrue = (text) => {
if (!text) return false;
return /(^|\n)\s*read_only\s*:\s*true\s*(#.*)?$/m.test(text);
};
const applyReadOnly = () => {
saveButton.disabled = true;
saveButton.title = 'Read-only mode';
editor.updateOptions({readOnly: true});
};
if (window.go2rtcReady) {
window.go2rtcReady.then(data => {
if (data && data.read_only) applyReadOnly();
});
}
saveButton.addEventListener('click', async () => {
let r = await fetch('api/config', {cache: 'no-cache'});
if (r.ok && dump !== await r.text()) {
alert('Config was changed from another place. Refresh the page and make changes again');
return;
}
r = await fetch('api/config', {method: 'POST', body: editor.getValue()});
const nextValue = editor.getValue();
if (hasReadOnlyTrue(nextValue) && !hasReadOnlyTrue(dump)) {
if (!confirm(readOnlyWarning)) return;
}
r = await fetch('api/config', {method: 'POST', body: nextValue});
if (r.ok) {
alert('OK');
dump = editor.getValue();
dump = nextValue;
await fetch('api/restart', {method: 'POST'});
// Poll server until it's back online before reloading
const waitForServer = async () => {
for (let i = 0; i < 20; i++) {
await new Promise(r => setTimeout(r, 500));
try {
const response = await fetch('api/config', {cache: 'no-cache'});
if (response.ok || response.status === 410) {
location.reload();
return;
}
} catch (e) {
// Server still down, continue polling
}
}
// Fallback: reload after 10 seconds even if server check fails
location.reload();
};
waitForServer();
} else {
alert(await r.text());
}
+24 -5
View File
@@ -47,11 +47,18 @@
</main>
<script>
const templates = [
let templates = [
'<a href="stream.html?src={name}">stream</a>',
'<a href="links.html?src={name}">links</a>',
'<a href="#" data-name="{name}">delete</a>',
];
let readOnly = false;
const applyReadOnly = (flag) => {
if (!flag || readOnly) return;
readOnly = true;
templates = templates.filter(link => link.indexOf('delete') < 0);
};
document.querySelector('.controls > button')
.addEventListener('click', () => {
@@ -263,6 +270,8 @@
function updateInfo() {
return fetch(infoURL, {cache: 'no-cache'}).then(r => r.json()).then(data => {
applyReadOnly(Boolean(data.read_only));
const cpuUsage = clampPercent(data.system?.cpu_usage);
const memUsed = toNumber(data.system?.mem_used);
const memTotal = toNumber(data.system?.mem_total);
@@ -287,11 +296,21 @@
});
}
updateInfo().then(isSupported => {
if (isSupported) startInfoUpdates();
});
const applyInfo = (data) => {
applyReadOnly(Boolean(data.read_only));
updateInfo().then(isSupported => {
if (isSupported) startInfoUpdates();
});
};
reload();
if (window.go2rtcReady) {
window.go2rtcReady.then(data => {
if (data) applyInfo(data);
});
} else {
const url = new URL('api', location.href);
fetch(url, {cache: 'no-cache'}).then(r => r.json()).then(data => applyInfo(data));
}
</script>
</body>
+15
View File
@@ -154,6 +154,21 @@ Telegram: rtmps://xxx-x.rtmp.t.me/s/xxxxxxxxxx:xxxxxxxxxxxxxxxxxxxxxx</pre>
fetch(url, {method: 'POST'});
});
</script>
<script>
const applyReadOnly = () => {
const ids = ['play-send', 'play-url', 'pub-send', 'pub-url'];
ids.forEach(id => {
const el = document.getElementById(id);
if (el) el.disabled = true;
});
};
if (window.go2rtcReady) {
window.go2rtcReady.then(data => {
if (data && data.read_only) applyReadOnly();
});
}
</script>
<div id="webrtc">
<h2>WebRTC Magic</h2>
+21
View File
@@ -136,3 +136,24 @@ document.body.innerHTML = `
</nav>
</header>
` + document.body.innerHTML;
window.go2rtcReady = (async () => {
try {
const url = new URL('api', location.href);
const r = await fetch(url, {cache: 'no-cache'});
if (!r.ok) return null;
const data = await r.json();
window.go2rtcInfo = data;
return data;
} catch (e) {
return null;
}
})();
window.go2rtcReady.then(data => {
if (!data || !data.read_only) return;
const links = document.querySelectorAll('nav a[href="add.html"], nav a[href="config.html"]');
links.forEach(link => {
link.style.display = 'none';
});
});
+5
View File
@@ -68,6 +68,11 @@
"password": {
"type": "string"
},
"read_only": {
"description": "Disable write actions in WebUI/API",
"type": "boolean",
"default": false
},
"local_auth": {
"description": "Enable auth check for localhost requests",
"type": "boolean",