Handle unloaded site workspace in CMS
This commit is contained in:
parent
33ab11ab08
commit
dc49acaa43
|
|
@ -13,6 +13,10 @@
|
||||||
|
|
||||||
Пароль хостинга не хранится в JSON. В конфиге указывается имя переменной окружения, например `MCHOST_PASSWORD`.
|
Пароль хостинга не хранится в JSON. В конфиге указывается имя переменной окружения, например `MCHOST_PASSWORD`.
|
||||||
|
|
||||||
|
CMS редактирует локальный workspace сайта, а внешний хостинг является целью публикации. На Synology текущий NODE.DC workspace монтируется в контейнер как `/sites/nodedc` и физически лежит в `/volume1/docker/dc-cms/sites/nodedc`.
|
||||||
|
|
||||||
|
Если workspace ещё не загружен, админка открывается без вкладок сайта и показывает состояние подключения. После загрузки `content/`, `templates/`, `tools/` и нужных assets появляются режимы администрирования, SEO и Knowledge.
|
||||||
|
|
||||||
## Локальный запуск
|
## Локальный запуск
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
|
||||||
|
|
@ -368,6 +368,48 @@ body[data-file-picker-active="true"] .editor {
|
||||||
stroke-linejoin: round;
|
stroke-linejoin: round;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.workspace-card-actions {
|
||||||
|
display: inline-flex;
|
||||||
|
margin-left: auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.34rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-action-button {
|
||||||
|
display: grid;
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
place-items: center;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.16);
|
||||||
|
border-radius: var(--launcher-radius-circle);
|
||||||
|
background: rgba(255, 255, 255, 0.055);
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
font-size: 1.04rem;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-action-button svg {
|
||||||
|
width: 0.92rem;
|
||||||
|
height: 0.92rem;
|
||||||
|
fill: none;
|
||||||
|
stroke: currentColor;
|
||||||
|
stroke-width: 1.8;
|
||||||
|
stroke-linecap: round;
|
||||||
|
stroke-linejoin: round;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-action-button--plus {
|
||||||
|
font-size: 1.18rem;
|
||||||
|
font-weight: 650;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-action-button:hover {
|
||||||
|
border-color: rgba(255, 255, 255, 0.3);
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
.admin-panel-close:hover {
|
.admin-panel-close:hover {
|
||||||
border-color: rgba(255, 255, 255, 0.3);
|
border-color: rgba(255, 255, 255, 0.3);
|
||||||
background: rgba(255, 255, 255, 0.07);
|
background: rgba(255, 255, 255, 0.07);
|
||||||
|
|
|
||||||
117
admin/admin.js
117
admin/admin.js
|
|
@ -150,6 +150,7 @@ const el = {
|
||||||
modeContent: document.querySelector("#admin-mode-content"),
|
modeContent: document.querySelector("#admin-mode-content"),
|
||||||
modeSeo: document.querySelector("#admin-mode-seo"),
|
modeSeo: document.querySelector("#admin-mode-seo"),
|
||||||
modeKnowledge: document.querySelector("#admin-mode-knowledge"),
|
modeKnowledge: document.querySelector("#admin-mode-knowledge"),
|
||||||
|
modeNav: document.querySelector(".nodedc-expanded-nav-group"),
|
||||||
sidebarTitle: document.querySelector("#sidebar-title"),
|
sidebarTitle: document.querySelector("#sidebar-title"),
|
||||||
workspaceName: document.querySelector("#workspace-name"),
|
workspaceName: document.querySelector("#workspace-name"),
|
||||||
workspaceRole: document.querySelector("#workspace-role"),
|
workspaceRole: document.querySelector("#workspace-role"),
|
||||||
|
|
@ -158,6 +159,7 @@ const el = {
|
||||||
panelTitle: document.querySelector("#content-panel-title"),
|
panelTitle: document.querySelector("#content-panel-title"),
|
||||||
panelNote: document.querySelector("#content-panel-note"),
|
panelNote: document.querySelector("#content-panel-note"),
|
||||||
regions: document.querySelector("#regions"),
|
regions: document.querySelector("#regions"),
|
||||||
|
addProject: document.querySelector("#add-project"),
|
||||||
projectSettings: document.querySelector("#project-settings"),
|
projectSettings: document.querySelector("#project-settings"),
|
||||||
addBlock: document.querySelector("#add-block"),
|
addBlock: document.querySelector("#add-block"),
|
||||||
reload: document.querySelector("#reload"),
|
reload: document.querySelector("#reload"),
|
||||||
|
|
@ -582,6 +584,14 @@ function projectRole() {
|
||||||
return state.projectConfig?.role || "Администратор сайта";
|
return state.projectConfig?.role || "Администратор сайта";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function siteStatus() {
|
||||||
|
return state.projectConfig?.site || { ready: false, missingFiles: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
function siteReady() {
|
||||||
|
return Boolean(siteStatus().ready);
|
||||||
|
}
|
||||||
|
|
||||||
function applyProjectConfigSurface() {
|
function applyProjectConfigSurface() {
|
||||||
if (el.workspaceName) el.workspaceName.textContent = projectName();
|
if (el.workspaceName) el.workspaceName.textContent = projectName();
|
||||||
if (el.workspaceRole && !isProjectSettingsSelected()) {
|
if (el.workspaceRole && !isProjectSettingsSelected()) {
|
||||||
|
|
@ -693,6 +703,13 @@ function ensureSeoModel() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function setAdminMode(nextMode) {
|
function setAdminMode(nextMode) {
|
||||||
|
if (!siteReady() && !isProjectSettingsSelected()) {
|
||||||
|
state.mode = "content";
|
||||||
|
updateAdminLayoutState({ mode: state.mode });
|
||||||
|
renderSiteUnavailable();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const mode = nextMode === "seo" || nextMode === "knowledge" ? nextMode : "content";
|
const mode = nextMode === "seo" || nextMode === "knowledge" ? nextMode : "content";
|
||||||
state.mode = mode;
|
state.mode = mode;
|
||||||
if (isProjectSettingsSelected()) {
|
if (isProjectSettingsSelected()) {
|
||||||
|
|
@ -714,8 +731,10 @@ function setAdminMode(nextMode) {
|
||||||
function updateAdminModeSurface() {
|
function updateAdminModeSurface() {
|
||||||
const isSeo = state.mode === "seo";
|
const isSeo = state.mode === "seo";
|
||||||
const isKnowledge = state.mode === "knowledge";
|
const isKnowledge = state.mode === "knowledge";
|
||||||
|
const hasSite = siteReady();
|
||||||
document.body.dataset.adminMode = state.mode;
|
document.body.dataset.adminMode = state.mode;
|
||||||
|
|
||||||
|
el.modeNav?.classList.toggle("hidden", !hasSite);
|
||||||
if (el.modeContent) el.modeContent.dataset.active = String(!isSeo && !isKnowledge);
|
if (el.modeContent) el.modeContent.dataset.active = String(!isSeo && !isKnowledge);
|
||||||
if (el.modeSeo) el.modeSeo.dataset.active = String(isSeo);
|
if (el.modeSeo) el.modeSeo.dataset.active = String(isSeo);
|
||||||
if (el.modeKnowledge) el.modeKnowledge.dataset.active = String(isKnowledge);
|
if (el.modeKnowledge) el.modeKnowledge.dataset.active = String(isKnowledge);
|
||||||
|
|
@ -736,14 +755,24 @@ function updateAdminModeSurface() {
|
||||||
if (el.addBlock) {
|
if (el.addBlock) {
|
||||||
el.addBlock.title = isKnowledge ? "Добавить раздел в корень базы знаний" : "Добавить блок";
|
el.addBlock.title = isKnowledge ? "Добавить раздел в корень базы знаний" : "Добавить блок";
|
||||||
el.addBlock.setAttribute("aria-label", el.addBlock.title);
|
el.addBlock.setAttribute("aria-label", el.addBlock.title);
|
||||||
|
el.addBlock.disabled = !hasSite;
|
||||||
|
}
|
||||||
|
if (el.addProject) {
|
||||||
|
el.addProject.title = "Добавить сайт";
|
||||||
|
el.addProject.setAttribute("aria-label", "Добавить сайт");
|
||||||
}
|
}
|
||||||
if (el.save) {
|
if (el.save) {
|
||||||
el.save.textContent = isKnowledge ? "Сохранить базу" : "Сохранить модель";
|
el.save.textContent = "Сохранить";
|
||||||
el.save.title = isKnowledge ? "Записать дерево в content/knowledge.json" : "Записать текущую модель в content/pages/home.json";
|
el.save.title = isKnowledge ? "Записать дерево в content/knowledge.json" : "Записать текущую модель в content/pages/home.json";
|
||||||
|
el.save.disabled = !hasSite && !isProjectSettingsSelected();
|
||||||
}
|
}
|
||||||
if (el.render) {
|
if (el.render) {
|
||||||
el.render.textContent = isKnowledge ? "Обновить /knowledge/" : "Обновить index.html";
|
el.render.textContent = "Опубликовать";
|
||||||
el.render.title = isKnowledge ? "Сохранить дерево и пересобрать публичный раздел /knowledge/" : "Сохранить модель и пересобрать публичный index.html";
|
el.render.title = isKnowledge ? "Сохранить дерево и пересобрать публичный раздел /knowledge/" : "Сохранить модель и пересобрать публичный index.html";
|
||||||
|
el.render.disabled = !hasSite && !isProjectSettingsSelected();
|
||||||
|
}
|
||||||
|
if (el.reload) {
|
||||||
|
el.reload.disabled = !hasSite && !isProjectSettingsSelected();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1796,7 +1825,7 @@ function openTemplateModal() {
|
||||||
|
|
||||||
function closeTemplateModal() {
|
function closeTemplateModal() {
|
||||||
el.templateModal.classList.add("hidden");
|
el.templateModal.classList.add("hidden");
|
||||||
el.addBlock.focus();
|
el.addBlock?.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeDeleteModal(confirmed = false) {
|
function closeDeleteModal(confirmed = false) {
|
||||||
|
|
@ -1996,6 +2025,43 @@ function renderProjectRuntimeLine(label, value) {
|
||||||
return row;
|
return row;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderEmptyState({ icon = "ND", title, body, stateName = "" }) {
|
||||||
|
el.empty.dataset.state = stateName;
|
||||||
|
el.empty.innerHTML = `
|
||||||
|
<div class="empty-icon">${escapeHtml(icon)}</div>
|
||||||
|
<div>
|
||||||
|
<h3>${escapeHtml(title)}</h3>
|
||||||
|
<p>${escapeHtml(body)}</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSiteUnavailable() {
|
||||||
|
updateAdminModeSurface();
|
||||||
|
applyProjectConfigSurface();
|
||||||
|
el.regions.innerHTML = "";
|
||||||
|
el.form.classList.add("hidden");
|
||||||
|
el.knowledgeForm.classList.add("hidden");
|
||||||
|
el.projectSettingsForm.classList.add("hidden");
|
||||||
|
el.empty.classList.remove("hidden");
|
||||||
|
|
||||||
|
const missing = siteStatus().missingFiles || [];
|
||||||
|
const missingText = missing.length ? ` Сейчас отсутствуют: ${missing.join(", ")}.` : "";
|
||||||
|
const root = siteStatus().root || state.projectConfig?.runtime?.siteRoot || "";
|
||||||
|
const rootText = root ? ` Локальная папка: ${root}.` : "";
|
||||||
|
|
||||||
|
renderEmptyState({
|
||||||
|
icon: "DC",
|
||||||
|
title: "Сайт недоступен",
|
||||||
|
body: `Проверьте настройки подключения к хостингу или загрузите архив сайта в локальный workspace CMS.${missingText}${rootText}`,
|
||||||
|
stateName: "site-unavailable",
|
||||||
|
});
|
||||||
|
if (el.editorHeading) el.editorHeading.textContent = "DC CMS";
|
||||||
|
el.selectedRegion.textContent = `DC CMS / ${projectName()} / Подключение`;
|
||||||
|
el.selectedTitle.textContent = "Подключите сайт";
|
||||||
|
setDirty(false);
|
||||||
|
}
|
||||||
|
|
||||||
function renderProjectSettings() {
|
function renderProjectSettings() {
|
||||||
updateAdminModeSurface();
|
updateAdminModeSurface();
|
||||||
el.empty.classList.add("hidden");
|
el.empty.classList.add("hidden");
|
||||||
|
|
@ -2038,17 +2104,17 @@ function renderProjectSettings() {
|
||||||
|
|
||||||
const deployCard = createProjectSettingsCard({
|
const deployCard = createProjectSettingsCard({
|
||||||
kicker: "Хостинг",
|
kicker: "Хостинг",
|
||||||
title: "Публикация на McHost / Synology",
|
title: "Публикация на McHost",
|
||||||
note: "Пароль не пишем в JSON. В поле passwordEnv указывается имя переменной окружения сервиса, где будет лежать пароль.",
|
note: "Для McHost обычно используются FTP host вида aXXXX.ftp.mchost.ru, порт 21 для FTP или 22 для SFTP и папка сайта httpdocs. Пароль не пишем в JSON: passwordEnv хранит имя переменной окружения сервиса.",
|
||||||
fields: [
|
fields: [
|
||||||
createProjectField({ path: "deploy.enabled", label: "Включить деплой", type: "checkbox" }),
|
createProjectField({ path: "deploy.enabled", label: "Включить деплой", type: "checkbox" }),
|
||||||
createProjectField({ path: "deploy.deployOnRender", label: "Деплоить после рендера", type: "checkbox" }),
|
createProjectField({ path: "deploy.deployOnRender", label: "Деплоить после рендера", type: "checkbox" }),
|
||||||
createProjectField({ path: "deploy.method", label: "Метод", options: ["sftp", "ftp"] }),
|
createProjectField({ path: "deploy.method", label: "Метод", options: ["sftp", "ftp"] }),
|
||||||
createProjectField({ path: "deploy.host", label: "Хост", placeholder: "server.mchost.ru" }),
|
createProjectField({ path: "deploy.host", label: "FTP / SFTP host", placeholder: "a0000.ftp.mchost.ru" }),
|
||||||
createProjectField({ path: "deploy.port", label: "Порт", type: "number", placeholder: "22" }),
|
createProjectField({ path: "deploy.port", label: "Порт", type: "number", placeholder: "22" }),
|
||||||
createProjectField({ path: "deploy.username", label: "Логин" }),
|
createProjectField({ path: "deploy.username", label: "Логин" }),
|
||||||
createProjectField({ path: "deploy.passwordEnv", label: "Переменная пароля", placeholder: "MCHOST_PASSWORD" }),
|
createProjectField({ path: "deploy.passwordEnv", label: "Переменная пароля", placeholder: "MCHOST_PASSWORD" }),
|
||||||
createProjectField({ path: "deploy.remotePath", label: "Папка сайта на хостинге", placeholder: "/public_html/nodedc.ru" }),
|
createProjectField({ path: "deploy.remotePath", label: "Папка сайта на хостинге", placeholder: "httpdocs" }),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -2081,6 +2147,11 @@ function renderEditor() {
|
||||||
if (!block) {
|
if (!block) {
|
||||||
el.empty.classList.remove("hidden");
|
el.empty.classList.remove("hidden");
|
||||||
el.form.classList.add("hidden");
|
el.form.classList.add("hidden");
|
||||||
|
renderEmptyState({
|
||||||
|
icon: "ND",
|
||||||
|
title: "Выберите блок слева",
|
||||||
|
body: "Статика, hero, форма и секции страницы разделены на рабочие группы. Сначала редактируем typed-поля, сырой JSON оставлен как технический fallback.",
|
||||||
|
});
|
||||||
el.selectedRegion.textContent = "NODE.DC / Выберите блок";
|
el.selectedRegion.textContent = "NODE.DC / Выберите блок";
|
||||||
el.selectedTitle.textContent = isSeo ? "SEO-слой" : "Контентный слой";
|
el.selectedTitle.textContent = isSeo ? "SEO-слой" : "Контентный слой";
|
||||||
renderLandingTargetOptions();
|
renderLandingTargetOptions();
|
||||||
|
|
@ -2124,6 +2195,11 @@ function renderEditor() {
|
||||||
|
|
||||||
function renderAll() {
|
function renderAll() {
|
||||||
try {
|
try {
|
||||||
|
if (!siteReady() && !isProjectSettingsSelected()) {
|
||||||
|
renderSiteUnavailable();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (isProjectSettingsSelected()) {
|
if (isProjectSettingsSelected()) {
|
||||||
if (state.mode === "knowledge" && state.knowledge) {
|
if (state.mode === "knowledge" && state.knowledge) {
|
||||||
ensureKnowledgeSelection();
|
ensureKnowledgeSelection();
|
||||||
|
|
@ -5327,6 +5403,11 @@ function renderKnowledgeAll() {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadKnowledge() {
|
async function loadKnowledge() {
|
||||||
|
if (!siteReady()) {
|
||||||
|
renderSiteUnavailable();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const response = await fetch("/api/knowledge");
|
const response = await fetch("/api/knowledge");
|
||||||
if (!response.ok) throw new Error(await response.text());
|
if (!response.ok) throw new Error(await response.text());
|
||||||
state.knowledge = await response.json();
|
state.knowledge = await response.json();
|
||||||
|
|
@ -5367,13 +5448,20 @@ async function renderKnowledge() {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadPage() {
|
async function loadPage() {
|
||||||
const [projectConfig, pageResponse, templatesResponse] = await Promise.all([
|
const projectConfig = await loadProjectConfig();
|
||||||
loadProjectConfig(),
|
|
||||||
fetch("/api/page/home"),
|
|
||||||
fetch("/api/block-templates/home"),
|
|
||||||
]);
|
|
||||||
|
|
||||||
state.projectConfig = projectConfig;
|
state.projectConfig = projectConfig;
|
||||||
|
state.knowledge = null;
|
||||||
|
|
||||||
|
if (!siteReady()) {
|
||||||
|
state.page = null;
|
||||||
|
state.templates = [];
|
||||||
|
state.selected = null;
|
||||||
|
renderSiteUnavailable();
|
||||||
|
setStatus("Сайт не загружен в локальный workspace CMS.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [pageResponse, templatesResponse] = await Promise.all([fetch("/api/page/home"), fetch("/api/block-templates/home")]);
|
||||||
if (!pageResponse.ok) throw new Error(await pageResponse.text());
|
if (!pageResponse.ok) throw new Error(await pageResponse.text());
|
||||||
if (!templatesResponse.ok) throw new Error(await templatesResponse.text());
|
if (!templatesResponse.ok) throw new Error(await templatesResponse.text());
|
||||||
|
|
||||||
|
|
@ -5514,6 +5602,7 @@ for (const input of [el.id, el.label, el.anchor, el.enabled]) {
|
||||||
el.modeContent?.addEventListener("click", () => setAdminMode("content"));
|
el.modeContent?.addEventListener("click", () => setAdminMode("content"));
|
||||||
el.modeSeo?.addEventListener("click", () => setAdminMode("seo"));
|
el.modeSeo?.addEventListener("click", () => setAdminMode("seo"));
|
||||||
el.modeKnowledge?.addEventListener("click", () => setAdminMode("knowledge"));
|
el.modeKnowledge?.addEventListener("click", () => setAdminMode("knowledge"));
|
||||||
|
el.addProject?.addEventListener("click", openProjectSettings);
|
||||||
el.projectSettings?.addEventListener("click", openProjectSettings);
|
el.projectSettings?.addEventListener("click", openProjectSettings);
|
||||||
|
|
||||||
el.json.addEventListener("blur", () => {
|
el.json.addEventListener("blur", () => {
|
||||||
|
|
@ -5525,7 +5614,7 @@ el.json.addEventListener("blur", () => {
|
||||||
|
|
||||||
syncBlockFromJson();
|
syncBlockFromJson();
|
||||||
});
|
});
|
||||||
el.addBlock.addEventListener("click", () => {
|
el.addBlock?.addEventListener("click", () => {
|
||||||
if (state.mode === "knowledge") {
|
if (state.mode === "knowledge") {
|
||||||
addKnowledgeNode(null);
|
addKnowledgeNode(null);
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -55,19 +55,22 @@
|
||||||
<div class="section-kicker">NODE.DC</div>
|
<div class="section-kicker">NODE.DC</div>
|
||||||
<h1 id="sidebar-title">Администрирование</h1>
|
<h1 id="sidebar-title">Администрирование</h1>
|
||||||
</div>
|
</div>
|
||||||
<button id="project-settings" class="admin-panel-close project-settings-button" type="button" aria-label="Настройки проекта" title="Настройки проекта">
|
<button id="add-project" class="admin-panel-close add-block-button" type="button" aria-label="Добавить сайт" title="Добавить сайт">+</button>
|
||||||
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
|
||||||
<path d="M12 8.2a3.8 3.8 0 1 1 0 7.6 3.8 3.8 0 0 1 0-7.6Z" />
|
|
||||||
<path d="M19.4 13.5c.06-.48.06-.98 0-1.5l2-1.55-1.9-3.3-2.35.95a8.2 8.2 0 0 0-1.3-.75L15.5 4.8h-3.8l-.35 2.55c-.46.2-.9.45-1.3.75L7.7 7.15l-1.9 3.3L7.8 12a8.5 8.5 0 0 0 0 1.5l-2 1.55 1.9 3.3 2.35-.95c.4.3.84.55 1.3.75l.35 2.55h3.8l.35-2.55c.46-.2.9-.45 1.3-.75l2.35.95 1.9-3.3-2-1.55Z" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<button id="add-block" class="admin-panel-close add-block-button" type="button" aria-label="Добавить блок">+</button>
|
|
||||||
<div class="workspace-card admin-panel-client-select">
|
<div class="workspace-card admin-panel-client-select">
|
||||||
<div class="workspace-logo admin-panel-client-select__icon">DC</div>
|
<div class="workspace-logo admin-panel-client-select__icon">DC</div>
|
||||||
<div>
|
<div>
|
||||||
<div id="workspace-name" class="workspace-name">NODE.DC</div>
|
<div id="workspace-name" class="workspace-name">NODE.DC</div>
|
||||||
<div id="workspace-role" class="workspace-role">Администратор сайта</div>
|
<div id="workspace-role" class="workspace-role">Администратор сайта</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="workspace-card-actions">
|
||||||
|
<button id="project-settings" class="workspace-action-button" type="button" aria-label="Настройки проекта" title="Настройки проекта">
|
||||||
|
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
||||||
|
<path d="M12 8.2a3.8 3.8 0 1 1 0 7.6 3.8 3.8 0 0 1 0-7.6Z" />
|
||||||
|
<path d="M19.4 13.5c.06-.48.06-.98 0-1.5l2-1.55-1.9-3.3-2.35.95a8.2 8.2 0 0 0-1.3-.75L15.5 4.8h-3.8l-.35 2.55c-.46.2-.9.45-1.3.75L7.7 7.15l-1.9 3.3L7.8 12a8.5 8.5 0 0 0 0 1.5l-2 1.55 1.9 3.3 2.35-.95c.4.3.84.55 1.3.75l.35 2.55h3.8l.35-2.55c.46-.2.9-.45 1.3-.75l2.35.95 1.9-3.3-2-1.55Z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button id="add-block" class="workspace-action-button workspace-action-button--plus" type="button" aria-label="Добавить блок" title="Добавить блок">+</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="regions" class="regions"></div>
|
<div id="regions" class="regions"></div>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { createServer } from "node:http";
|
import { createServer } from "node:http";
|
||||||
import { createReadStream } from "node:fs";
|
import { createReadStream, statSync } from "node:fs";
|
||||||
import { copyFile, cp, mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
import { copyFile, cp, mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
||||||
import { extname, join, normalize, relative, resolve } from "node:path";
|
import { extname, join, normalize, relative, resolve } from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
@ -117,6 +117,15 @@ const REFERENCE_TEXT_EXTENSIONS = new Set([
|
||||||
".xml",
|
".xml",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const REQUIRED_SITE_FILES = [
|
||||||
|
{ key: "page", label: "content/pages/home.json", path: pagePath },
|
||||||
|
{ key: "knowledge", label: "content/knowledge.json", path: knowledgePath },
|
||||||
|
{ key: "blockTemplates", label: "content/block-templates/home.json", path: blockTemplatesPath },
|
||||||
|
{ key: "renderHome", label: "tools/render-home.mjs", path: join(repoRoot, "tools", "render-home.mjs") },
|
||||||
|
{ key: "renderKnowledge", label: "tools/render-knowledge.mjs", path: join(repoRoot, "tools", "render-knowledge.mjs") },
|
||||||
|
{ key: "homeShell", label: "templates/pages/home/shell.html", path: join(repoRoot, "templates", "pages", "home", "shell.html") },
|
||||||
|
];
|
||||||
|
|
||||||
const MIME = {
|
const MIME = {
|
||||||
".html": "text/html; charset=utf-8",
|
".html": "text/html; charset=utf-8",
|
||||||
".css": "text/css; charset=utf-8",
|
".css": "text/css; charset=utf-8",
|
||||||
|
|
@ -206,6 +215,42 @@ function trailingSlashUrl(value) {
|
||||||
return trimmed.endsWith("/") ? trimmed : `${trimmed}/`;
|
return trimmed.endsWith("/") ? trimmed : `${trimmed}/`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function fileReady(filePath) {
|
||||||
|
try {
|
||||||
|
return statSync(filePath).isFile();
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function currentSiteStatus() {
|
||||||
|
const files = REQUIRED_SITE_FILES.map((item) => ({
|
||||||
|
key: item.key,
|
||||||
|
label: item.label,
|
||||||
|
exists: fileReady(item.path),
|
||||||
|
}));
|
||||||
|
const missingFiles = files.filter((item) => !item.exists).map((item) => item.label);
|
||||||
|
|
||||||
|
return {
|
||||||
|
ready: missingFiles.length === 0,
|
||||||
|
root: repoRoot,
|
||||||
|
requiredFiles: files,
|
||||||
|
missingFiles,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureSiteReady(res) {
|
||||||
|
const site = currentSiteStatus();
|
||||||
|
if (site.ready) return true;
|
||||||
|
sendJson(res, 409, {
|
||||||
|
ok: false,
|
||||||
|
code: "SITE_NOT_READY",
|
||||||
|
error: "Сайт не загружен в локальный workspace CMS.",
|
||||||
|
site,
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const authConfig = {
|
const authConfig = {
|
||||||
enabled: boolEnv(process.env.CMS_AUTH_ENABLED, false),
|
enabled: boolEnv(process.env.CMS_AUTH_ENABLED, false),
|
||||||
baseUrl: String(process.env.CMS_BASE_URL || "").trim(),
|
baseUrl: String(process.env.CMS_BASE_URL || "").trim(),
|
||||||
|
|
@ -773,6 +818,7 @@ function publicProjectConfig() {
|
||||||
configPath: projectConfigPath,
|
configPath: projectConfigPath,
|
||||||
siteRootChangesRequireRestart: true,
|
siteRootChangesRequireRestart: true,
|
||||||
},
|
},
|
||||||
|
site: currentSiteStatus(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2165,16 +2211,19 @@ const server = createServer(async (req, res) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.method === "GET" && url.pathname === "/api/page/home") {
|
if (req.method === "GET" && url.pathname === "/api/page/home") {
|
||||||
|
if (!ensureSiteReady(res)) return;
|
||||||
send(res, 200, await readFile(pagePath, "utf8"), "application/json; charset=utf-8");
|
send(res, 200, await readFile(pagePath, "utf8"), "application/json; charset=utf-8");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.method === "GET" && url.pathname === "/api/knowledge") {
|
if (req.method === "GET" && url.pathname === "/api/knowledge") {
|
||||||
|
if (!ensureSiteReady(res)) return;
|
||||||
send(res, 200, await readFile(knowledgePath, "utf8"), "application/json; charset=utf-8");
|
send(res, 200, await readFile(knowledgePath, "utf8"), "application/json; charset=utf-8");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.method === "GET" && url.pathname === "/api/landing-targets") {
|
if (req.method === "GET" && url.pathname === "/api/landing-targets") {
|
||||||
|
if (!ensureSiteReady(res)) return;
|
||||||
const page = JSON.parse(await readFile(pagePath, "utf8"));
|
const page = JSON.parse(await readFile(pagePath, "utf8"));
|
||||||
validateLandingTargets(page);
|
validateLandingTargets(page);
|
||||||
sendJson(res, 200, { ok: true, targets: landingTargetsForPage(page) });
|
sendJson(res, 200, { ok: true, targets: landingTargetsForPage(page) });
|
||||||
|
|
@ -2182,6 +2231,7 @@ const server = createServer(async (req, res) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.method === "GET" && url.pathname === "/api/block-templates/home") {
|
if (req.method === "GET" && url.pathname === "/api/block-templates/home") {
|
||||||
|
if (!ensureSiteReady(res)) return;
|
||||||
send(res, 200, await readFile(blockTemplatesPath, "utf8"), "application/json; charset=utf-8");
|
send(res, 200, await readFile(blockTemplatesPath, "utf8"), "application/json; charset=utf-8");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -2207,6 +2257,7 @@ const server = createServer(async (req, res) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.method === "PUT" && url.pathname === "/api/page/home") {
|
if (req.method === "PUT" && url.pathname === "/api/page/home") {
|
||||||
|
if (!ensureSiteReady(res)) return;
|
||||||
const body = await readBody(req);
|
const body = await readBody(req);
|
||||||
const parsed = JSON.parse(body);
|
const parsed = JSON.parse(body);
|
||||||
validateLandingTargets(parsed);
|
validateLandingTargets(parsed);
|
||||||
|
|
@ -2216,6 +2267,7 @@ const server = createServer(async (req, res) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.method === "PUT" && url.pathname === "/api/knowledge") {
|
if (req.method === "PUT" && url.pathname === "/api/knowledge") {
|
||||||
|
if (!ensureSiteReady(res)) return;
|
||||||
const body = await readBody(req);
|
const body = await readBody(req);
|
||||||
const parsed = JSON.parse(body);
|
const parsed = JSON.parse(body);
|
||||||
await writeFile(knowledgePath, `${JSON.stringify(parsed, null, 2)}\n`);
|
await writeFile(knowledgePath, `${JSON.stringify(parsed, null, 2)}\n`);
|
||||||
|
|
@ -2224,12 +2276,14 @@ const server = createServer(async (req, res) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.method === "POST" && url.pathname === "/api/render/home") {
|
if (req.method === "POST" && url.pathname === "/api/render/home") {
|
||||||
|
if (!ensureSiteReady(res)) return;
|
||||||
const result = await runNodeScript("render-home.mjs");
|
const result = await runNodeScript("render-home.mjs");
|
||||||
sendJson(res, 200, { ok: true, ...result });
|
sendJson(res, 200, { ok: true, ...result });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.method === "POST" && url.pathname === "/api/render/knowledge") {
|
if (req.method === "POST" && url.pathname === "/api/render/knowledge") {
|
||||||
|
if (!ensureSiteReady(res)) return;
|
||||||
const result = await runNodeScript("render-knowledge.mjs");
|
const result = await runNodeScript("render-knowledge.mjs");
|
||||||
sendJson(res, 200, { ok: true, ...result });
|
sendJson(res, 200, { ok: true, ...result });
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue