v2: модульный монолит — очередь, персистентность, безопасность, SSE, тесты
Some checks failed
test / test (push) Failing after 24s
Some checks failed
test / test (push) Failing after 24s
Реализация целевого дизайна (docs/architecture/target-design.md): - src/: модульная структура (config, logger, errors, http, core: auth/history/jobs/engines/comfy/codex/images) - Очередь заданий: FIFO, кониурентность codex=1 / comfy-upscale=2, MAX_QUEUED_JOBS=5, JSONL-журнал и восстановление после рестарта (running → interrupted, upscale requeue) - Персистентные сессии (sha256-хеши токенов), scrypt-хеш пароля, rate-limit входа, Origin-проверка, magic-byte валидация аплоадов - Атомарная запись манифеста истории, каскадное удаление, URL с фактическим расширением + 302-алиас /image.jpg, миграция history/ → data/history - Даунскейл sharp до 1024px перед Codex + масштабирование области референса - SSE /api/events с фолбэком на polling, фронтенд переведён на ES-модули (public/js/) - Тесты node:test + supertest (27), CI workflow test.yml, Docker/деплой: том kadr-data - Документация: README, docs/architecture/
This commit is contained in:
138
public/js/ui.js
Normal file
138
public/js/ui.js
Normal file
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* ui.js — Утилиты форматирования, отображения статусов и DOM-элементов
|
||||
*/
|
||||
|
||||
export const MAX_FILE_SIZE = 25 * 1024 * 1024;
|
||||
export const ACCEPTED_TYPES = new Set([
|
||||
"image/jpeg",
|
||||
"image/jpg",
|
||||
"image/png",
|
||||
"image/webp",
|
||||
"image/heic",
|
||||
"image/heif",
|
||||
]);
|
||||
|
||||
export function isHeicFile(file) {
|
||||
const type = String(file?.type || "").toLowerCase();
|
||||
const name = String(file?.name || "").toLowerCase();
|
||||
return (
|
||||
type === "image/heic" ||
|
||||
type === "image/heif" ||
|
||||
name.endsWith(".heic") ||
|
||||
name.endsWith(".heif")
|
||||
);
|
||||
}
|
||||
|
||||
export function formatFileSize(bytes) {
|
||||
if (!bytes || bytes <= 0) return "";
|
||||
const units = ["Б", "КБ", "МБ", "ГБ"];
|
||||
let value = bytes;
|
||||
let unitIndex = 0;
|
||||
while (value >= 1024 && unitIndex < units.length - 1) {
|
||||
value /= 1024;
|
||||
unitIndex++;
|
||||
}
|
||||
return `${value.toFixed(value >= 10 || unitIndex === 0 ? 0 : 1)} ${units[unitIndex]}`;
|
||||
}
|
||||
|
||||
export function versionFileName(version) {
|
||||
const dateStr = version?.createdAt
|
||||
? new Date(version.createdAt).toISOString().replace(/[:.]/g, "-").slice(0, 19)
|
||||
: "photo";
|
||||
const ext = version?.ext || "jpg";
|
||||
const label = version?.label ? version.label.replace(/[^\p{L}\p{N}._-]+/gu, "-") : "version";
|
||||
return `kadr-${label}-${dateStr}.${ext}`;
|
||||
}
|
||||
|
||||
export function formatHistoryDate(iso) {
|
||||
if (!iso) return "";
|
||||
try {
|
||||
const d = new Date(iso);
|
||||
const date = d.toLocaleDateString("ru-RU", {
|
||||
day: "numeric",
|
||||
month: "short",
|
||||
});
|
||||
const time = d.toLocaleTimeString("ru-RU", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
return `${date}, ${time}`;
|
||||
} catch {
|
||||
return iso;
|
||||
}
|
||||
}
|
||||
|
||||
export function setJobStatus(kind, message) {
|
||||
const jobStatus = document.querySelector("#job-status");
|
||||
const jobStatusText = document.querySelector("#job-status-text");
|
||||
|
||||
if (!jobStatus || !jobStatusText) return;
|
||||
|
||||
if (kind === "idle") {
|
||||
jobStatus.hidden = true;
|
||||
jobStatus.classList.remove("status-error");
|
||||
jobStatusText.textContent = "";
|
||||
return;
|
||||
}
|
||||
|
||||
jobStatus.hidden = false;
|
||||
jobStatus.classList.toggle("status-error", kind === "error");
|
||||
jobStatusText.textContent = message;
|
||||
}
|
||||
|
||||
export function resetResult() {
|
||||
const resultImage = document.querySelector("#result-image");
|
||||
const resultPlaceholder = document.querySelector("#result-placeholder");
|
||||
const resultLabel = document.querySelector("#result-label");
|
||||
const downloadLink = document.querySelector("#download-link");
|
||||
|
||||
if (resultImage) {
|
||||
resultImage.hidden = true;
|
||||
resultImage.removeAttribute("src");
|
||||
}
|
||||
if (resultPlaceholder) resultPlaceholder.hidden = false;
|
||||
if (resultLabel) resultLabel.textContent = "";
|
||||
if (downloadLink) downloadLink.hidden = true;
|
||||
}
|
||||
|
||||
export function clearSourcePreview() {
|
||||
const sourceImage = document.querySelector("#source-image");
|
||||
const sourcePlaceholder = document.querySelector("#source-placeholder");
|
||||
const downloadCurrent = document.querySelector("#download-current");
|
||||
|
||||
if (sourceImage) {
|
||||
sourceImage.hidden = true;
|
||||
sourceImage.removeAttribute("src");
|
||||
}
|
||||
if (sourcePlaceholder) sourcePlaceholder.hidden = false;
|
||||
if (downloadCurrent) downloadCurrent.hidden = true;
|
||||
}
|
||||
|
||||
export function showSourcePreview(url, title, meta) {
|
||||
const sourceImage = document.querySelector("#source-image");
|
||||
const sourcePlaceholder = document.querySelector("#source-placeholder");
|
||||
const dropTitle = document.querySelector("#drop-title");
|
||||
const fileMeta = document.querySelector("#file-meta");
|
||||
|
||||
if (sourceImage) {
|
||||
sourceImage.src = url;
|
||||
sourceImage.hidden = false;
|
||||
}
|
||||
if (sourcePlaceholder) sourcePlaceholder.hidden = true;
|
||||
if (dropTitle && title) dropTitle.textContent = title;
|
||||
if (fileMeta && meta) fileMeta.textContent = meta;
|
||||
}
|
||||
|
||||
export function updateDownloadCurrent(version) {
|
||||
const downloadCurrent = document.querySelector("#download-current");
|
||||
if (!downloadCurrent) return;
|
||||
|
||||
if (!version || !version.url) {
|
||||
downloadCurrent.hidden = true;
|
||||
return;
|
||||
}
|
||||
|
||||
downloadCurrent.href = version.url;
|
||||
downloadCurrent.download = versionFileName(version);
|
||||
downloadCurrent.hidden = false;
|
||||
}
|
||||
Reference in New Issue
Block a user