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:
@@ -0,0 +1,45 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>RCS Options</title>
|
||||
<style>
|
||||
body { font-family: "Segoe UI", sans-serif; margin: 12px; max-width: 420px; }
|
||||
label { display: block; margin: 0.6rem 0; }
|
||||
input { width: 100%; padding: 0.4rem; box-sizing: border-box; }
|
||||
button { margin-top: 0.6rem; }
|
||||
.row { display: flex; gap: 0.4rem; }
|
||||
.row input { flex: 1; }
|
||||
.row button { margin-top: 0; width: auto; }
|
||||
ul { list-style: none; padding: 0; }
|
||||
li { display: flex; justify-content: space-between; gap: 0.5rem; margin: 0.35rem 0; }
|
||||
.muted { color: #666; font-size: 0.85rem; }
|
||||
.error { color: #b00020; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>RCS settings</h1>
|
||||
<label>
|
||||
Backend URL
|
||||
<input id="backendUrl" type="url" placeholder="http://127.0.0.1:8080">
|
||||
</label>
|
||||
<label>
|
||||
API key (optional)
|
||||
<input id="apiKey" type="password">
|
||||
</label>
|
||||
<button id="save" type="button">Save connection</button>
|
||||
<p id="msg"></p>
|
||||
|
||||
<h2>Blocked authors</h2>
|
||||
<p class="muted">Synced with the backend. Comments from these users are not saved.</p>
|
||||
<div class="row">
|
||||
<input id="blockUserInput" type="text" placeholder="AutoModerator">
|
||||
<button id="blockAdd" type="button">Add</button>
|
||||
</div>
|
||||
<p id="blocklistStatus" class="muted"></p>
|
||||
<ul id="blocklist"></ul>
|
||||
|
||||
<script src="../lib/api.js"></script>
|
||||
<script src="options.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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");
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user