A tool for modifying websim project info (alt+m)
// ==UserScript==
// @name Websim menu
// @namespace http://tampermonkey.net/
// @version 7.HTMLrenderfix
// @description A tool for modifying websim project info (alt+m)
// @match *://*.websim.com/*
// @match *://websim.com/*
// @run-at document-start
// @license MIT
// @grant none
// ==/UserScript==
(function () {
'use strict';
if (window === window.top) return;
console.log("Websim Manager + RoomState loaded");
let currentUsername = null;
let allPosts = [];
let displayedPosts = [];
let currentType = "";
let currentPage = 1;
const PAGE_SIZE = 50;
let roomState = null;
let selectedRoomKey = null; // which top-level key in roomState we're viewing
let isRoomMode = false;
let allCollectionTypes = new Set();
function escapeHTML(str) {
str=String(str);
if (!str) return '';
return str
.replace(/&/g, "&") // must be first
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'")
.replace(/=/g, "=")
.replace(/\\/g, "\");
}
function prettyPrintJson(value, indentSize = 2, maxPrettyDepth = 80) {
if (typeof value !== 'object' || value === null) {
return JSON.stringify(value);
}
const indent = ' '.repeat(indentSize);
function helper(obj, depth, currentIndent) {
if (depth > maxPrettyDepth || typeof obj !== 'object' || obj === null) {
return JSON.stringify(obj);
}
const isArray = Array.isArray(obj);
const keys = isArray ? null : Object.keys(obj);
const opening = isArray ? '[' : '{';
const closing = isArray ? ']' : '}';
let result = opening;
const length = isArray ? obj.length : keys.length;
for (let i = 0; i < length; i++) {
const key = isArray ? null : keys[i];
const child = isArray ? obj[i] : obj[key];
result += '\n' + currentIndent + indent;
if (!isArray) {
result += JSON.stringify(key) + ': ';
}
if (typeof child === 'object' && child !== null && depth + 1 <= maxPrettyDepth) {
result += helper(child, depth + 1, currentIndent + indent);
} else {
result += JSON.stringify(child);
}
if (i < length - 1) {
result += ','; // comma only between siblings
}
}
result += '\n' + currentIndent + closing;
return result;
}
return helper(value, 0, '');
}
async function startUI() {
if (document.__websim_ui_initialized) return;
document.__websim_ui_initialized = true;
window.room=new WebsimSocket();
try {
const user = await window.websim?.getCurrentUser?.();
currentUsername = user?.username || null;
} catch {}
// ── Container ────────────────────────────────────────
const container = document.createElement("div");
Object.assign(container.style, {
position: "fixed", top: "10px", right: "10px",
width: "720px", height: "850px",
background: "#1e1e1e", color: "#ddd",
overflowY: "auto",
border: "2px solid #555", borderRadius: "12px",
zIndex: "999999", padding: "14px", fontFamily: "monospace",
fontSize: "13px", display: "none", flexDirection: "column",
boxShadow: "0 10px 40px #000c", boxSizing: "border-box"
});
document.body.appendChild(container);
const toggleBtn = document.createElement("button");
toggleBtn.textContent = "Manager (Alt+M)";
toggleBtn.style.cssText = "width:100%;padding:12px;background:#0066cc;color:white;font-weight:bold;border:none;border-radius:8px;cursor:pointer;font-size:14px;margin-bottom:12px";
container.appendChild(toggleBtn);
const panel = document.createElement("div");
panel.style.flex = "1"; panel.style.minHeight = "0";
panel.style.display = "flex"; panel.style.flexDirection = "column";
panel.style.gap = "10px";
container.appendChild(panel);
let visible = false;
toggleBtn.onclick = () => {
visible = !visible;
container.style.display = visible ? "flex" : "none";
};
document.addEventListener("keydown", e => {
if (e.altKey && e.code === "KeyM") {
e.preventDefault();
toggleBtn.click();
}
});
// ── Mode switch + Collection selector ────────────────
const modeRow = document.createElement("div");
modeRow.style.display = "flex"; modeRow.style.gap = "12px";
modeRow.style.alignItems = "center"; modeRow.style.padding = "6px 0";
const modeToggle = document.createElement("input");
modeToggle.type = "checkbox";
modeToggle.id = "room-mode-toggle";
const modeLabel = document.createElement("label");
modeLabel.htmlFor = "room-mode-toggle";
modeLabel.textContent = "Room State Mode";
modeLabel.style.color = "#ffcc00"; modeLabel.style.fontWeight = "bold";
const collSelect = document.createElement("select");
collSelect.style.flex = "1"; collSelect.style.padding = "6px";
collSelect.style.background = "#2a2a2a"; collSelect.style.color = "white";
collSelect.style.border = "1px solid #555"; collSelect.style.borderRadius = "4px";
const defaultOpt = document.createElement("option");
defaultOpt.value = ""; defaultOpt.text = "— all collections —";
collSelect.appendChild(defaultOpt);
modeRow.append(modeToggle, modeLabel, collSelect);
panel.appendChild(modeRow);
// ── Type / Filter input row ──────────────────────────
const filterRow = document.createElement("div");
filterRow.style.display = "flex"; filterRow.style.gap = "8px";
const typeInput = document.createElement("input");
typeInput.placeholder = "Type filter (required for create/nuke)";
typeInput.style.flex = "1"; typeInput.style.padding = "8px";
typeInput.style.background = "#333"; typeInput.style.color = "white";
typeInput.style.border = "1px solid #555"; typeInput.style.borderRadius = "4px";
const reloadBtn = document.createElement("button");
reloadBtn.textContent = "↻ Reload";
reloadBtn.style.padding = "8px 16px"; reloadBtn.style.background = "#28a745";
reloadBtn.style.color = "white"; reloadBtn.style.border = "none";
reloadBtn.style.borderRadius = "4px";
const status = document.createElement("div");
status.style.padding = "8px 12px"; status.style.background = "#333";
status.style.borderRadius = "4px"; status.style.minWidth = "140px";
status.textContent = "Not loaded";
filterRow.append(typeInput, reloadBtn, status);
panel.appendChild(filterRow);
// Custom property filter (still applies in post mode)
const customFilterBox = document.createElement("textarea");
customFilterBox.placeholder = '{"username":"fish", "score":9001} — exact match';
customFilterBox.style.width = "100%"; customFilterBox.style.height = "48px";
customFilterBox.style.background = "#2a2a2a"; customFilterBox.style.color = "#eee";
customFilterBox.style.border = "1px solid #555"; customFilterBox.style.borderRadius = "4px";
customFilterBox.style.fontSize = "12px"; customFilterBox.style.padding = "6px";
panel.appendChild(customFilterBox);
const filterActions = document.createElement("div");
filterActions.style.display = "flex"; filterActions.style.gap = "8px";
const applyF = document.createElement("button");
applyF.textContent = "Apply Filter"; applyF.style.background = "#7b1fa2";
applyF.style.color = "white"; applyF.style.padding = "6px 14px";
const clearF = document.createElement("button");
clearF.textContent = "Clear"; clearF.style.background = "#444"; clearF.style.color = "white";
clearF.style.padding = "6px 14px";
filterActions.append(applyF, clearF);
panel.appendChild(filterActions);
// ── Main content area ────────────────────────────────
const contentArea = document.createElement("div");
contentArea.style.flex = "1"; contentArea.style.overflowY = "auto";
contentArea.style.background = "#252525"; contentArea.style.border = "1px solid #444";
contentArea.style.borderRadius = "8px"; contentArea.style.padding = "10px";
contentArea.style.height = "auto";
contentArea.style.minHeight = "700px";
contentArea.style.maxHeight = "85vh";
contentArea.style.overflowY = "auto";
panel.appendChild(contentArea);
// ── Pagination ───────────────────────────────────────
const pagi = document.createElement("div");
pagi.style.display = "flex"; pagi.style.justifyContent = "center";
pagi.style.gap = "16px"; pagi.style.padding = "8px";
const prev = document.createElement("button"); prev.textContent = "← Prev";
const pageTxt = document.createElement("span"); pageTxt.style.color = "#aaa";
const next = document.createElement("button"); next.textContent = "Next →";
pagi.append(prev, pageTxt, next);
panel.appendChild(pagi);
// ── Create / Nuke (only visible in post mode) ────────
const createNukeArea = document.createElement("div");
createNukeArea.style.display = "flex"; createNukeArea.style.flexDirection = "column";
createNukeArea.style.gap = "12px"; createNukeArea.style.marginTop = "auto";
createNukeArea.style.paddingTop = "12px"; createNukeArea.style.borderTop = "1px solid #444";
const createSection = document.createElement("div");
createSection.style.background = "#1b3a1b"; createSection.style.padding = "10px";
createSection.style.borderRadius = "6px";
const createTA = document.createElement("textarea");
createTA.placeholder = "JSON data for new entry";
createTA.style.width = "97%"; createTA.style.height = "64px";
const createB = document.createElement("button");
createB.textContent = "Create"; createB.style.background = "#2e7d32";
createSection.append(createTA, createB);
const nukeSection = document.createElement("div");
nukeSection.style.background = "#3a1b1b"; nukeSection.style.padding = "10px";
nukeSection.style.borderRadius = "6px";
nukeSection.innerHTML = `
<div style="color:#ff9999;margin-bottom:6px">Depth Bomb</div>
<div style="display:flex;gap:8px">
<input id="bomb-depth" type="number" value="15000" min="1000" style="width:110px;padding:6px;background:#300;color:#fee">
<input id="bomb-key" type="text" value="data" style="flex:1;padding:6px;background:#300;color:#fee">
<button id="bomb-go" style="background:#900;color:#fee;padding:6px 12px">NUKE</button>
</div>
`;
createNukeArea.append(createSection, nukeSection);
panel.appendChild(createNukeArea);
// ── Helpers ──────────────────────────────────────────
function getCollection(t) {
return new window.WebsimSocket().collection(t);
}
function flattenEntry(p) {
return {
// first the user-controlled data
...p.data,
// then the real metadata → these overwrite any shadowing keys
id: p.id,
type: p.type,
created_at: p.created_at,
updated_at: p.updated_at || null,
username: p.author?.username || "?"
};
}
function matchesCustomFilter(entry, filterObj) {
if (!filterObj || typeof filterObj !== "object") return true;
return Object.entries(filterObj).every(([k, v]) => entry[k] === v);
}
function updateCollectionList() {
collSelect.innerHTML = "";
const allOpt = document.createElement("option");
allOpt.value = ""; allOpt.text = "— all collections —";
collSelect.appendChild(allOpt);
[...allCollectionTypes].sort().forEach(t => {
const opt = document.createElement("option");
opt.value = t; opt.text = t;
collSelect.appendChild(opt);
});
}
async function loadPosts() {
status.textContent = "Loading posts...";
try {
const proj = await window.websim.getCurrentProject();
const res = await fetch(`https://websim.com/api/v1/projects/${proj.id}/posts`);
if (!res.ok) throw new Error(res.status);
const { posts } = await res.json();
allPosts = posts || [];
allCollectionTypes.clear();
allPosts.forEach(p => allCollectionTypes.add(p.type));
updateCollectionList();
status.textContent = `Posts: ${allPosts.length}`;
refreshView();
} catch (err) {
status.textContent = "Error";
contentArea.innerHTML = `<div style="color:#f66;padding:20px">Failed to load: ${err}</div>`;
}
}
async function loadRoomState() {
status.textContent = "Loading room state...";
try {
roomState = await window.room.roomState;
status.textContent = `Room keys: ${Object.keys(roomState).length}`;
refreshView();
} catch (err) {
status.textContent = "Room error";
contentArea.innerHTML = `<div style="color:#f66;padding:20px">Cannot read room state: ${err}</div>`;
}
}
function refreshView() {
contentArea.innerHTML = "";
pagi.style.display = isRoomMode ? "none" : "flex";
createNukeArea.style.display = isRoomMode ? "none" : "flex";
if (isRoomMode) {
renderRoomStateView();
} else {
renderPostsView();
}
}
function renderPostsView() {
let filtered = allPosts;
if (currentType) {
filtered = filtered.filter(p => p.type === currentType);
}
const custom = (() => {
try { return customFilterBox.value.trim() ? JSON.parse(customFilterBox.value) : {}; }
catch { return {}; }
})();
if (Object.keys(custom).length > 0) {
filtered = filtered.filter(p => matchesCustomFilter(flattenEntry(p), custom));
}
displayedPosts = filtered;
const total = displayedPosts.length;
const pages = Math.ceil(total / PAGE_SIZE) || 1;
currentPage = Math.max(1, Math.min(currentPage, pages));
pageTxt.textContent = `Page ${currentPage} / ${pages} (${total})`;
prev.disabled = currentPage <= 1;
next.disabled = currentPage >= pages;
const start = (currentPage - 1) * PAGE_SIZE;
const pageItems = displayedPosts.slice(start, start + PAGE_SIZE);
if (!pageItems.length) {
contentArea.innerHTML = '<div style="color:#888;text-align:center;padding:60px">No matching entries</div>';
return;
}
pageItems.forEach(post => {
const flat = flattenEntry(post);
const mine = flat.username === currentUsername;
const block = document.createElement("div");
block.style.marginBottom = "16px"; block.style.paddingBottom = "12px";
block.style.borderBottom = "1px dashed #444";
const head = document.createElement("div");
head.style.display = "flex"; head.style.gap = "12px"; head.style.alignItems = "center";
head.innerHTML = `
<b style="color:#4caf50">${post.id.slice(0,12)}…</b>
<span style="color:${mine?'#8bc34a':'#ff9800'}">@${flat.username}</span>
<span style="color:#aaa;font-size:11px">${new Date(post.created_at).toLocaleString()}</span>
<code style="background:#333;padding:2px 6px;border-radius:3px">${escapeHTML(post.type)}</code>
`;
if (mine) {
const del = document.createElement("button");
del.textContent = "Delete"; del.style.marginLeft = "auto";
del.style.background = "#c62828"; del.style.color = "white";
del.onclick = async () => {
if (!confirm("Delete?")) return;
await getCollection(post.type).delete(post.id);
await loadPosts();
};
head.appendChild(del);
}
// ── NEW: Copy JSON button ───────────────────────────────
const copyBtn = document.createElement("button");
copyBtn.textContent = "Copy JSON";
copyBtn.style.marginLeft = "auto"; // push to right if you want
copyBtn.style.padding = "4px 10px";
copyBtn.style.background = "#555";
copyBtn.style.color = "white";
copyBtn.style.border = "none";
copyBtn.style.borderRadius = "4px";
copyBtn.style.fontSize = "12px";
copyBtn.style.cursor = "pointer";
copyBtn.onclick = () => {
const dataOnly = { ...post.data };
const json = prettyPrintJson(dataOnly); // removed , null, 2 why the hell does SPACING make it fail/recursive
navigator.clipboard.writeText(json).then(() => {
copyBtn.textContent = "Copied ✓";
copyBtn.style.background = "#388e3c";
setTimeout(() => {
copyBtn.textContent = "Copy JSON";
copyBtn.style.background = "#555";
}, 1200);
}).catch(err => {
console.error(err);
copyBtn.textContent = "Copy failed";
setTimeout(() => copyBtn.textContent = "Copy JSON", 2000);
});
};
head.appendChild(copyBtn);
// ─────────────────────────────────────────────────────────
block.appendChild(head);
const tree = document.createElement("div");
tree.style.marginLeft = "16px"; tree.style.marginTop = "6px";
Object.keys(flat).sort().forEach(k => tree.appendChild(createTreeNode(k, flat[k])));
block.appendChild(tree);
if (mine) {
const updateArea = document.createElement("div");
updateArea.style.marginTop = "12px";
updateArea.style.padding = "10px";
updateArea.style.background = "#222";
updateArea.style.borderRadius = "6px";
const txt = document.createElement("textarea");
txt.value = prettyPrintJson(post.data);
txt.style.width = "100%";
txt.style.height = "80px";
txt.style.background = "#333";
txt.style.color = "white";
txt.style.border = "1px solid #555";
txt.style.fontFamily = "monospace";
const btn = document.createElement("button");
btn.textContent = "Update";
btn.style.marginTop = "8px";
btn.style.background = "#1976d2";
btn.style.color = "white";
btn.style.padding = "6px 12px";
btn.style.border = "none";
btn.style.borderRadius = "4px";
btn.onclick = async () => {
try {
const updatedData = JSON.parse(txt.value);
await getCollection(post.type).update(post.id, updatedData);
await loadPosts();
} catch (e) {
alert("Update failed: " + e.message);
}
};
updateArea.append(txt, btn);
block.appendChild(updateArea);
}
contentArea.appendChild(block);
});
}
function renderRoomStateView() {
contentArea.innerHTML = '';
if (!roomState || typeof roomState !== 'object') {
const msg = document.createElement('div');
msg.style.color = '#f88';
msg.style.padding = '40px';
msg.style.textAlign = 'center';
msg.textContent = 'room.roomState not loaded or invalid';
contentArea.appendChild(msg);
return;
}
const keys = Object.keys(roomState);
if (!selectedRoomKey || !roomState[selectedRoomKey]) {
// ── LIST VIEW ──────────────────────────────────────────────────────
// Existing keys
if (keys.length > 0) {
const title = document.createElement('div');
title.textContent = 'Top-level keys in room.roomState';
title.style.color = '#ffcc00';
title.style.fontWeight = 'bold';
title.style.marginBottom = '12px';
title.style.fontSize = '14px';
contentArea.appendChild(title);
const list = document.createElement('div');
list.style.display = 'flex';
list.style.flexWrap = 'wrap';
list.style.gap = '8px';
list.style.marginBottom = '24px';
keys.sort().forEach(k => {
const btn = document.createElement('button');
btn.textContent = k;
btn.style.padding = '6px 12px';
btn.style.background = '#444';
btn.style.color = '#eee';
btn.style.border = '1px solid #666';
btn.style.borderRadius = '4px';
btn.style.cursor = 'pointer';
btn.onclick = () => {
selectedRoomKey = k;
refreshView();
};
list.appendChild(btn);
});
contentArea.appendChild(list);
} else {
const empty = document.createElement('div');
empty.style.color = '#aaa';
empty.style.padding = '20px';
empty.style.textAlign = 'center';
empty.textContent = 'room.roomState is empty — add a key below';
contentArea.appendChild(empty);
}
// Add form (only in list view)
const addSection = document.createElement('div');
addSection.style.padding = '16px';
addSection.style.background = '#1e2a1e';
addSection.style.borderRadius = '8px';
addSection.style.border = '1px solid #4a6c4a';
addSection.innerHTML = '<div style="color:#a5d6a7; font-weight:bold; margin-bottom:12px">Add new top-level key</div>';
const keyInput = document.createElement('input');
keyInput.placeholder = 'Key name (e.g. config, players)';
keyInput.style.width = '100%';
keyInput.style.padding = '8px';
keyInput.style.marginBottom = '12px';
keyInput.style.background = '#0d1a0d';
keyInput.style.color = '#eee';
keyInput.style.border = '1px solid #4a6c4a';
keyInput.style.borderRadius = '4px';
addSection.appendChild(keyInput);
const valueTA = document.createElement('textarea');
valueTA.placeholder = 'Initial JSON value\n{}\n[]\n"hello"\n42\n{"enabled":true}';
valueTA.style.width = '100%';
valueTA.style.height = '120px';
valueTA.style.background = '#0d1a0d';
valueTA.style.color = '#eee';
valueTA.style.border = '1px solid #4a6c4a';
valueTA.style.borderRadius = '4px';
valueTA.style.fontFamily = 'monospace';
valueTA.style.padding = '8px';
addSection.appendChild(valueTA);
const btnRow = document.createElement('div');
btnRow.style.marginTop = '12px';
btnRow.style.display = 'flex';
btnRow.style.gap = '12px';
const createBtn = document.createElement('button');
createBtn.textContent = 'Create / Update';
createBtn.style.background = '#2e7d32';
createBtn.style.color = 'white';
createBtn.style.padding = '8px 20px';
createBtn.style.border = 'none';
createBtn.style.borderRadius = '4px';
createBtn.onclick = async () => {
const name = keyInput.value.trim();
if (!name) return alert('Key name required');
let val;
try {
const text = valueTA.value.trim();
val = text ? JSON.parse(text) : {};
} catch (e) {
return alert('Bad JSON: ' + e.message);
}
if (roomState[name] !== undefined && !confirm(`"${name}" exists. Overwrite?`)) return;
if (!confirm(`Set room.roomState["${name}"]?`)) return;
try {
await window.room.updateRoomState({ [name]: val });
await loadRoomState();
keyInput.value = '';
valueTA.value = '';
} catch (err) {
alert('Save failed: ' + err.message);
}
};
btnRow.appendChild(createBtn);
const clearBtn = document.createElement('button');
clearBtn.textContent = 'Clear';
clearBtn.style.background = '#444';
clearBtn.style.color = '#ddd';
clearBtn.style.padding = '8px 16px';
clearBtn.style.border = '1px solid #666';
clearBtn.style.borderRadius = '4px';
clearBtn.onclick = () => {
keyInput.value = '';
valueTA.value = '';
};
btnRow.appendChild(clearBtn);
addSection.appendChild(btnRow);
contentArea.appendChild(addSection);
} else {
// ── EDITOR VIEW (when a key is selected) ──────────────────────────
const backBtn = document.createElement('button');
backBtn.textContent = '← Back to keys';
backBtn.style.marginBottom = '16px';
backBtn.style.padding = '6px 12px';
backBtn.style.background = '#555';
backBtn.style.color = 'white';
backBtn.style.border = 'none';
backBtn.style.borderRadius = '4px';
backBtn.onclick = () => {
selectedRoomKey = null;
refreshView();
};
contentArea.appendChild(backBtn);
const title = document.createElement('div');
title.innerHTML = `<b style="color:#8bc34a">room.roomState["${escapeHTML(selectedRoomKey)}"]</b>`;
title.style.marginBottom = '12px';
title.style.fontSize = '15px';
contentArea.appendChild(title);
const info = document.createElement('div');
info.style.color = '#ffcc00';
info.style.margin = '12px 0';
info.style.padding = '10px';
info.style.background = '#332200';
info.style.borderRadius = '6px';
info.innerHTML = `
<b>Update notes</b><br>
• Partial update (merge-style)<br>
• Omit fields → they stay unchanged<br>
• Set field to <code>null</code> to delete it<br>
• Objects/arrays replace fully when included
`;
contentArea.appendChild(info);
const tree = document.createElement('div');
tree.style.marginBottom = '20px';
tree.appendChild(createTreeNode(selectedRoomKey, roomState[selectedRoomKey]));
contentArea.appendChild(tree);
const editArea = document.createElement('div');
editArea.style.marginTop = '20px';
const textarea = document.createElement('textarea');
textarea.value = prettyPrintJson(roomState[selectedRoomKey]);
textarea.style.width = '100%';
textarea.style.height = '220px';
textarea.style.background = '#1a1a1a';
textarea.style.color = '#eee';
textarea.style.border = '1px solid #555';
textarea.style.borderRadius = '4px';
textarea.style.fontFamily = 'monospace';
textarea.style.padding = '10px';
editArea.appendChild(textarea);
const saveBtn = document.createElement('button');
saveBtn.textContent = 'Save Changes';
saveBtn.style.marginTop = '12px';
saveBtn.style.background = '#1976d2';
saveBtn.style.color = 'white';
saveBtn.style.padding = '10px 20px';
saveBtn.style.border = 'none';
saveBtn.style.borderRadius = '4px';
saveBtn.onclick = async () => {
try {
const patch = JSON.parse(textarea.value);
if (!confirm(`Update "${selectedRoomKey}"?`)) return;
await window.room.updateRoomState({ [selectedRoomKey]: patch });
await loadRoomState();
} catch (e) {
alert('Error: ' + e.message);
}
};
editArea.appendChild(saveBtn);
contentArea.appendChild(editArea);
}
}
function createTreeNode(key, value, depth = 0) {
const node = document.createElement("div");
node.style.marginLeft = depth ? "20px" : "0";
node.style.lineHeight = "1.6";
if (depth > 35) {
node.textContent = `${escapeHTML(key)}: [too deep]`;
return node;
}
if (value === null) {
node.innerHTML = `<span style="color:#999">${escapeHTML(key)}: null</span>`;
} else if (typeof value === "object" && !Array.isArray(value)) {
const keys = Object.keys(value);
const summary = document.createElement("div");
summary.textContent = `${escapeHTML(key)}: { ${keys.join(", ") || ""} }`;
summary.style.cursor = "pointer"; summary.style.color = "#81d4fa";
summary.style.fontWeight = "bold";
const children = document.createElement("div");
children.style.display = "none";
keys.forEach(k => children.appendChild(createTreeNode(k, value[k], depth + 1)));
summary.onclick = () => {
children.style.display = children.style.display === "none" ? "block" : "none";
};
node.append(summary, children);
} else if (Array.isArray(value)) {
const summary = document.createElement("div");
summary.textContent = `${escapeHTML(key)}: [${value.length}]`;
summary.style.cursor = "pointer"; summary.style.color = "#ffd54f";
summary.style.fontWeight = "bold";
const children = document.createElement("div");
children.style.display = "none";
value.forEach((v, i) => children.appendChild(createTreeNode(i, v, depth + 1)));
summary.onclick = () => {
children.style.display = children.style.display === "none" ? "block" : "none";
};
node.append(summary, children);
} else {
let str = JSON.stringify(value);
if (str.length > 280) str = str.slice(0, 275) + "…";
node.innerHTML = `<span style="color:#ccc">${escapeHTML(key)}:</span> <span style="color:#a5d6a7">${escapeHTML(str)}</span>`;
if (typeof value === "string" && value.startsWith("http")) {
const a = document.createElement("a");
a.href = value; a.target = "_blank"; a.textContent = " ↗";
a.style.color = "#66bb6a"; a.style.marginLeft = "6px";
node.appendChild(a);
}
}
return node;
}
// ── Event handlers ───────────────────────────────────
modeToggle.onchange = () => {
isRoomMode = modeToggle.checked;
if (isRoomMode && !roomState) loadRoomState();
else refreshView();
};
collSelect.onchange = () => {
currentType = collSelect.value;
typeInput.value = currentType;
currentPage = 1;
refreshView();
};
typeInput.onkeydown = e => {
if (e.key === "Enter") {
currentType = typeInput.value.trim();
collSelect.value = currentType || "";
currentPage = 1;
refreshView();
}
};
reloadBtn.onclick = async () => {
if (isRoomMode) await loadRoomState();
else await loadPosts();
};
applyF.onclick = () => refreshView();
clearF.onclick = () => { customFilterBox.value = ""; refreshView(); };
prev.onclick = () => { currentPage--; refreshView(); };
next.onclick = () => { currentPage++; refreshView(); };
createB.onclick = async () => {
if (!currentType) return alert("Set type filter first");
try {
const data = JSON.parse(createTA.value || "{}");
if (!Object.keys(data).length) return alert("Empty");
await getCollection(currentType).create(data);
createTA.value = "";
await loadPosts();
} catch (e) { alert("Bad JSON"); }
};
nukeSection.querySelector("#bomb-go").onclick = async () => {
if (!currentType) return alert("Set type filter first");
const depth = parseInt(nukeSection.querySelector("#bomb-depth").value) || 15000;
const key = nukeSection.querySelector("#bomb-key").value.trim() || "data";
if (!confirm(`Create ${depth}-deep bomb in ${currentType}?`)) return;
let bomb = []; let ptr = bomb;
for (let i = 1; i < depth; i++) ptr = ptr[0] = [];
ptr[0] = "💥 NUKED";
await getCollection(currentType).create({ [key]: bomb });
await loadPosts();
};
// Initial load
setTimeout(loadPosts, 600);
}
// Wait for websim / room
const interval = setInterval(() => {
if (window.websim?.getCurrentProject || window.room?.getRoomState) {
clearInterval(interval);
setTimeout(startUI, 300);
}
}, 400);
setTimeout(startUI, 3000);
})();