Add self-hosted RCS backend, extension, and ops tooling

Ship the Go/SQLite API and Web UI, Chrome/Brave capture addon,
Docker Compose, Pangolin reverse-proxy support, and a user-crontab
watchdog so the binary stays running without systemd.
This commit is contained in:
2026-08-06 22:02:45 +02:00
parent 3f05c97e1f
commit 5a0e2e630b
30 changed files with 4334 additions and 2 deletions
+102
View File
@@ -0,0 +1,102 @@
const backendUrlInput = document.getElementById("backendUrl");
const apiKeyInput = document.getElementById("apiKey");
const msg = document.getElementById("msg");
const blocklistEl = document.getElementById("blocklist");
const blockUserInput = document.getElementById("blockUserInput");
const blocklistStatus = document.getElementById("blocklistStatus");
function renderBlocklist(usernames) {
blocklistEl.innerHTML = "";
if (!usernames.length) {
const empty = document.createElement("li");
empty.className = "muted";
empty.textContent = "No blocked authors.";
blocklistEl.appendChild(empty);
return;
}
usernames.forEach((name) => {
const li = document.createElement("li");
const label = document.createElement("span");
label.textContent = name;
const remove = document.createElement("button");
remove.type = "button";
remove.textContent = "Remove";
remove.addEventListener("click", async () => {
blocklistStatus.textContent = "Syncing…";
try {
const list = await rcsRemoveBlocked(name);
renderBlocklist(list);
blocklistStatus.textContent = "Synced";
} catch (err) {
blocklistStatus.textContent = String(err && err.message ? err.message : err);
blocklistStatus.classList.add("error");
}
});
li.appendChild(label);
li.appendChild(remove);
blocklistEl.appendChild(li);
});
}
async function syncBlocklist() {
blocklistStatus.classList.remove("error");
blocklistStatus.textContent = "Syncing…";
try {
const list = await rcsPullBlocklist();
renderBlocklist(list);
blocklistStatus.textContent = `Synced (${list.length})`;
} catch (err) {
const cached = await rcsGetCachedBlocklist();
renderBlocklist(cached);
blocklistStatus.textContent = "Sync failed — showing cache. " + (err && err.message ? err.message : err);
blocklistStatus.classList.add("error");
}
}
chrome.storage.sync.get({ backendUrl: "", apiKey: "" }, (settings) => {
backendUrlInput.value = settings.backendUrl || "";
apiKeyInput.value = settings.apiKey || "";
if (settings.backendUrl) {
syncBlocklist();
} else {
rcsGetCachedBlocklist().then(renderBlocklist);
}
});
document.getElementById("save").addEventListener("click", async () => {
const backendUrl = backendUrlInput.value.trim().replace(/\/+$/, "");
const apiKey = apiKeyInput.value.trim();
try {
if (backendUrl) new URL(backendUrl);
} catch {
msg.textContent = "Invalid URL";
return;
}
if (backendUrl) {
try {
const origin = new URL(backendUrl).origin + "/*";
await chrome.permissions.request({ origins: [origin] });
} catch {
// ignore
}
}
await chrome.storage.sync.set({ backendUrl, apiKey });
msg.textContent = "Saved.";
if (backendUrl) await syncBlocklist();
});
document.getElementById("blockAdd").addEventListener("click", async () => {
const username = blockUserInput.value.trim();
if (!username) return;
blocklistStatus.classList.remove("error");
blocklistStatus.textContent = "Syncing…";
try {
const list = await rcsAddBlocked(username);
blockUserInput.value = "";
renderBlocklist(list);
blocklistStatus.textContent = "Synced";
} catch (err) {
blocklistStatus.textContent = String(err && err.message ? err.message : err);
blocklistStatus.classList.add("error");
}
});