Files
RoleForge/app/static/js/dashboard.js
Sergey Antropoff b2d3b6b803 Профиль и аккаунт
- API и страницы профиля (редактирование, смена пароля, аватар), публичные карточки.
- Сайдбар: блок пользователя, пункт Users для admin/root, исправлен порядок
  инициализации (показ admin-only после initAuthSession, currentUser).
- GET /auth/me: ответ через ProfileMeResponse, исправлена валидация (is_founder bool).

Команды и роли
- Маршруты и UI команд; при редактировании роли: видимость Team, выбор команды
  в модалке, только команды с активным членством; API team_id в details/ update.
- GET /api/v1/teams?membership=active для списка «своих» команд.
- Форма роли: сегмент Team, панель выбора команды только при Team и не при
  с
2026-05-05 08:15:21 +03:00

254 lines
9.4 KiB
JavaScript

const form = document.getElementById("event-form");
const input = document.getElementById("message-input");
const list = document.getElementById("events-list");
const clusterForm = document.getElementById("cluster-config-form");
const clusterFieldsContainer = document.getElementById("cluster-schema-fields");
const clusterOutput = document.getElementById("cluster-form-output");
const addonSelect = document.getElementById("addon-name");
const addonForm = document.getElementById("addon-config-form");
const addonFieldsContainer = document.getElementById("addon-schema-fields");
const addonOutput = document.getElementById("addon-form-output");
const clusterStatsForm = document.getElementById("cluster-stats-form");
const clusterIdInput = document.getElementById("cluster-id-input");
const clusterStatsOutput = document.getElementById("cluster-stats-output");
const protocol = window.location.protocol === "https:" ? "wss" : "ws";
const socket = new WebSocket(`${protocol}://${window.location.host}/ws`);
let clusterStatsSocket = null;
function prependEvent(eventData) {
const li = document.createElement("li");
li.textContent = `[${eventData.created_at}] ${eventData.message}`;
list.prepend(li);
}
async function loadInitialEvents() {
const response = await fetch("/api/events", { method: "GET" });
const events = await response.json();
events.forEach(prependEvent);
}
function buildField(name, schema) {
const wrapper = document.createElement("div");
wrapper.className = "schema-field";
let inputElement;
if (schema.type === "boolean") {
wrapper.classList.add("schema-field--boolean");
const safeId = `field-${String(name).replace(/[^a-zA-Z0-9_-]/g, "_")}`;
inputElement = document.createElement("input");
inputElement.type = "checkbox";
inputElement.className = "form-check-input";
inputElement.id = safeId;
inputElement.name = name;
inputElement.checked = Boolean(schema.default);
const titleLabel = document.createElement("label");
titleLabel.setAttribute("for", safeId);
titleLabel.textContent = name;
if (schema.help) {
const labelRow = document.createElement("div");
labelRow.className = "schema-field-label-row";
labelRow.appendChild(titleLabel);
const tipWrap = document.createElement("div");
tipWrap.className = "schema-field-tip-wrap";
const tipBtn = document.createElement("button");
tipBtn.type = "button";
tipBtn.className = "schema-field-tip";
tipBtn.setAttribute("aria-label", `Hint: ${name}`);
tipBtn.innerHTML = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" width=\"17\" height=\"17\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M12 16v-4\"/><path d=\"M12 8h.01\"/></svg>";
const panel = document.createElement("div");
panel.className = "schema-field-tip-panel";
panel.setAttribute("role", "tooltip");
panel.textContent = schema.help;
tipWrap.appendChild(tipBtn);
tipWrap.appendChild(panel);
labelRow.appendChild(tipWrap);
wrapper.appendChild(labelRow);
} else {
wrapper.appendChild(titleLabel);
}
const swRow = document.createElement("div");
swRow.className = "form-check form-switch";
swRow.appendChild(inputElement);
wrapper.appendChild(swRow);
} else if (schema.type === "select") {
inputElement = document.createElement("select");
(schema.options || []).forEach((optionValue) => {
const option = document.createElement("option");
option.value = optionValue;
option.textContent = optionValue;
if (optionValue === schema.default) {
option.selected = true;
}
inputElement.appendChild(option);
});
} else {
inputElement = document.createElement("input");
inputElement.type = schema.type === "number" ? "number" : "text";
if (schema.default !== undefined && schema.default !== null) {
inputElement.value = String(schema.default);
}
if (schema.validation && schema.validation.min !== undefined) {
inputElement.min = String(schema.validation.min);
}
if (schema.validation && schema.validation.max !== undefined) {
inputElement.max = String(schema.validation.max);
}
if (schema.validation && schema.validation.min_length !== undefined) {
inputElement.minLength = Number(schema.validation.min_length);
}
if (schema.validation && schema.validation.max_length !== undefined) {
inputElement.maxLength = Number(schema.validation.max_length);
}
}
if (schema.type !== "boolean") {
const label = document.createElement("label");
label.setAttribute("for", name);
label.textContent = name;
if (schema.help) {
const row = document.createElement("div");
row.className = "schema-field-label-row";
row.appendChild(label);
const tipWrap = document.createElement("div");
tipWrap.className = "schema-field-tip-wrap";
const tipBtn = document.createElement("button");
tipBtn.type = "button";
tipBtn.className = "schema-field-tip";
tipBtn.setAttribute("aria-label", `Hint: ${name}`);
tipBtn.innerHTML = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" width=\"17\" height=\"17\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M12 16v-4\"/><path d=\"M12 8h.01\"/></svg>";
const panel = document.createElement("div");
panel.className = "schema-field-tip-panel";
panel.setAttribute("role", "tooltip");
panel.textContent = schema.help;
tipWrap.appendChild(tipBtn);
tipWrap.appendChild(panel);
row.appendChild(tipWrap);
wrapper.appendChild(row);
} else {
wrapper.appendChild(label);
}
inputElement.id = name;
inputElement.name = name;
if (schema.validation && schema.validation.required) {
inputElement.required = true;
}
wrapper.appendChild(inputElement);
}
const description = document.createElement("small");
description.className = "field-description";
description.textContent = schema.description || "";
if (description.textContent.trim()) wrapper.appendChild(description);
return wrapper;
}
function renderSchema(container, schema) {
container.innerHTML = "";
Object.entries(schema.fields || {}).forEach(([name, fieldSchema]) => {
container.appendChild(buildField(name, fieldSchema));
});
}
function collectValues(container, schema) {
const values = {};
Object.entries(schema.fields || {}).forEach(([name, fieldSchema]) => {
const element = container.querySelector(`[name="${name}"]`);
if (!element) {
return;
}
if (fieldSchema.type === "boolean") {
values[name] = element.checked;
} else if (fieldSchema.type === "number") {
values[name] = Number(element.value);
} else {
values[name] = element.value;
}
});
return values;
}
let clusterSchema = null;
let addonSchema = null;
async function loadClusterSchema() {
const response = await fetch("/api/schemas/cluster", { method: "GET" });
clusterSchema = await response.json();
renderSchema(clusterFieldsContainer, clusterSchema);
}
async function loadAddonSchema(addon) {
const response = await fetch(`/api/schemas/addons/${encodeURIComponent(addon)}`, { method: "GET" });
addonSchema = await response.json();
renderSchema(addonFieldsContainer, addonSchema);
}
socket.onmessage = (event) => {
const data = JSON.parse(event.data);
prependEvent(data);
};
form.addEventListener("submit", (event) => {
event.preventDefault();
const message = input.value.trim();
if (!message || socket.readyState !== WebSocket.OPEN) {
return;
}
socket.send(message);
input.value = "";
});
clusterForm.addEventListener("submit", (event) => {
event.preventDefault();
if (!clusterSchema) {
return;
}
const payload = collectValues(clusterFieldsContainer, clusterSchema);
clusterOutput.textContent = JSON.stringify(payload, null, 2);
});
addonSelect.addEventListener("change", async (event) => {
await loadAddonSchema(event.target.value);
});
addonForm.addEventListener("submit", (event) => {
event.preventDefault();
if (!addonSchema) {
return;
}
const payload = collectValues(addonFieldsContainer, addonSchema);
addonOutput.textContent = JSON.stringify(payload, null, 2);
});
function connectClusterStats(clusterId) {
if (clusterStatsSocket) {
clusterStatsSocket.close();
}
clusterStatsOutput.textContent = "Connecting...";
clusterStatsSocket = new WebSocket(`${protocol}://${window.location.host}/ws/clusters/${clusterId}/stats`);
clusterStatsSocket.onmessage = (event) => {
const payload = JSON.parse(event.data);
if (payload.event === "cluster.stats") {
clusterStatsOutput.textContent = JSON.stringify(payload.data, null, 2);
} else {
clusterStatsOutput.textContent = JSON.stringify(payload, null, 2);
}
};
clusterStatsSocket.onerror = () => {
clusterStatsOutput.textContent = "WebSocket stats connection error";
};
}
clusterStatsForm.addEventListener("submit", (event) => {
event.preventDefault();
const clusterId = Number(clusterIdInput.value);
if (!clusterId || clusterId < 1) {
return;
}
connectClusterStats(clusterId);
});
loadInitialEvents();
loadClusterSchema();
loadAddonSchema(addonSelect.value);
connectClusterStats(Number(clusterIdInput.value));