The Debug button on a Sieve script sent a copy to an outside site, whose interpreter knows only upstream's extension names: a script that requires vnd.inbuxa.expressions or vnd.inbuxa.while failed to compile there, and the button took admins to another project's site. The playground now ships inside the console and runs the server's own interpreter. sieve-playground/ is built by inbuxa-server's tools/sieve-playground (SOURCE.md names the commit); sieve-playground.ts copies it, with Monaco 0.52.2 from node_modules, into sieve-playground/<content hash>/ (the server caches every file but the console's index.html for good) and gives the console its path. The script stays on the console's origin, so the warning dialog before opening it is gone. Strings (en.json): sievepad.failed reworded, sievepad.debugHint added, the three warning-dialog strings removed. webui.zip grows from 1.3 MB to 3.4 MB: 0.9 MB of compressed WebAssembly, the rest Monaco's editor core.
148 lines
3.0 KiB
JavaScript
148 lines
3.0 KiB
JavaScript
/*
|
|
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
|
* SPDX-FileCopyrightText: 2026 Coffey Labs LLC
|
|
*
|
|
* SPDX-License-Identifier: AGPL-3.0-only
|
|
*
|
|
* Modified by Coffey Labs in 2026 for INBUXA.
|
|
*/
|
|
|
|
const DB_NAME = "inbuxa-sieve-playground";
|
|
const DB_VERSION = 1;
|
|
const STORE = "workspaces";
|
|
const PREFS_KEY = "inbuxa-sieve.prefs";
|
|
const FALLBACK_KEY = "inbuxa-sieve.workspaces";
|
|
const WELCOME_KEY = "inbuxa-sieve.welcome-dismissed";
|
|
|
|
function openDb() {
|
|
return new Promise((resolve, reject) => {
|
|
if (!("indexedDB" in self)) {
|
|
reject(new Error("IndexedDB unavailable"));
|
|
return;
|
|
}
|
|
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
|
request.onupgradeneeded = () => {
|
|
request.result.createObjectStore(STORE, { keyPath: "id" });
|
|
};
|
|
request.onsuccess = () => resolve(request.result);
|
|
request.onerror = () => reject(request.error);
|
|
});
|
|
}
|
|
|
|
function wrap(request) {
|
|
return new Promise((resolve, reject) => {
|
|
request.onsuccess = () => resolve(request.result);
|
|
request.onerror = () => reject(request.error);
|
|
});
|
|
}
|
|
|
|
class IdbStore {
|
|
constructor(db) {
|
|
this.db = db;
|
|
}
|
|
|
|
tx(mode) {
|
|
return this.db.transaction(STORE, mode).objectStore(STORE);
|
|
}
|
|
|
|
list() {
|
|
return wrap(this.tx("readonly").getAll());
|
|
}
|
|
|
|
get(id) {
|
|
return wrap(this.tx("readonly").get(id));
|
|
}
|
|
|
|
put(workspace) {
|
|
return wrap(this.tx("readwrite").put(workspace));
|
|
}
|
|
|
|
delete(id) {
|
|
return wrap(this.tx("readwrite").delete(id));
|
|
}
|
|
}
|
|
|
|
class MemoryStore {
|
|
constructor() {
|
|
this.items = new Map();
|
|
try {
|
|
for (const ws of JSON.parse(localStorage.getItem(FALLBACK_KEY) || "[]")) {
|
|
this.items.set(ws.id, ws);
|
|
}
|
|
} catch (_) {
|
|
this.items.clear();
|
|
}
|
|
}
|
|
|
|
persist() {
|
|
try {
|
|
localStorage.setItem(FALLBACK_KEY, JSON.stringify([...this.items.values()]));
|
|
} catch (_) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
async list() {
|
|
return [...this.items.values()];
|
|
}
|
|
|
|
async get(id) {
|
|
return this.items.get(id);
|
|
}
|
|
|
|
async put(workspace) {
|
|
this.items.set(workspace.id, workspace);
|
|
this.persist();
|
|
}
|
|
|
|
async delete(id) {
|
|
this.items.delete(id);
|
|
this.persist();
|
|
}
|
|
}
|
|
|
|
export async function openStore() {
|
|
try {
|
|
return new IdbStore(await openDb());
|
|
} catch (_) {
|
|
return new MemoryStore();
|
|
}
|
|
}
|
|
|
|
export function loadPrefs() {
|
|
try {
|
|
return JSON.parse(localStorage.getItem(PREFS_KEY) || "{}");
|
|
} catch (_) {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
export function savePrefs(prefs) {
|
|
try {
|
|
localStorage.setItem(PREFS_KEY, JSON.stringify({ ...loadPrefs(), ...prefs }));
|
|
} catch (_) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
export function isWelcomeDismissed() {
|
|
try {
|
|
return localStorage.getItem(WELCOME_KEY) !== null;
|
|
} catch (_) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
export function dismissWelcome() {
|
|
try {
|
|
localStorage.setItem(WELCOME_KEY, new Date().toISOString());
|
|
} catch (_) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
export function newId() {
|
|
if (self.crypto && crypto.randomUUID) return crypto.randomUUID();
|
|
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
}
|