fix notifications + improve them, improve normal UI stuff further + fix readme.
This commit is contained in:
@@ -16,6 +16,9 @@ const wsStatus = document.getElementById("ws-status");
|
||||
const listEl = document.getElementById("notification-list");
|
||||
const emptyState = document.getElementById("empty-state");
|
||||
const notificationScopeHint = document.getElementById("notification-scope-hint");
|
||||
const tableHead = document.getElementById("notification-thead");
|
||||
const searchInput = document.getElementById("notification-search");
|
||||
const resultHint = document.getElementById("notification-result-hint");
|
||||
|
||||
// Keyword section
|
||||
const keywordSection = document.getElementById("keyword-section");
|
||||
@@ -196,9 +199,163 @@ function wireToggle(inputEl, btnEl) {
|
||||
});
|
||||
}
|
||||
|
||||
// ── Notification table row ────────────────────────────────────────────────────
|
||||
function renderRow(item, prepend = true) {
|
||||
// ── Unified table state (notifications + client events) ──────────────────────
|
||||
const tableState = {
|
||||
entries: new Map(),
|
||||
search: "",
|
||||
sortBy: "ts",
|
||||
sortDir: "desc",
|
||||
};
|
||||
|
||||
let clientEventCounter = 0;
|
||||
|
||||
function getEntryUid(item, kind) {
|
||||
if (kind === "event") {
|
||||
return `event:${item.clientId || ""}:${item.event || ""}:${item.ts || 0}:${++clientEventCounter}`;
|
||||
}
|
||||
return `notification:${item.id || `${item.clientId || ""}:${item.ts || ""}:${item.title || ""}`}`;
|
||||
}
|
||||
|
||||
function getSortValue(entry, key) {
|
||||
const item = entry.item;
|
||||
if (key === "ts") return Number(item.ts) || 0;
|
||||
if (key === "source") {
|
||||
if (entry.kind === "event") return `event:${item.event || ""}`;
|
||||
return `notif:${item.category || ""}`;
|
||||
}
|
||||
const value = item[key];
|
||||
return value == null ? "" : String(value).toLowerCase();
|
||||
}
|
||||
|
||||
function compareEntries(a, b) {
|
||||
const key = tableState.sortBy;
|
||||
const av = getSortValue(a, key);
|
||||
const bv = getSortValue(b, key);
|
||||
let cmp;
|
||||
if (typeof av === "number" && typeof bv === "number") {
|
||||
cmp = av - bv;
|
||||
} else {
|
||||
cmp = String(av).localeCompare(String(bv));
|
||||
}
|
||||
if (cmp === 0) {
|
||||
cmp = (Number(a.item.ts) || 0) - (Number(b.item.ts) || 0);
|
||||
}
|
||||
return tableState.sortDir === "asc" ? cmp : -cmp;
|
||||
}
|
||||
|
||||
function entryMatchesSearch(entry, query) {
|
||||
if (!query) return true;
|
||||
const item = entry.item;
|
||||
const haystack = [
|
||||
item.clientId,
|
||||
item.user,
|
||||
item.host,
|
||||
item.title,
|
||||
item.process,
|
||||
item.processPath,
|
||||
item.keyword,
|
||||
entry.kind === "event" ? item.event : item.category,
|
||||
item.os,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ")
|
||||
.toLowerCase();
|
||||
return haystack.includes(query);
|
||||
}
|
||||
|
||||
function applyTableView() {
|
||||
if (!listEl) return;
|
||||
const q = tableState.search.trim().toLowerCase();
|
||||
const all = Array.from(tableState.entries.values());
|
||||
const filtered = q ? all.filter((entry) => entryMatchesSearch(entry, q)) : all;
|
||||
filtered.sort(compareEntries);
|
||||
|
||||
listEl.innerHTML = "";
|
||||
for (const entry of filtered) {
|
||||
listEl.appendChild(entry.element);
|
||||
}
|
||||
|
||||
if (emptyState) {
|
||||
if (all.length === 0) {
|
||||
emptyState.textContent = "No notifications yet.";
|
||||
emptyState.classList.remove("hidden");
|
||||
} else if (filtered.length === 0) {
|
||||
emptyState.textContent = "No notifications match the current search.";
|
||||
emptyState.classList.remove("hidden");
|
||||
} else {
|
||||
emptyState.classList.add("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
if (resultHint) {
|
||||
if (q) {
|
||||
resultHint.textContent = `${filtered.length} of ${all.length}`;
|
||||
} else {
|
||||
resultHint.textContent = all.length > 0 ? `${all.length} total` : "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function trimEntries() {
|
||||
if (tableState.entries.size <= MAX_ROWS) return;
|
||||
const ordered = Array.from(tableState.entries.entries())
|
||||
.sort((a, b) => (Number(b[1].item.ts) || 0) - (Number(a[1].item.ts) || 0));
|
||||
const keep = new Set(ordered.slice(0, MAX_ROWS).map(([uid]) => uid));
|
||||
for (const uid of tableState.entries.keys()) {
|
||||
if (!keep.has(uid)) tableState.entries.delete(uid);
|
||||
}
|
||||
}
|
||||
|
||||
function upsertEntry(item, kind, isLive) {
|
||||
const uid = getEntryUid(item, kind);
|
||||
const existing = tableState.entries.get(uid);
|
||||
if (existing) {
|
||||
existing.item = { ...existing.item, ...item };
|
||||
return existing;
|
||||
}
|
||||
const element =
|
||||
kind === "event"
|
||||
? createClientEventRowElement(item)
|
||||
: createNotificationRowElement(item, isLive);
|
||||
const entry = { item, kind, element, isLive };
|
||||
tableState.entries.set(uid, entry);
|
||||
trimEntries();
|
||||
return entry;
|
||||
}
|
||||
|
||||
function addNotification(item, isLive) {
|
||||
upsertEntry(item, "notification", isLive);
|
||||
applyTableView();
|
||||
}
|
||||
|
||||
function addClientEvent(item) {
|
||||
upsertEntry(item, "event", true);
|
||||
applyTableView();
|
||||
}
|
||||
|
||||
function clearTable() {
|
||||
tableState.entries.clear();
|
||||
applyTableView();
|
||||
}
|
||||
|
||||
// ── Client ID cell (collapsed by default, click to expand) ──────────────────
|
||||
const CLIENT_ID_SHORT_LEN = 8;
|
||||
|
||||
function clientIdCellHtml(clientId) {
|
||||
const id = String(clientId || "");
|
||||
if (!id) {
|
||||
return `<td class="py-2 pr-4 whitespace-nowrap text-slate-500">-</td>`;
|
||||
}
|
||||
if (id.length <= CLIENT_ID_SHORT_LEN) {
|
||||
return `<td class="py-2 pr-4 whitespace-nowrap font-mono text-xs text-slate-300">${escapeHtml(id)}</td>`;
|
||||
}
|
||||
const safeFull = escapeHtml(id);
|
||||
const safeShort = escapeHtml(id.slice(0, CLIENT_ID_SHORT_LEN));
|
||||
return `<td class="py-2 pr-4 whitespace-nowrap"><button type="button" class="client-id-toggle inline-flex items-center gap-1 font-mono text-xs px-2 py-0.5 rounded border border-slate-700 bg-slate-900/80 hover:bg-slate-800 hover:border-slate-600 text-slate-300" data-full="${safeFull}" data-short="${safeShort}" title="${safeFull} (click to expand)"><span class="client-id-text">${safeShort}<span class="text-slate-500">…</span></span><i class="fa-solid fa-chevron-right text-[10px] text-slate-500"></i></button></td>`;
|
||||
}
|
||||
|
||||
// ── Notification row factory ─────────────────────────────────────────────────
|
||||
function createNotificationRowElement(item, isLive) {
|
||||
const isClipboard = item.category === "clipboard";
|
||||
const sourceBadge = isClipboard
|
||||
? `<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs bg-violet-900/60 text-violet-300 border border-violet-700/50"><i class="fa-solid fa-clipboard text-xs"></i> clipboard</span>`
|
||||
@@ -207,7 +364,7 @@ function renderRow(item, prepend = true) {
|
||||
row.className = "border-t border-slate-800/60";
|
||||
row.innerHTML = `
|
||||
<td class="py-2 pr-4 whitespace-nowrap text-slate-400">${formatTime(item.ts)}</td>
|
||||
<td class="py-2 pr-4 whitespace-nowrap">${escapeHtml(item.clientId || "-")}</td>
|
||||
${clientIdCellHtml(item.clientId)}
|
||||
<td class="py-2 pr-4 whitespace-nowrap">${escapeHtml(item.user || "-")}</td>
|
||||
<td class="py-2 pr-4 max-w-xl truncate" title="${escapeHtml(item.title || "")}">${escapeHtml(item.title || "-")}</td>
|
||||
<td class="py-2 pr-4 whitespace-nowrap">${escapeHtml(item.process || "-")}</td>
|
||||
@@ -219,7 +376,8 @@ function renderRow(item, prepend = true) {
|
||||
const preview = row.querySelector(".preview-slot");
|
||||
if (preview) {
|
||||
const notificationId = item?.id || "";
|
||||
if (!notificationId) {
|
||||
const skipFetch = !notificationId || (!isLive && !item?.screenshotId);
|
||||
if (skipFetch) {
|
||||
preview.textContent = "-";
|
||||
preview.className = "text-slate-500";
|
||||
} else {
|
||||
@@ -280,16 +438,7 @@ function renderRow(item, prepend = true) {
|
||||
}
|
||||
}
|
||||
|
||||
if (prepend) {
|
||||
listEl.prepend(row);
|
||||
} else {
|
||||
listEl.appendChild(row);
|
||||
}
|
||||
|
||||
const rows = listEl.querySelectorAll("tr");
|
||||
if (rows.length > MAX_ROWS) rows[rows.length - 1].remove();
|
||||
|
||||
if (emptyState) emptyState.classList.toggle("hidden", listEl.children.length > 0);
|
||||
return row;
|
||||
}
|
||||
|
||||
// ── Keyword section ───────────────────────────────────────────────────────────
|
||||
@@ -559,14 +708,16 @@ function connect() {
|
||||
});
|
||||
|
||||
subscribeReady((history) => {
|
||||
if (listEl) listEl.innerHTML = "";
|
||||
history.reverse().forEach((item) => renderRow(item, false));
|
||||
if (emptyState) emptyState.classList.toggle("hidden", history.length > 0);
|
||||
clearTable();
|
||||
for (const item of history) {
|
||||
upsertEntry(item, "notification", false);
|
||||
}
|
||||
applyTableView();
|
||||
markAllNotificationsRead();
|
||||
});
|
||||
|
||||
subscribeNotifications((item) => renderRow(item, true));
|
||||
subscribeClientEvents((item) => renderClientEventRow(item));
|
||||
subscribeNotifications((item) => addNotification(item, true));
|
||||
subscribeClientEvents((item) => addClientEvent(item));
|
||||
}
|
||||
|
||||
const CLIENT_EVENT_BADGE = {
|
||||
@@ -575,15 +726,14 @@ const CLIENT_EVENT_BADGE = {
|
||||
client_purgatory: `<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs bg-amber-900/60 text-amber-300 border border-amber-700/50"><i class="fa-solid fa-hourglass-half text-xs"></i> purgatory</span>`,
|
||||
};
|
||||
|
||||
function renderClientEventRow(item) {
|
||||
if (!listEl) return;
|
||||
function createClientEventRowElement(item) {
|
||||
const badge = CLIENT_EVENT_BADGE[item.event] ||
|
||||
`<span class="text-xs text-slate-400">${escapeHtml(item.event)}</span>`;
|
||||
const row = document.createElement("tr");
|
||||
row.className = "border-t border-slate-800/60";
|
||||
row.innerHTML = `
|
||||
<td class="py-2 pr-4 whitespace-nowrap text-slate-400">${formatTime(item.ts)}</td>
|
||||
<td class="py-2 pr-4 whitespace-nowrap">${escapeHtml(item.clientId || "-")}</td>
|
||||
${clientIdCellHtml(item.clientId)}
|
||||
<td class="py-2 pr-4 whitespace-nowrap">${escapeHtml(item.user || "-")}</td>
|
||||
<td class="py-2 pr-4 max-w-xl truncate italic text-slate-400">client event</td>
|
||||
<td class="py-2 pr-4 whitespace-nowrap">${escapeHtml(item.os || "-")}</td>
|
||||
@@ -591,10 +741,7 @@ function renderClientEventRow(item) {
|
||||
<td class="py-2 pr-4 whitespace-nowrap">${badge}</td>
|
||||
<td class="py-2 pr-4"></td>
|
||||
`;
|
||||
listEl.prepend(row);
|
||||
const rows = listEl.querySelectorAll("tr");
|
||||
if (rows.length > MAX_ROWS) rows[rows.length - 1].remove();
|
||||
if (emptyState) emptyState.classList.add("hidden");
|
||||
return row;
|
||||
}
|
||||
|
||||
// ── Preview modal ─────────────────────────────────────────────────────────────
|
||||
@@ -700,6 +847,76 @@ function wireEventChannelsSave() {
|
||||
});
|
||||
}
|
||||
|
||||
function updateSortIndicators() {
|
||||
if (!tableHead) return;
|
||||
const headers = tableHead.querySelectorAll("th[data-sort-key]");
|
||||
for (const th of headers) {
|
||||
const icon = th.querySelector(".sort-icon");
|
||||
if (!icon) continue;
|
||||
const key = th.getAttribute("data-sort-key");
|
||||
if (key === tableState.sortBy) {
|
||||
icon.className = `sort-icon fa-solid ${tableState.sortDir === "asc" ? "fa-sort-up" : "fa-sort-down"} text-xs text-slate-300`;
|
||||
th.classList.add("text-slate-200");
|
||||
} else {
|
||||
icon.className = "sort-icon fa-solid fa-sort text-xs text-slate-600";
|
||||
th.classList.remove("text-slate-200");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function wireClientIdToggle() {
|
||||
if (!listEl) return;
|
||||
listEl.addEventListener("click", (event) => {
|
||||
const btn = event.target.closest(".client-id-toggle");
|
||||
if (!btn) return;
|
||||
const textEl = btn.querySelector(".client-id-text");
|
||||
const icon = btn.querySelector("i");
|
||||
if (!textEl) return;
|
||||
const expanded = btn.classList.toggle("expanded");
|
||||
if (expanded) {
|
||||
textEl.textContent = btn.dataset.full || "";
|
||||
if (icon) icon.className = "fa-solid fa-chevron-left text-[10px] text-slate-500";
|
||||
btn.title = `${btn.dataset.full} (click to collapse)`;
|
||||
} else {
|
||||
textEl.innerHTML = `${escapeHtml(btn.dataset.short || "")}<span class="text-slate-500">…</span>`;
|
||||
if (icon) icon.className = "fa-solid fa-chevron-right text-[10px] text-slate-500";
|
||||
btn.title = `${btn.dataset.full} (click to expand)`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function wireTableControls() {
|
||||
if (tableHead) {
|
||||
tableHead.addEventListener("click", (event) => {
|
||||
const th = event.target.closest("th[data-sort-key]");
|
||||
if (!th) return;
|
||||
const key = th.getAttribute("data-sort-key");
|
||||
if (!key) return;
|
||||
if (tableState.sortBy === key) {
|
||||
tableState.sortDir = tableState.sortDir === "asc" ? "desc" : "asc";
|
||||
} else {
|
||||
tableState.sortBy = key;
|
||||
tableState.sortDir = key === "ts" ? "desc" : "asc";
|
||||
}
|
||||
updateSortIndicators();
|
||||
applyTableView();
|
||||
});
|
||||
}
|
||||
|
||||
if (searchInput) {
|
||||
let debounce = null;
|
||||
searchInput.addEventListener("input", () => {
|
||||
if (debounce) clearTimeout(debounce);
|
||||
debounce = setTimeout(() => {
|
||||
tableState.search = searchInput.value || "";
|
||||
applyTableView();
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
|
||||
updateSortIndicators();
|
||||
}
|
||||
|
||||
// ── Bootstrap ─────────────────────────────────────────────────────────────────
|
||||
initWebhookTemplateEditor();
|
||||
wireKeywordSave();
|
||||
@@ -711,5 +928,7 @@ wireEventNotifSave();
|
||||
wireEventChannelsSave();
|
||||
updateDesktopPermissionUi();
|
||||
wireDesktopPermissionBtn();
|
||||
wireTableControls();
|
||||
wireClientIdToggle();
|
||||
initRoleUi();
|
||||
connect();
|
||||
|
||||
@@ -355,25 +355,51 @@
|
||||
id="notification-panel"
|
||||
class="bg-slate-900/60 border border-slate-800 rounded-xl p-4"
|
||||
>
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div class="flex items-center justify-between flex-wrap gap-3 mb-3">
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold">Notifications</h2>
|
||||
<p id="notification-scope-hint" class="text-xs text-slate-500 mt-0.5">
|
||||
Showing notifications for your accessible clients.
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="relative">
|
||||
<i class="fa-solid fa-magnifying-glass absolute left-3 top-1/2 -translate-y-1/2 text-slate-500 text-xs"></i>
|
||||
<input
|
||||
id="notification-search"
|
||||
type="search"
|
||||
placeholder="Search notifications..."
|
||||
class="w-64 sm:w-72 rounded-lg bg-slate-950 border border-slate-800 text-slate-100 pl-8 pr-3 py-2 text-sm focus:outline-none focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<span id="notification-result-hint" class="text-xs text-slate-500"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="overflow-x-auto max-h-96">
|
||||
<table class="min-w-full text-sm">
|
||||
<thead class="text-slate-400 sticky top-0 bg-slate-900">
|
||||
<thead id="notification-thead" class="text-slate-400 sticky top-0 bg-slate-900">
|
||||
<tr class="text-left">
|
||||
<th class="py-2 pr-4">Time</th>
|
||||
<th class="py-2 pr-4">Client</th>
|
||||
<th class="py-2 pr-4">User</th>
|
||||
<th class="py-2 pr-4">Title / Content</th>
|
||||
<th class="py-2 pr-4">Process</th>
|
||||
<th class="py-2 pr-4">Keyword</th>
|
||||
<th class="py-2 pr-4">Source</th>
|
||||
<th class="py-2 pr-4 select-none cursor-pointer hover:text-slate-200" data-sort-key="ts">
|
||||
<span class="inline-flex items-center gap-1">Time<i class="sort-icon fa-solid fa-sort text-xs text-slate-600"></i></span>
|
||||
</th>
|
||||
<th class="py-2 pr-4 select-none cursor-pointer hover:text-slate-200" data-sort-key="clientId">
|
||||
<span class="inline-flex items-center gap-1">Client<i class="sort-icon fa-solid fa-sort text-xs text-slate-600"></i></span>
|
||||
</th>
|
||||
<th class="py-2 pr-4 select-none cursor-pointer hover:text-slate-200" data-sort-key="user">
|
||||
<span class="inline-flex items-center gap-1">User<i class="sort-icon fa-solid fa-sort text-xs text-slate-600"></i></span>
|
||||
</th>
|
||||
<th class="py-2 pr-4 select-none cursor-pointer hover:text-slate-200" data-sort-key="title">
|
||||
<span class="inline-flex items-center gap-1">Title / Content<i class="sort-icon fa-solid fa-sort text-xs text-slate-600"></i></span>
|
||||
</th>
|
||||
<th class="py-2 pr-4 select-none cursor-pointer hover:text-slate-200" data-sort-key="process">
|
||||
<span class="inline-flex items-center gap-1">Process<i class="sort-icon fa-solid fa-sort text-xs text-slate-600"></i></span>
|
||||
</th>
|
||||
<th class="py-2 pr-4 select-none cursor-pointer hover:text-slate-200" data-sort-key="keyword">
|
||||
<span class="inline-flex items-center gap-1">Keyword<i class="sort-icon fa-solid fa-sort text-xs text-slate-600"></i></span>
|
||||
</th>
|
||||
<th class="py-2 pr-4 select-none cursor-pointer hover:text-slate-200" data-sort-key="source">
|
||||
<span class="inline-flex items-center gap-1">Source<i class="sort-icon fa-solid fa-sort text-xs text-slate-600"></i></span>
|
||||
</th>
|
||||
<th class="py-2 pr-4">Preview</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
@@ -137,10 +137,16 @@ for (const mode of ["powershell", "shell", "python"]) {
|
||||
/* ── Ace Editor ──────────────────────────────────────────────────── */
|
||||
|
||||
console.log("Copying Ace Editor ...");
|
||||
copyFile(
|
||||
path.join(NM, "ace-builds", "src-min-noconflict", "ace.js"),
|
||||
path.join(VENDOR, "ace-builds", "ace.js"),
|
||||
);
|
||||
const aceSrc = path.join(NM, "ace-builds", "src-min-noconflict");
|
||||
const aceDst = path.join(VENDOR, "ace-builds");
|
||||
for (const file of [
|
||||
"ace.js",
|
||||
"mode-json.js",
|
||||
"worker-json.js",
|
||||
"theme-tomorrow_night.js",
|
||||
]) {
|
||||
copyFile(path.join(aceSrc, file), path.join(aceDst, file));
|
||||
}
|
||||
|
||||
/* ── Chart.js ────────────────────────────────────────────────────── */
|
||||
|
||||
|
||||
@@ -1839,11 +1839,6 @@ export function getNotificationScreenshot(notificationId: string): NotificationS
|
||||
};
|
||||
}
|
||||
|
||||
export function clearNotificationScreenshots() {
|
||||
db.run(`DELETE FROM notification_screenshots`);
|
||||
console.log("[db] cleared notification screenshots");
|
||||
}
|
||||
|
||||
export type NotificationRow = {
|
||||
id: string;
|
||||
clientId: string;
|
||||
@@ -1910,14 +1905,17 @@ export function getNotificationHistory(limit: number = 500): NotificationRow[] {
|
||||
}));
|
||||
}
|
||||
|
||||
const NOTIFICATION_RETENTION_MS = 3 * 24 * 60 * 60 * 1000;
|
||||
const NOTIFICATION_RETENTION_DAYS = 3;
|
||||
const NOTIFICATION_RETENTION_MS = NOTIFICATION_RETENTION_DAYS * 24 * 60 * 60 * 1000;
|
||||
|
||||
export function pruneOldNotifications() {
|
||||
const cutoff = Date.now() - NOTIFICATION_RETENTION_MS;
|
||||
db.run(`DELETE FROM notification_screenshots WHERE notification_id IN (SELECT id FROM notifications WHERE ts < ?)`, cutoff);
|
||||
db.run(`DELETE FROM notification_screenshots WHERE ts < ?`, cutoff);
|
||||
db.run(`DELETE FROM notification_screenshots WHERE notification_id NOT IN (SELECT id FROM notifications)`);
|
||||
const result = db.run(`DELETE FROM notifications WHERE ts < ?`, cutoff);
|
||||
if (result.changes > 0) {
|
||||
console.log(`[db] pruned ${result.changes} notifications older than 3 days`);
|
||||
console.log(`[db] pruned ${result.changes} notifications older than ${NOTIFICATION_RETENTION_DAYS} days`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { fileURLToPath } from "url";
|
||||
import path from "path";
|
||||
import fs from "fs/promises";
|
||||
import { existsSync } from "node:fs";
|
||||
import { upsertClientRow, setOnlineState, listClients, markAllClientsOffline, getBuild, getBuildByTag, getAllBuilds, deleteExpiredBuilds, deleteBuild, getNotificationScreenshot, clearNotificationScreenshots, deleteClientRow, getClientIp, banIp, isIpBanned, clientExists, deleteExpiredSharedFiles, getChatHistory, insertChatMessage, getOnlineClientCountForUser, deleteExpiredChatMessages, recordBuildClaim, getClientMetricsSummary, pruneOldNotifications } from "./db";
|
||||
import { upsertClientRow, setOnlineState, listClients, markAllClientsOffline, getBuild, getBuildByTag, getAllBuilds, deleteExpiredBuilds, deleteBuild, getNotificationScreenshot, deleteClientRow, getClientIp, banIp, isIpBanned, clientExists, deleteExpiredSharedFiles, getChatHistory, insertChatMessage, getOnlineClientCountForUser, deleteExpiredChatMessages, recordBuildClaim, getClientMetricsSummary, pruneOldNotifications } from "./db";
|
||||
import { handleFrame, handleHello, handlePing, handlePong } from "./wsHandlers";
|
||||
import { getMessageByteLength, getMaxPayloadLimit, isAllowedClientMessageType } from "./wsValidation";
|
||||
import { ClientInfo, ClientRole } from "./types";
|
||||
@@ -732,7 +732,6 @@ async function startServer() {
|
||||
|
||||
|
||||
markAllClientsOffline();
|
||||
clearNotificationScreenshots();
|
||||
loadOrGenerateVapidKeys();
|
||||
|
||||
deleteExpiredBuilds();
|
||||
|
||||
@@ -109,17 +109,20 @@ Official docs: https://docs.docker.com/engine/install/debian/
|
||||
Set up Docker's apt repository:
|
||||
|
||||
```bash
|
||||
# Add Docker's official GPG key:
|
||||
sudo apt update
|
||||
sudo apt install -y ca-certificates curl
|
||||
sudo apt install ca-certificates curl
|
||||
sudo install -m 0755 -d /etc/apt/keyrings
|
||||
sudo curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
|
||||
sudo chmod a+r /etc/apt/keyrings/docker.asc
|
||||
|
||||
# Add the repository to Apt sources:
|
||||
sudo tee /etc/apt/sources.list.d/docker.sources <<EOF
|
||||
Types: deb
|
||||
URIs: https://download.docker.com/linux/debian
|
||||
Suites: $(. /etc/os-release && echo "$VERSION_CODENAME")
|
||||
Components: stable
|
||||
Architectures: $(dpkg --print-architecture)
|
||||
Signed-By: /etc/apt/keyrings/docker.asc
|
||||
EOF
|
||||
|
||||
@@ -131,27 +134,13 @@ On a derivative distro (e.g. Kali), replace the codename expansion with the matc
|
||||
Install Docker:
|
||||
|
||||
```bash
|
||||
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
|
||||
sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
|
||||
```
|
||||
|
||||
Make sure the daemon is running:
|
||||
|
||||
```bash
|
||||
sudo systemctl start docker
|
||||
```
|
||||
|
||||
Optional — run Docker without sudo:
|
||||
|
||||
```bash
|
||||
sudo usermod -aG docker $USER
|
||||
newgrp docker
|
||||
```
|
||||
|
||||
Verify:
|
||||
|
||||
```bash
|
||||
docker --version
|
||||
docker compose version
|
||||
sudo systemctl status docker
|
||||
```
|
||||
|
||||
**2. Grab the compose file**
|
||||
|
||||
Reference in New Issue
Block a user