Files
Edouard Durand 2b1b3b11ce Admin: clear the chat history
DELETE /rigshare/api/chat (admin) empties chat.jsonl, tells every client to
clear its history, then posts a notice saying who cleared it. Admin tab gets
a Chat section with a confirmed Clear chat history button.
2026-09-26 07:21:58 +00:00

234 lines
7.6 KiB
JavaScript

// Connection, identity and HTTP helpers shared by every RigShare module.
const LS = {
token: "rigshare.token",
guestId: "rigshare.guestId",
guestName: "rigshare.guestName",
};
function storageGet(key) {
try { return localStorage.getItem(key); } catch { return null; }
}
function storageSet(key, value) {
try {
if (value == null) localStorage.removeItem(key);
else localStorage.setItem(key, value);
} catch { /* private mode */ }
}
function randomId() {
return crypto.randomUUID?.() ?? Math.random().toString(36).slice(2) + Date.now().toString(36);
}
export class RigClient extends EventTarget {
constructor() {
super();
this.ws = null;
this.status = "offline"; // offline | connecting | online | denied
this.self = null;
this.users = [];
this.chat = []; // loaded chat messages, oldest first
this.chatMore = false; // older messages exist on the server
this.rooms = [];
this.stats = null;
this.server = {};
this.retry = 0;
this.token = storageGet(LS.token);
this.guestId = storageGet(LS.guestId) || randomId();
storageSet(LS.guestId, this.guestId);
this.guestName = storageGet(LS.guestName) || "";
}
get perms() {
return this.self?.perms ?? { edit: false, queue: false, admin: false };
}
get online() {
return this.status === "online";
}
emit(type, detail) {
this.dispatchEvent(new CustomEvent(type, { detail }));
}
on(type, fn) {
this.addEventListener(type, (e) => fn(e.detail));
}
// ----- websocket ------------------------------------------------------
connect() {
clearTimeout(this.reconnectTimer);
const base = location.pathname.replace(/[^/]*$/, "");
const url = `${location.protocol === "https:" ? "wss" : "ws"}://${location.host}${base}rigshare/ws`;
this.setStatus("connecting");
const ws = new WebSocket(url);
this.ws = ws;
ws.onopen = () => {
this.retry = 0;
this.hello();
};
ws.onmessage = (e) => {
let msg;
try { msg = JSON.parse(e.data); } catch { return; }
this.handle(msg);
};
ws.onclose = () => {
if (this.ws !== ws) return;
if (this.status !== "denied") this.setStatus("offline");
const delay = Math.min(15000, 1000 * 2 ** this.retry++);
this.reconnectTimer = setTimeout(() => this.connect(), delay);
};
}
hello() {
this.send({ type: "hello", token: this.token, guest_id: this.guestId, name: this.guestName });
}
send(msg) {
if (this.ws?.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(msg));
return true;
}
return false;
}
setStatus(status) {
if (this.status === status) return;
this.status = status;
this.emit("status", status);
}
handle(msg) {
switch (msg.type) {
case "welcome":
this.self = msg.self;
this.users = msg.users;
this.chat = msg.chat || [];
this.chatMore = !!msg.chat_more;
this.rooms = msg.rooms || [];
this.stats = msg.stats;
this.server = msg.server || {};
this.setStatus("online");
this.emit("self", this.self);
this.emit("presence", this.users);
this.emit("chat-history", this.chat);
this.emit("welcome", msg);
this.emit("server", this.server);
break;
case "denied":
this.self = null;
this.denyReason = msg.reason;
this.setStatus("denied");
this.emit("self", null);
if (msg.login) this.gotoLogin();
break;
case "self":
this.self = msg.self;
this.emit("self", this.self);
break;
case "logged_out":
this.setToken(null);
this.gotoLogin();
this.emit("toast", { severity: "warn", summary: "RigShare", detail: "Your session ended. You are now a guest." });
break;
case "presence":
this.users = msg.users;
this.rooms = msg.rooms || this.rooms;
this.emit("presence", this.users);
break;
case "server":
this.server = msg.server || {};
this.emit("server", this.server);
break;
case "stats":
this.stats = msg.stats;
this.emit("stats", msg.stats);
break;
case "chat_cleared":
this.chat = [];
this.chatMore = false;
this.emit("chat-history", this.chat);
break;
case "chat":
this.chat.push(msg.message);
this.emit("chat", msg.message);
break;
default:
this.emit(msg.type, msg);
}
}
// ----- identity -------------------------------------------------------
setToken(token) {
this.token = token;
storageSet(LS.token, token);
}
async login(username, password) {
const res = await this.request("POST", "/rigshare/api/login", { username, password });
this.setToken(res.token);
this.reconnect();
return res.user;
}
async logout() {
try { await this.request("POST", "/rigshare/api/logout"); } catch { /* ignore */ }
this.setToken(null);
this.gotoLogin();
}
gotoLogin() {
const base = location.pathname.replace(/[^/]*$/, "");
const next = encodeURIComponent(location.pathname + location.search + location.hash);
location.replace(`${base}rigshare/login?next=${next}`);
}
rename(name) {
this.guestName = name;
storageSet(LS.guestName, name);
this.send({ type: "rename", name });
}
reconnect() {
const ws = this.ws;
this.ws = null;
this.status = "offline";
ws?.close();
this.connect();
}
// Headers that identify this browser to the ComfyUI HTTP API.
authHeaders() {
if (this.token) return { "X-RigShare-Token": this.token };
return { "X-RigShare-Guest": this.guestId };
}
/** Fetch the page of chat before the oldest loaded message; returns them (oldest first). */
async loadOlderChat(limit = 50) {
const oldest = this.chat[0]?.seq;
if (!this.chatMore || oldest === undefined) return [];
const data = await this.request("GET", `/rigshare/api/chat?before=${oldest}&limit=${limit}`);
// A reconnect may have replaced the history meanwhile.
if (this.chat[0]?.seq !== oldest) return [];
this.chat.unshift(...data.messages);
this.chatMore = data.more;
return data.messages;
}
async request(method, path, body) {
const base = location.pathname.replace(/[^/]*$/, "");
const res = await fetch(base + path.replace(/^\//, ""), {
method,
headers: { "Content-Type": "application/json", ...this.authHeaders() },
body: body === undefined ? undefined : JSON.stringify(body),
});
let data = null;
try { data = await res.json(); } catch { /* empty */ }
if (!res.ok) throw new Error(data?.error || `${res.status} ${res.statusText}`);
return data;
}
}