import { mountEditor } from "./flow-editor.js";
const app = document.querySelector("#app");
const esc = (v) =>
String(v ?? "").replace(
/[&<>"']/g,
(c) =>
({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[
c
],
);
const paths = {
dashboard: "M3 3h7v7H3z M14 3h7v7h-7z M3 14h7v7H3z M14 14h7v7h-7z",
bot: "M9 3h6 M12 3v3 M5 8h14v12H5z M8 12v3 M16 12v3 M2 11v6 M22 11v6",
flow: "M4 3h6v6H4z M14 15h6v6h-6z M7 9v9h7 M10 6h7v9",
play: "M8 5l12 7-12 7z",
clock: "M12 8v5l3 2 M12 3a9 9 0 1 0 0 18 9 9 0 1 0 0-18",
users:
"M16 21v-3a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v3 M9 3a4 4 0 1 0 0 8 4 4 0 1 0 0-8 M18 4a4 4 0 0 1 0 7 M22 21v-3a4 4 0 0 0-3-4",
link: "M10 14l4-4 M8 16l-1 1a4 4 0 0 1-6-6l4-4a4 4 0 0 1 6 0 M16 8l1-1a4 4 0 0 1 6 6l-4 4a4 4 0 0 1-6 0",
settings:
"M12 8a4 4 0 1 0 0 8 4 4 0 1 0 0-8 M12 2v3 M12 19v3 M2 12h3 M19 12h3 M5 5l2 2 M17 17l2 2 M5 19l2-2 M17 7l2-2",
variable: "M8 3H5v18h3 M16 3h3v18h-3 M10 9l4 6 M14 9l-4 6",
plus: "M12 5v14 M5 12h14",
save: "M4 3h14l3 3v15H3V3z M7 3v6h10V3 M7 21v-8h10v8",
refresh: "M20 7a9 9 0 1 0 1 6 M20 2v5h-5",
check: "M5 12l4 4L19 6",
close: "M6 6l12 12 M18 6L6 18",
shield: "M12 3l8 3v6c0 5-8 9-8 9s-8-4-8-9V6z M8 12l3 3 5-6",
music:
"M9 18V5l11-2v13 M9 18a3 3 0 1 1-6 0 3 3 0 1 1 6 0 M20 16a3 3 0 1 1-6 0 3 3 0 1 1 6 0",
trash: "M3 6h18 M8 6V3h8v3 M5 6l1 15h12l1-15 M10 10v7 M14 10v7",
data: "M3 5c0-4 18-4 18 0s-18 4-18 0z M3 5v14c0 4 18 4 18 0V5 M3 12c0 4 18 4 18 0",
condition: "M12 2l9 10-9 10-9-10z",
text: "M4 5h16 M12 5v15 M8 20h8",
sort: "M4 5h16 M4 12h12 M4 19h8",
channel: "M8 3l-2 18 M18 3l-2 18 M3 8h18 M2 16h18",
expand: "M8 3H3v5 M16 3h5v5 M3 16v5h5 M21 16v5h-5",
};
const icon = (n) =>
` `;
const nodeIcon = (t) =>
({
timer: "clock",
manual: "play",
serverinfo: "data",
clients: "users",
condition: "condition",
sort: "sort",
limit: "dashboard",
template: "text",
channelEdit: "channel",
message: "text",
delay: "clock",
log: "text",
})[t];
let user,
data,
page = "dashboard",
botId,
pending,
dirty = false,
editorView,
executionFilter = { bot: "", status: "", mode: "" },
executionId,
polling = false;
const owner = () => user?.role === "Dono",
dis = () =>
owner() ? "" : 'disabled title="O perfil Leitor permite consultar"';
const selectedBot = () => data.bots.find((b) => b.id === botId);
async function api(path, options = {}) {
const r = await fetch(path, {
...options,
headers: { "Content-Type": "application/json" },
});
const body = await r.json();
if (!r.ok) throw new Error(body.error || "O pedido falhou.");
return body;
}
function toast(text, error = false) {
document.querySelector("#toast")?.remove();
const el = document.createElement("div");
el.id = "toast";
el.className = `toast ${error ? "error" : ""}`;
el.setAttribute("role", "status");
el.textContent = text;
document.body.append(el);
setTimeout(() => el.remove(), 6000);
}
async function refresh() {
data = await api("/api/dashboard");
if (!data.bots.some((b) => b.id === botId)) botId = data.bots[0]?.id;
}
const nav = [
["dashboard", "dashboard", "Dashboard"],
["bots", "bot", "Bots"],
["editor", "flow", "Editor de flows"],
["executions", "clock", "Execuções"],
["variables", "variable", "Variáveis"],
["connections", "link", "Ligações"],
["settings", "settings", "Definições"],
];
const empty = (i, title, text) =>
`
${icon(i)} ${title} ${text}
`;
const button = (id, text, i = "plus", style = "primary") =>
`${icon(i)}${text} `;
const heading = (title, subtitle, actions = "") =>
``;
const ago = (d) =>
!d
? "Sem execuções"
: Math.floor((Date.now() - Date.parse(d)) / 60000) < 1
? "Agora"
: `Há ${Math.floor((Date.now() - Date.parse(d)) / 60000)} min`;
async function boot() {
const me = await api("/api/me");
user = me.user;
if (!user) {
app.innerHTML = `TS6 BOTFLOW O teu centro de bots. Cria flows, gere bots e acompanha as tuas automações num só lugar.
Entrar com Discord ${icon("play")} Acesso reservado às contas autorizadas.
`;
const messages = {
denied: "Esta conta não está autorizada.",
authentication: "O login expirou ou foi cancelado.",
configuration: "Configura a aplicação Discord no Docker.",
};
const msg =
messages[new URLSearchParams(location.search).get("error")] ||
(!me.configured && messages.configuration);
if (msg) {
const el = document.querySelector("#notice");
el.className = "notice";
el.textContent = msg;
}
return;
}
await refresh();
app.innerHTML = `XanaX · TS6 Botflow O teu centro de automações `;
document
.querySelectorAll("[data-page]")
.forEach((b) => (b.onclick = () => navigate(b.dataset.page)));
document.querySelector(".brand").onclick = (e) => {
e.preventDefault();
navigate("dashboard");
};
document.querySelector("#logout").onclick = async () => {
await api("/auth/logout", { method: "POST" });
location.assign("/");
};
document.querySelector("#server-select").onchange = () =>
navigate("connections");
navigate(location.hash.slice(1));
}
function navigate(name) {
if (dirty && !confirm("Sair sem guardar as alterações do flow?")) return;
dirty = false;
pending = null;
page = nav.some(([id]) => id === name) ? name : "dashboard";
location.hash = page;
document
.querySelectorAll(".nav")
.forEach((b) => b.classList.toggle("active", b.dataset.page === page));
document.querySelector("#page-label").textContent = nav.find(
([id]) => id === page,
)[2];
document
.querySelector(".workspace")
.classList.toggle("editor-workspace", page === "editor");
({
dashboard: dashboardPage,
bots: botsPage,
editor: editorPage,
executions: executionsPage,
variables: variablesPage,
connections: connectionsPage,
settings: settingsPage,
})[page]();
wireExecutions();
updateServerLabel();
}
function botCard(bot) {
const runs = data.executions.filter((e) => e.botId === bot.id);
return `${icon(bot.color === "red" ? "shield" : bot.color === "orange" ? "music" : bot.color === "cyan" ? "users" : "bot")}
${esc(bot.name)} ${bot.enabled ? "Ativo" : "Rascunho"} ${esc(bot.description || "Personaliza o flow deste bot.")}
${icon("flow")}1 flow ${icon("play")}${runs.length} execuções ${icon("clock")}${ago(runs[0]?.startedAt)}
${icon("flow")}Abrir flow ${icon("settings")}
`;
}
function wireBots() {
document.querySelectorAll("[data-open]").forEach(
(b) =>
(b.onclick = () => {
botId = b.dataset.open;
navigate("editor");
}),
);
document
.querySelectorAll("[data-config]")
.forEach((b) => (b.onclick = () => configureBot(b.dataset.config)));
const create = document.querySelector("#create-bot");
if (create) create.onclick = () => newBot();
document.querySelectorAll("[data-toggle]").forEach(
(b) =>
(b.onclick = async () => {
try {
const bot = data.bots.find((bot) => bot.id === b.dataset.toggle);
await api(`/api/bots/${bot.id}`, {
method: "PUT",
body: JSON.stringify({ enabled: !bot.enabled }),
});
await refresh();
navigate(page);
toast("Estado atualizado.");
} catch (e) {
toast(e.message, true);
}
}),
);
}
function dashboardPage() {
const today = new Date().toLocaleDateString("pt-PT", {
timeZone: "Europe/Lisbon",
});
document.querySelector("#content").innerHTML =
heading(
"Dashboard",
"Uma visão geral dos teus bots e automações.",
button("create-bot", "Criar bot"),
) +
`${[
[
"Bots ativos",
data.bots.filter((b) => b.enabled).length,
"bot",
"green",
],
[
"Execuções hoje",
data.executions.filter(
(e) =>
new Date(e.startedAt).toLocaleDateString("pt-PT", {
timeZone: "Europe/Lisbon",
}) === today,
).length,
"play",
"cyan",
],
["Flows guardados", data.bots.length, "flow", "purple"],
["Servidores configurados", data.connections.length, "link", "orange"],
]
.map(
([label, n, i, c]) =>
`
`,
)
.join(
"",
)}
Os teus bots Ver todos → ${data.bots.map(botCard).join("")}
Atividade recente Ver execuções → ${data.executions.slice(0, 4).map(runRow).join("") || empty("clock", "Ainda sem execuções", "Simula um flow para acompanhar aqui cada passo.")}${icon("flow")} O teu primeiro flow O rascunho Estatísticas prepara a descrição do canal 21. Escolhe a ligação e simula antes de ativar.
${icon("flow")}Abrir editor `;
wireBots();
document.querySelector("#all-bots").onclick = () => navigate("bots");
document.querySelector("#all-runs").onclick = () => navigate("executions");
document.querySelector("#first-flow").onclick = () => navigate("editor");
}
function botsPage() {
document.querySelector("#content").innerHTML =
heading(
"Bots",
"Gere os teus bots do TeamSpeak num só lugar.",
button("import-bot", "Importar", "flow", "secondary") +
button("create-bot", "Criar bot"),
) +
`${data.bots.length} bots · ${data.bots.filter((b) => b.enabled).length} ativos
${data.bots.map(botCard).join("")}
Templates Flows prontos para configurar ${data.templates.map((t) => `
${icon("flow")} ${esc(t.name)} ${esc(t.description)}
Criar bot → `).join("")}
`;
wireBots();
document.querySelector("#search-bots").oninput = (e) => {
document.querySelector("#bot-list").innerHTML = data.bots
.filter((b) =>
b.name.toLowerCase().includes(e.target.value.toLowerCase()),
)
.map(botCard)
.join("");
wireBots();
};
document
.querySelectorAll("[data-template]")
.forEach((b) => (b.onclick = () => newBot(b.dataset.template)));
document.querySelector("#import-bot").onclick = () =>
document.querySelector("#bot-file").click();
document.querySelector("#bot-file").onchange = async (e) => {
const file = e.target.files[0];
if (!file) return;
try {
if (file.size > 500000)
throw new Error("O ficheiro deve ter menos de 500 KB.");
const exported = JSON.parse(await file.text());
if (exported.format !== "ts6-botflow/v1" || !exported.bot)
throw new Error("Usa um ficheiro exportado pelo Botflow.");
const bot = await api("/api/bots", {
method: "POST",
body: JSON.stringify({ import: exported.bot }),
});
botId = bot.id;
await refresh();
navigate("editor");
toast(
"Bot importado como rascunho. Configura a ligação e os campos secretos.",
);
} catch (error) {
toast(error.message, true);
} finally {
e.target.value = "";
}
};
}
function modal(title, body, submit) {
const dialog = document.querySelector("#dialog");
dialog.innerHTML = ``;
dialog.showModal();
document.querySelector("#close-dialog").onclick = document.querySelector(
"#cancel",
).onclick = () => dialog.close();
document.querySelector("#modal-form").onsubmit = async (e) => {
e.preventDefault();
try {
await submit(new FormData(e.target));
dialog.close();
await refresh();
dirty = false;
navigate(page);
toast("Guardado.");
} catch (error) {
toast(error.message, true);
}
};
}
function newBot(templateId = "custom") {
const template =
data.templates.find((t) => t.id === templateId) ||
data.templates.find((t) => t.id === "custom");
modal(
"Criar bot",
`Nome Template${data.templates.map((t) => `${esc(t.name)} `).join("")} Descrição Cor${["purple", "green", "red", "cyan", "orange"].map((c) => `${c} `).join("")} O bot começa desativado. Configura os blocos e escolhe o servidor no editor.
`,
async (form) => {
const bot = await api("/api/bots", {
method: "POST",
body: JSON.stringify(Object.fromEntries(form)),
});
botId = bot.id;
page = "editor";
},
);
}
function configureBot(id) {
botId = id;
const bot = selectedBot();
modal(
"Configuração do bot",
`Nome Descrição ServidorEscolher ligação ${data.connections.map((c) => `${esc(c.name)} `).join("")} Limite por execução (10–600 segundos) ${icon("plus")}Duplicar ${icon("save")}Exportar ${icon("trash")}
A exportação omite a ligação e os campos secretos dos blocos.
`,
(f) =>
api(`/api/bots/${bot.id}`, {
method: "PUT",
body: JSON.stringify(Object.fromEntries(f)),
}),
);
document.querySelector("#duplicate-bot").onclick = async () => {
try {
const copy = await api(`/api/bots/${bot.id}/duplicate`, {
method: "POST",
});
botId = copy.id;
document.querySelector("#dialog").close();
await refresh();
navigate("bots");
toast("Cópia criada, desativada.");
} catch (error) {
toast(error.message, true);
}
};
document.querySelector("#export-bot").onclick = async () => {
try {
const body = await api(`/api/bots/${bot.id}/export`, { method: "POST" }),
url = URL.createObjectURL(
new Blob([JSON.stringify(body, null, 2)], {
type: "application/json",
}),
),
a = document.createElement("a");
a.href = url;
a.download = `${bot.name.replace(/[^a-zA-Z0-9_-]/g, "_")}.json`;
a.click();
setTimeout(() => URL.revokeObjectURL(url), 1000);
toast("Bot exportado.");
} catch (error) {
toast(error.message, true);
}
};
document.querySelector("#delete-bot").onclick = async () => {
if (!confirm(`Apagar o bot «${bot.name}»?`)) return;
try {
await api(`/api/bots/${bot.id}`, { method: "DELETE" });
document.querySelector("#dialog").close();
await refresh();
navigate("bots");
toast("Bot apagado.");
} catch (error) {
toast(error.message, true);
}
};
}
function editorPage() {
const bot = selectedBot();
if (!bot) {
document.querySelector("#content").innerHTML =
heading(
"Editor de flows",
"Cria um bot para começar.",
button("create-bot", "Criar bot"),
) + empty("flow", "Ainda sem flows", "Cria o teu primeiro bot.");
wireBots();
return;
}
editorView = mountEditor(bot, {
api,
data,
owner,
dis,
icon,
esc,
heading,
toast,
refresh,
showRun,
onDirty: (value) => {
dirty = value;
updateServerLabel();
},
onBot: (id) => {
if (dirty && !confirm("Sair sem guardar o flow?")) {
document.querySelector("#bot-select").value = botId;
return;
}
botId = id;
dirty = false;
editorPage();
updateServerLabel();
},
});
}
const statuses = {
success: ["green", "check", "Concluída"],
error: ["red", "close", "Falhou"],
warning: ["orange", "shield", "Erro tratado"],
running: ["cyan", "clock", "Em execução"],
cancelled: ["gray", "close", "Cancelada"],
};
function runRow(e) {
const [color, i, label] = statuses[e.status] || statuses.error;
return `${icon(i)} ${esc(e.botName)} ${e.simulation ? "Simulação" : "Execução real"} · ${label} ${ago(e.startedAt)} `;
}
function runDetail(e) {
const [color, , label] = statuses[e.status] || statuses.error;
return `
${esc(e.botName)} ${label} ${new Date(e.startedAt).toLocaleString("pt-PT")} · ${e.simulation ? "Simulação com dados de exemplo" : "Servidor TeamSpeak"} Duração ${((e.durationMs || 0) / 1000).toFixed(2)}s
Iniciado por ${esc(e.actor)}
Passos ${e.steps.length}
${e.status === "running" ? `Cancelar execução ` : ""}Eventos e etapas ${e.steps
.map((s) => {
const [c, i] = statuses[s.status] || statuses.error;
return `
${icon(i)} ${esc(s.label)} ${esc(s.type)}${s.attempts > 1 ? ` · ${s.attempts} tentativas` : ""} ${esc(s.message || "A executar…")}
${s.durationMs || 0} ms `;
})
.join(
"",
)}
${e.error ? `${esc(e.error)}
` : ""}${e.output ? `Texto produzido ${esc(e.output)} ` : ""}`;
}
function showRun(e) {
const dialog = document.querySelector("#dialog");
dialog.innerHTML = `
${e.simulation ? "Resultado da simulação" : "Resultado da execução"} ${icon("close")} ${runDetail(e)}`;
dialog.showModal();
wireCancel();
document.querySelector("#close-run").onclick = () => dialog.close();
}
function wireExecutions() {
document.querySelectorAll("[data-execution]").forEach(
(b) =>
(b.onclick = () => {
executionId = b.dataset.execution;
const e = data.executions.find((e) => e.id === executionId),
el = document.querySelector("#execution-detail");
if (el) {
el.innerHTML = runDetail(e);
document
.querySelectorAll(".execution-row")
.forEach((row) => row.classList.toggle("selected", row === b));
wireCancel();
} else showRun(e);
}),
);
wireCancel();
}
function wireCancel() {
document.querySelectorAll("[data-cancel-execution]").forEach(
(b) =>
(b.onclick = async () => {
try {
await api(`/api/bots/${b.dataset.cancelExecution}/cancel`, {
method: "POST",
});
toast("Cancelamento solicitado.");
} catch (error) {
toast(error.message, true);
}
}),
);
}
function executionsPage() {
const filtered = data.executions.filter(
(e) =>
(!executionFilter.bot || e.botId === executionFilter.bot) &&
(!executionFilter.status || e.status === executionFilter.status) &&
(!executionFilter.mode || String(e.simulation) === executionFilter.mode),
);
if (!filtered.some((e) => e.id === executionId))
executionId = filtered[0]?.id;
document.querySelector("#content").innerHTML =
heading(
"Execuções",
"Histórico por passo, com atualização automática a cada 3 segundos.",
`${icon("refresh")}Atualizar `,
) +
`Todos os bots ${data.bots.map((b) => `${esc(b.name)} `).join("")}Todos os estados ${Object.entries(
statuses,
)
.map(
([key, [, , label]]) =>
`${label} `,
)
.join(
"",
)}Todos os modos Execução real Simulação
Execuções recentes ${filtered.length} ${filtered.map(runRow).join("") || empty("clock", "Sem resultados", "Escolhe outro filtro ou simula um flow.")}${filtered.length ? runDetail(filtered.find((e) => e.id === executionId)) : empty("flow", "Cada passo, num só lugar", "Os resultados ficam guardados aqui.")} `;
for (const [name, key] of [
["bot", "bot"],
["status", "status"],
["mode", "mode"],
])
document.querySelector(`#execution-${name}`).onchange = (e) => {
executionFilter[key] = e.target.value;
executionsPage();
wireExecutions();
};
document.querySelector("#refresh-runs").onclick = async () => {
try {
await refresh();
executionsPage();
wireExecutions();
} catch (error) {
toast(error.message, true);
}
};
}
const variableRow = (k, v) => {
const kind =
(v !== null && typeof v === "object") || v === null
? "json"
: typeof v === "number"
? "number"
: typeof v === "boolean"
? "boolean"
: "text";
return `${["text", "number", "boolean", "json"].map((t) => `${{ text: "Texto", number: "Número", boolean: "Booleano", json: "JSON" }[t]} `).join("")} ${icon("trash")}
`;
};
function variablesPage() {
const bot = selectedBot();
document.querySelector("#content").innerHTML =
heading(
"Variáveis",
"Valores persistentes para reutilizar nos teus flows.",
`${data.bots.map((b) => `${esc(b.name)} `).join("")} `,
) +
`
Variáveis do bot ${button("add-variable", "Adicionar variável", "plus", "secondary")}Usa {{variables.nome}} no texto de um bloco.
`;
const wire = () =>
document
.querySelectorAll("[data-remove-var]")
.forEach((b) => (b.onclick = () => b.closest(".variable-row").remove()));
wire();
document.querySelector("#variables-bot").onchange = (e) => {
botId = e.target.value;
variablesPage();
};
document.querySelector("#add-variable").onclick = () => {
document
.querySelector("#variable-rows")
.insertAdjacentHTML("beforeend", variableRow("", ""));
wire();
};
document.querySelector("#variables-form").onsubmit = async (e) => {
e.preventDefault();
if (!bot) return;
try {
const variables = Object.create(null);
document.querySelectorAll(".variable-row").forEach((row) => {
const key = row.querySelector("[data-key]").value.trim();
if (
!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key) ||
Object.hasOwn(variables, key)
)
throw new Error("Usa nomes únicos com letras, números e underscore.");
const value = row.querySelector("[data-value]").value,
type = row.querySelector("[data-value-type]").value;
if (type === "number") {
if (value.trim() === "" || !Number.isFinite(Number(value)))
throw new Error("Indica um número válido.");
variables[key] = Number(value);
} else if (type === "json") variables[key] = JSON.parse(value);
else if (type === "boolean") {
if (!["true", "false"].includes(value))
throw new Error("Usa true ou false para booleanos.");
variables[key] = value === "true";
} else variables[key] = value;
});
await api(`/api/bots/${bot.id}`, {
method: "PUT",
body: JSON.stringify({ variables }),
});
await refresh();
toast("Variáveis guardadas.");
} catch (error) {
toast(error.message, true);
}
};
}
function connectionsPage() {
document.querySelector("#content").innerHTML =
heading(
"Ligações",
"WebQuery para ações e SSH Query para eventos e comandos.",
button("new-connection", "Adicionar servidor", "link"),
) +
`${
data.connections
.map((c) => {
const states = c.eventState || [],
connected = states.filter((s) => s.status === "connected").length;
return `
${icon("link")}
${esc(c.name)} WebQuery · Servidor #${c.serverId} ${esc(c.url)}
Eventos SSH ${c.eventsEnabled ? (states.length ? `${connected} / ${states.length} escutas ligadas` : "À espera de um bot ativo") : "Desativados"}
${c.eventsEnabled ? `
Destino SSH ${esc(c.sshHost)}:${c.sshPort}
` : ""}
${states
.filter((s) => s.error)
.map((s) => `${esc(s.error)}
`)
.join(
"",
)}${icon("settings")}Configurar ${icon("refresh")}Testar WebQuery
`;
})
.join("") ||
`
${empty("link", "Liga o teu TeamSpeak", "Adiciona o endereço WebQuery e a API key.")} `
}
Os eventos usam a porta SSH Query do TeamSpeak (habitualmente 10022). O nome teamspeak6 funciona quando os containers partilham a rede Docker.
`;
document.querySelector("#new-connection").onclick = () => connectionModal();
document
.querySelectorAll("[data-edit-connection]")
.forEach(
(b) =>
(b.onclick = () =>
connectionModal(
data.connections.find((c) => c.id === b.dataset.editConnection),
)),
);
document.querySelectorAll("[data-test-connection]").forEach(
(b) =>
(b.onclick = async () => {
b.disabled = true;
try {
const result = await api(
`/api/connections/${b.dataset.testConnection}/test`,
{ method: "POST" },
);
toast(
`Ligação confirmada: ${result.server.virtualserver_name || "TeamSpeak"}`,
);
} catch (e) {
toast(e.message, true);
} finally {
b.disabled = !owner();
}
}),
);
}
function connectionModal(c = {}) {
modal(
c.id ? "Configurar servidor" : "Adicionar servidor",
`Nome Endereço WebQuery ID do servidor virtual API key EVENTOS E COMANDOS Escuta SSH QueryDesativada Ativada Host SSH Porta SSH Query
Utilizador SSH Query Password SSH Query Identidade do servidor SSH Impressão digital SHA-256 (hex) A primeira ligação guarda a impressão digital. Se o servidor mudar de chave, confirma a nova chave e atualiza este campo.
As credenciais são guardadas cifradas. A escuta inicia quando ativares um bot com eventos ou comandos.
${c.id ? `Apagar ligação ` : ""}`,
(f) =>
api("/api/connections", {
method: "POST",
body: JSON.stringify({ ...Object.fromEntries(f), id: c.id }),
}),
);
if (c.id)
document.querySelector("#delete-connection").onclick = async () => {
if (!confirm("Apagar esta ligação?")) return;
try {
await api(`/api/connections/${c.id}`, { method: "DELETE" });
document.querySelector("#dialog").close();
await refresh();
navigate("connections");
toast("Ligação apagada.");
} catch (error) {
toast(error.message, true);
}
};
}
function settingsPage() {
document.querySelector("#content").innerHTML =
heading("Definições", "A tua conta e o ambiente do painel.") +
`O teu acesso ${esc(user.name.slice(0, 1).toUpperCase())}
${esc(user.name)} ${esc(user.role)} ID Discord ${esc(user.id)}
Autenticação Discord
Permissões ${owner() ? "Configurar, guardar e executar" : "Consultar bots, flows e execuções"}
Ambiente Blocos disponíveis ${Object.keys(data.nodeTypes).length}
Motor WebQuery + eventos SSH
Painel TS6 Botflow 0.3
Dados Volume Docker persistente
Novas contas Apenas IDs autorizados
A tua conta Discord e as ligações TeamSpeak têm permissões separadas.
`;
}
boot().catch((e) => {
app.innerHTML = ``;
});
window.addEventListener("beforeunload", (e) => {
if (dirty) {
e.preventDefault();
e.returnValue = "";
}
});
function updateServerLabel() {
const select = document.querySelector("#server-select");
if (!select) return;
const connectionId =
page === "editor"
? editorView?.draft.connectionId
: selectedBot()?.connectionId;
select.innerHTML =
'Sem servidor escolhido ' +
data.connections
.map(
(c) =>
`${esc(c.name)} `,
)
.join("");
}
window.addEventListener("keydown", (e) => {
if (page === "editor" && !document.querySelector("dialog[open]"))
editorView?.keyboard(e);
});
setInterval(async () => {
if (
!user ||
polling ||
document.hidden ||
document.querySelector("dialog[open]") ||
!["executions", "connections"].includes(page)
)
return;
polling = true;
try {
await refresh();
if (page === "executions") {
executionsPage();
wireExecutions();
}
if (page === "connections") connectionsPage();
updateServerLabel();
} catch {
} finally {
polling = false;
}
}, 3000);