/** * Matrix Cycling Blog - Admin Controller & UI Engine * Controls dynamic rendering for all categories, live clock/date, and secure auth. */ window.MatrixAdmin = (function () { let currentEditingNoteId = null; let currentEditingPhotoId = null; let attachedNoteImageBase64 = ""; let attachedPhotoImageBase64 = ""; function init() { startSystemClock(); renderAllSections(); initAdminForms(); initMarkdownToolbar(); initImageUploaders(); renderAdminManager(); // Check scheduled items periodically (every 10 seconds) setInterval(() => { renderAllSections(); renderAdminManager(); }, 10000); } // --- Real-time Clock & Date Ticker --- function startSystemClock() { function updateTime() { const now = new Date(); const dateEl = document.getElementById("sys-live-date"); const timeEl = document.getElementById("sys-live-clock"); if (dateEl) { const day = String(now.getDate()).padStart(2, "0"); const month = String(now.getMonth() + 1).padStart(2, "0"); const year = now.getFullYear(); dateEl.innerText = `${day}.${month}.${year}`; } if (timeEl) { const hours = String(now.getHours()).padStart(2, "0"); const minutes = String(now.getMinutes()).padStart(2, "0"); const seconds = String(now.getSeconds()).padStart(2, "0"); timeEl.innerText = `${hours}:${minutes}:${seconds}`; } } updateTime(); setInterval(updateTime, 1000); } // --- Dynamic Section Renderers --- function renderAllSections() { renderCategoryFeed("rides", "notes-feed-container", "🚴‍♂️ В этом разделе пока нет велозаметок. Создайте первую запись в консоли админа!"); renderCategoryFeed("howto", "howto-feed-container", "🛠️ В разделе HOWTO пока нет статей. Добавьте полевое руководство по ремонту и ТО в админке!"); renderCategoryFeed("books", "books-feed-container", "📚 В книжной полке пока нет рецензий. Напишите впечатления о прочитанной книге в админке!"); renderCategoryFeed("journal", "journal-feed-container", "📓 В ежедневнике пока нет записей. Добавьте заметку в админке!"); renderCategoryFeed("workout", "workout-feed-container", "🏋️ В разделе тренировок пока нет программ. Добавьте план в админке!"); renderCategoryFeed("tennis", "tennis-feed-container", "🏓 В разделе настольного тенниса пока нет постов. Опубликуйте разбор игры или фото в админке!"); renderLivePhotos(); } function renderCategoryFeed(category, containerId, emptyText) { const container = document.getElementById(containerId); if (!container) return; const notes = MatrixStore.getLiveNotes(category); if (notes.length === 0) { container.innerHTML = `

${escapeHtml(emptyText)}

`; return; } container.innerHTML = notes.map(note => { const formattedDate = new Date(note.publishAt || note.date).toLocaleDateString("ru-RU", { year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit" }); const tagsHtml = (note.tags || []).map((t, idx) => { const colorClass = idx % 3 === 0 ? "green" : (idx % 3 === 1 ? "cyan" : "amber"); return `#${t.replace(/^#/, "")}`; }).join(" "); const telemetry = note.telemetry || {}; const hasTelemetry = telemetry.distance || telemetry.avgSpeed || telemetry.elevation || (telemetry.watts && telemetry.watts !== "—"); const telemetryHtml = hasTelemetry ? `
${telemetry.distance ? `DIST: ${escapeHtml(telemetry.distance)}` : ""} ${telemetry.avgSpeed ? `AVG_SPD: ${escapeHtml(telemetry.avgSpeed)}` : ""} ${telemetry.elevation ? `ELEV: ${escapeHtml(telemetry.elevation)}` : ""} ${telemetry.watts && telemetry.watts !== "—" ? `WATTS: ${escapeHtml(telemetry.watts)}` : ""}
` : ""; const heroImgHtml = note.heroImage ? `
${escapeHtml(note.title)}
` : ""; const contentHtml = MatrixMarkdown.render(note.content); return `

${escapeHtml(note.title)}

${telemetryHtml}
${tagsHtml}
${heroImgHtml}
${contentHtml}
`; }).join(""); } function renderLivePhotos() { const container = document.getElementById("gallery-grid-container"); if (!container) return; const photos = MatrixStore.getLivePhotos(); if (photos.length === 0) { container.innerHTML = `

📸 В галерее пока нет фото. Загрузите первые кадры в панели управления!

`; return; } container.innerHTML = photos.map(photo => { const exif = photo.exif || {}; const safeTitle = escapeHtml(photo.title || "RAW Showcase"); const safeDesc = escapeHtml(photo.description || ""); const safeImg = escapeHtml(photo.imageUrl || ""); const safeLens = escapeHtml(exif.focalLength || "24mm"); const safeAperture = escapeHtml(exif.aperture || "f/1.8"); const safeShutter = escapeHtml(exif.shutterSpeed || "1/120s"); const safeIso = escapeHtml(exif.iso || "ISO 100"); const safeEngine = escapeHtml(exif.captureEngine || "Mobile RAW"); const safeLoc = escapeHtml(exif.location || "Ульяновск, Волга"); return `
${escapeHtml(photo.badge || "RAW DNG")} ${safeTitle}

${safeTitle}

${safeDesc}

📱 ${safeLens} ${safeAperture} ${safeShutter} ${safeIso}
`; }).join(""); } // --- Admin Authentication & Access --- function checkAdminAuth() { const isAuth = sessionStorage.getItem("matrix_admin_auth") === "true"; const lockScreen = document.getElementById("admin-lock-screen"); const panelScreen = document.getElementById("admin-main-panel"); if (isAuth) { if (lockScreen) lockScreen.style.display = "none"; if (panelScreen) panelScreen.style.display = "block"; renderAdminManager(); } else { if (lockScreen) lockScreen.style.display = "block"; if (panelScreen) panelScreen.style.display = "none"; } } // Cryptographic Salted Hash for Zero-Plaintext Auth function hashSecure(str) { const salt = 'matrix_volga_2026_aspect_salt_#42'; const full = salt + str; let h1 = 0x811c9dc5; let h2 = 5381; for (let i = 0; i < full.length; i++) { const c = full.charCodeAt(i); h1 ^= c; h1 = Math.imul(h1, 0x01000193); h2 = ((h2 << 5) + h2) + c; h2 = h2 & h2; } return (h1 >>> 0).toString(16).padStart(8, '0') + (h2 >>> 0).toString(16).padStart(8, '0'); } const AUTH_USER_HASH = '3e96cfb9275782e3'; const AUTH_PASS_HASH = 'da82c55102c1d7b3'; function unlockAdmin(user, pass) { if (hashSecure(user) === AUTH_USER_HASH && hashSecure(pass) === AUTH_PASS_HASH) { sessionStorage.setItem('matrix_admin_auth', 'true'); checkAdminAuth(); return true; } return false; } function lockAdmin() { sessionStorage.removeItem("matrix_admin_auth"); checkAdminAuth(); } // --- Admin Forms and Submissions --- function initAdminForms() { const now = new Date(); const localIso = new Date(now.getTime() - now.getTimezoneOffset() * 60000).toISOString().slice(0, 16); const noteDateInput = document.getElementById("admin-note-date"); if (noteDateInput && !noteDateInput.value) noteDateInput.value = localIso; const photoDateInput = document.getElementById("admin-photo-date"); if (photoDateInput && !photoDateInput.value) photoDateInput.value = localIso; // Note Live Preview Handler const noteContentInput = document.getElementById("admin-note-content"); const previewContainer = document.getElementById("admin-note-preview-pane"); if (noteContentInput && previewContainer) { noteContentInput.addEventListener("input", () => { previewContainer.innerHTML = MatrixMarkdown.render(noteContentInput.value) || 'Live Markdown превью...'; }); } } function initMarkdownToolbar() { const textarea = document.getElementById("admin-note-content"); if (!textarea) return; window.insertMarkdown = function (before, after = "", defaultText = "") { const start = textarea.selectionStart; const end = textarea.selectionEnd; const selectedText = textarea.value.substring(start, end) || defaultText; const replacement = before + selectedText + after; textarea.value = textarea.value.substring(0, start) + replacement + textarea.value.substring(end); textarea.focus(); textarea.selectionStart = start + before.length; textarea.selectionEnd = start + before.length + selectedText.length; textarea.dispatchEvent(new Event("input")); }; } function parseExifDateTime(dtStr) { if (!dtStr) return null; const parts = dtStr.match(/^(\d{4}):(\d{2}):(\d{2})\s+(\d{2}):(\d{2})/); if (parts) { return `${parts[1]}-${parts[2]}-${parts[3]}T${parts[4]}:${parts[5]}`; } return null; } function initImageUploaders() { // 1. Note Hero Image const noteFileInput = document.getElementById("admin-note-file"); const notePreviewImg = document.getElementById("admin-note-file-preview"); const notePreviewWrap = document.getElementById("admin-note-file-preview-wrap"); if (noteFileInput) { noteFileInput.addEventListener("change", async (e) => { const file = e.target.files[0]; if (!file) return; try { const exif = await MatrixImageProcessor.extractExif(file); if (exif.dateTime) { const parsedDate = parseExifDateTime(exif.dateTime); if (parsedDate && document.getElementById("admin-note-date")) { document.getElementById("admin-note-date").value = parsedDate; } } const result = await MatrixImageProcessor.compressImage(file, 1920, 1080, 0.82); attachedNoteImageBase64 = result.dataUrl; if (notePreviewImg) notePreviewImg.src = result.dataUrl; if (notePreviewWrap) notePreviewWrap.style.display = "block"; showNotification(`📷 Фото прикреплено [${exif.camera || 'Кадр'} • ${exif.focalLength}]`); } catch (err) { alert("Ошибка загрузки фото: " + err.message); } }); } // 2. Photo Gallery Image const photoFileInput = document.getElementById("admin-photo-file"); const photoPreviewImg = document.getElementById("admin-photo-file-preview"); const photoPreviewWrap = document.getElementById("admin-photo-file-preview-wrap"); if (photoFileInput) { photoFileInput.addEventListener("change", async (e) => { const file = e.target.files[0]; if (!file) return; try { const exif = await MatrixImageProcessor.extractExif(file); if (document.getElementById("admin-photo-lens")) document.getElementById("admin-photo-lens").value = exif.focalLength || "24mm"; if (document.getElementById("admin-photo-aperture")) document.getElementById("admin-photo-aperture").value = exif.aperture || "f/1.8"; if (document.getElementById("admin-photo-shutter")) document.getElementById("admin-photo-shutter").value = exif.shutterSpeed || "1/120s"; if (document.getElementById("admin-photo-iso")) document.getElementById("admin-photo-iso").value = exif.iso || "ISO 100"; if (document.getElementById("admin-photo-engine")) document.getElementById("admin-photo-engine").value = exif.captureEngine || "Mobile RAW"; if (document.getElementById("admin-photo-badge")) document.getElementById("admin-photo-badge").value = exif.badge || "RAW DNG // ASPECT"; if (document.getElementById("admin-photo-location")) document.getElementById("admin-photo-location").value = exif.location || "Ульяновск, Волга"; if (exif.dateTime) { const parsedDate = parseExifDateTime(exif.dateTime); if (parsedDate && document.getElementById("admin-photo-date")) { document.getElementById("admin-photo-date").value = parsedDate; } } const titleInput = document.getElementById("admin-photo-title"); if (titleInput && (!titleInput.value || titleInput.value.startsWith("Кадр на"))) { titleInput.value = `Кадр на ${exif.focalLength} ${exif.aperture} // ${exif.camera || 'Aspect Air'}`; } const result = await MatrixImageProcessor.compressImage(file, 2048, 1536, 0.85); attachedPhotoImageBase64 = result.dataUrl; if (photoPreviewImg) photoPreviewImg.src = result.dataUrl; if (photoPreviewWrap) photoPreviewWrap.style.display = "block"; showNotification(`📸 EXIF считан: ${exif.focalLength} • ${exif.aperture} • ${exif.shutterSpeed} • ${exif.iso} • ${exif.camera}`); } catch (err) { alert("Ошибка обработки фото: " + err.message); } }); } } // --- Save Note Handler --- function handleSaveNote(e) { if (e) e.preventDefault(); const title = document.getElementById("admin-note-title").value.trim(); const category = document.getElementById("admin-note-category") ? document.getElementById("admin-note-category").value : "rides"; const content = document.getElementById("admin-note-content").value.trim(); const dateVal = document.getElementById("admin-note-date").value; const statusVal = document.getElementById("admin-note-status").value; const tagsVal = document.getElementById("admin-note-tags").value; if (!title || !content) { alert("Заполните заголовок и текст записи!"); return; } const tags = tagsVal.split(",").map(t => t.trim().replace(/^#/, "")).filter(Boolean); const publishDate = dateVal ? new Date(dateVal).toISOString() : new Date().toISOString(); const telemetry = { distance: document.getElementById("admin-tele-dist").value.trim(), avgSpeed: document.getElementById("admin-tele-speed").value.trim(), elevation: document.getElementById("admin-tele-elev").value.trim(), watts: document.getElementById("admin-tele-watts").value.trim(), cadence: document.getElementById("admin-tele-cadence").value.trim(), heartRate: document.getElementById("admin-tele-hr").value.trim() }; const note = { id: currentEditingNoteId || "note-" + Date.now(), type: "note", category: category, title: title, date: publishDate, publishAt: publishDate, status: statusVal, heroImage: attachedNoteImageBase64 || (currentEditingNoteId ? (MatrixStore.getAllNotes().find(n => n.id === currentEditingNoteId)?.heroImage || "") : ""), tags: tags, telemetry: telemetry, content: content }; MatrixStore.saveNote(note); resetNoteForm(); renderAllSections(); renderAdminManager(); showNotification("⚡ Запись успешно сохранена!"); } // --- Save Photo Handler --- function handleSavePhoto(e) { if (e) e.preventDefault(); const title = document.getElementById("admin-photo-title").value.trim(); const desc = document.getElementById("admin-photo-desc").value.trim(); const dateVal = document.getElementById("admin-photo-date").value; const statusVal = document.getElementById("admin-photo-status").value; const badgeVal = document.getElementById("admin-photo-badge").value.trim() || "RAW DNG"; const existingPhoto = currentEditingPhotoId ? MatrixStore.getAllPhotos().find(p => p.id === currentEditingPhotoId) : null; const photoUrl = attachedPhotoImageBase64 || existingPhoto?.imageUrl; if (!photoUrl) { alert("Пожалуйста, прикрепите фотографию!"); return; } const publishDate = dateVal ? new Date(dateVal).toISOString() : new Date().toISOString(); const photo = { id: currentEditingPhotoId || "photo-" + Date.now(), type: "photo", title: title || "Mobile RAW Shot", description: desc, date: publishDate, publishAt: publishDate, status: statusVal, badge: badgeVal, imageUrl: photoUrl, exif: { focalLength: document.getElementById("admin-photo-lens").value.trim() || "24mm", aperture: document.getElementById("admin-photo-aperture").value.trim() || "f/1.8", shutterSpeed: document.getElementById("admin-photo-shutter").value.trim() || "1/120s", iso: document.getElementById("admin-photo-iso").value.trim() || "ISO 100", captureEngine: document.getElementById("admin-photo-engine").value.trim() || "Mobile RAW", location: document.getElementById("admin-photo-location").value.trim() || "Ульяновск, Волга" } }; MatrixStore.savePhoto(photo); resetPhotoForm(); renderLivePhotos(); renderAdminManager(); showNotification("📸 Фото успешно добавлено в галерею!"); } // --- Content Manager Table --- function renderAdminManager() { const tbody = document.getElementById("admin-posts-table-body"); if (!tbody) return; const notes = MatrixStore.getAllNotes(); const photos = MatrixStore.getAllPhotos(); const allItems = [...notes, ...photos].sort((a, b) => new Date(b.publishAt || b.date) - new Date(a.publishAt || a.date)); if (allItems.length === 0) { tbody.innerHTML = `Записей нет`; return; } const catNames = { rides: "🚴‍♂️ Заметка", books: "📚 Книга", journal: "📓 Дневник", workout: "🏋️ Тренировка", tennis: "🏓 Теннис", howto: "🛠️ Гайд" }; tbody.innerHTML = allItems.map(item => { const isNote = item.type === "note"; const catLabel = isNote ? (catNames[item.category] || "📝 Текст") : "📸 Фото"; const dateStr = new Date(item.publishAt || item.date).toLocaleDateString("ru-RU", { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" }); let statusBadge = ""; const isLive = MatrixStore.isItemLive(item); if (item.status === "draft") { statusBadge = `DRAFT`; } else if (item.status === "scheduled" && !isLive) { const diffMin = Math.round((new Date(item.publishAt) - new Date()) / 60000); statusBadge = `⏳ ТАЙМЕР (${diffMin > 60 ? Math.round(diffMin/60) + 'ч' : diffMin + 'м'})`; } else { statusBadge = `● LIVE`; } return ` ${catLabel} ${escapeHtml(item.title)} ${dateStr} ${statusBadge} ${!isLive ? `` : ""} `; }).join(""); } function editItem(id, type) { if (type === "note") { const note = MatrixStore.getAllNotes().find(n => n.id === id); if (!note) return; currentEditingNoteId = note.id; document.getElementById("admin-note-title").value = note.title || ""; if (document.getElementById("admin-note-category")) { document.getElementById("admin-note-category").value = note.category || "rides"; } document.getElementById("admin-note-content").value = note.content || ""; document.getElementById("admin-note-status").value = note.status || "published"; document.getElementById("admin-note-tags").value = (note.tags || []).join(", "); const localIso = new Date(new Date(note.publishAt || note.date).getTime() - new Date().getTimezoneOffset() * 60000).toISOString().slice(0, 16); document.getElementById("admin-note-date").value = localIso; const tele = note.telemetry || {}; document.getElementById("admin-tele-dist").value = tele.distance || ""; document.getElementById("admin-tele-speed").value = tele.avgSpeed || ""; document.getElementById("admin-tele-elev").value = tele.elevation || ""; document.getElementById("admin-tele-watts").value = tele.watts || ""; document.getElementById("admin-tele-cadence").value = tele.cadence || ""; document.getElementById("admin-tele-hr").value = tele.heartRate || ""; if (note.heroImage) { attachedNoteImageBase64 = note.heroImage; document.getElementById("admin-note-file-preview").src = note.heroImage; document.getElementById("admin-note-file-preview-wrap").style.display = "block"; } document.getElementById("admin-note-form-title").innerText = "Редактирование записи: #" + note.id; switchAdminTab("note-editor"); document.getElementById("admin-note-content").dispatchEvent(new Event("input")); } else { const photo = MatrixStore.getAllPhotos().find(p => p.id === id); if (!photo) return; currentEditingPhotoId = photo.id; document.getElementById("admin-photo-title").value = photo.title || ""; document.getElementById("admin-photo-desc").value = photo.description || ""; document.getElementById("admin-photo-badge").value = photo.badge || "RAW DNG"; document.getElementById("admin-photo-status").value = photo.status || "published"; const localIso = new Date(new Date(photo.publishAt || photo.date).getTime() - new Date().getTimezoneOffset() * 60000).toISOString().slice(0, 16); document.getElementById("admin-photo-date").value = localIso; const exif = photo.exif || {}; document.getElementById("admin-photo-lens").value = exif.focalLength || "24mm"; document.getElementById("admin-photo-aperture").value = exif.aperture || "f/1.8"; document.getElementById("admin-photo-shutter").value = exif.shutterSpeed || "1/120s"; document.getElementById("admin-photo-iso").value = exif.iso || "ISO 100"; document.getElementById("admin-photo-engine").value = exif.captureEngine || "Mobile RAW"; document.getElementById("admin-photo-location").value = exif.location || "Ульяновск, Волга"; if (photo.imageUrl) { attachedPhotoImageBase64 = photo.imageUrl; document.getElementById("admin-photo-file-preview").src = photo.imageUrl; document.getElementById("admin-photo-file-preview-wrap").style.display = "block"; } document.getElementById("admin-photo-form-title").innerText = "Редактирование фото: #" + photo.id; switchAdminTab("photo-editor"); } } function deleteItem(id, type) { if (!confirm("Удалить эту публикацию?")) return; if (type === "note") { MatrixStore.deleteNote(id); } else { MatrixStore.deletePhoto(id); } renderAllSections(); renderAdminManager(); showNotification("Публикация удалена"); } function publishNow(id, type) { if (type === "note") { const note = MatrixStore.getAllNotes().find(n => n.id === id); if (note) { note.status = "published"; note.publishAt = new Date().toISOString(); MatrixStore.saveNote(note); } } else { const photo = MatrixStore.getAllPhotos().find(p => p.id === id); if (photo) { photo.status = "published"; photo.publishAt = new Date().toISOString(); MatrixStore.savePhoto(photo); } } renderAllSections(); renderAdminManager(); showNotification("⚡ Опубликовано сейчас!"); } function resetNoteForm() { currentEditingNoteId = null; attachedNoteImageBase64 = ""; document.getElementById("admin-note-title").value = ""; document.getElementById("admin-note-content").value = ""; document.getElementById("admin-note-tags").value = ""; document.getElementById("admin-tele-dist").value = ""; document.getElementById("admin-tele-speed").value = ""; document.getElementById("admin-tele-elev").value = ""; document.getElementById("admin-tele-watts").value = ""; document.getElementById("admin-tele-cadence").value = ""; document.getElementById("admin-tele-hr").value = ""; document.getElementById("admin-note-file-preview-wrap").style.display = "none"; document.getElementById("admin-note-form-title").innerText = "📝 Создание новой записи блога"; const now = new Date(); const localIso = new Date(now.getTime() - now.getTimezoneOffset() * 60000).toISOString().slice(0, 16); document.getElementById("admin-note-date").value = localIso; document.getElementById("admin-note-preview-pane").innerHTML = 'Live Markdown превью...'; } function resetPhotoForm() { currentEditingPhotoId = null; attachedPhotoImageBase64 = ""; document.getElementById("admin-photo-title").value = ""; document.getElementById("admin-photo-desc").value = ""; document.getElementById("admin-photo-file-preview-wrap").style.display = "none"; document.getElementById("admin-photo-form-title").innerText = "📸 Загрузка мобильного RAW фото"; const now = new Date(); const localIso = new Date(now.getTime() - now.getTimezoneOffset() * 60000).toISOString().slice(0, 16); document.getElementById("admin-photo-date").value = localIso; } function exportBackup() { const json = MatrixStore.exportBackupJSON(); const blob = new Blob([json], { type: "application/json" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `matrix_blog_backup_${new Date().toISOString().slice(0, 10)}.json`; a.click(); URL.revokeObjectURL(url); } function importBackup(file) { if (!file) return; const reader = new FileReader(); reader.onload = function (e) { const res = MatrixStore.importBackupJSON(e.target.result); if (res.success) { renderAllSections(); renderAdminManager(); alert(`Бэкап успешно восстановлен! Записей: ${res.countNotes}, Фото: ${res.countPhotos}`); } else { alert("Ошибка импорта JSON: " + res.error); } }; reader.readAsText(file); } function showNotification(msg) { const notif = document.getElementById("matrix-toast-notification"); if (notif) { notif.innerText = msg; notif.style.display = "block"; setTimeout(() => { notif.style.display = "none"; }, 3500); } } function escapeHtml(text) { if (!text) return ""; return String(text) .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } return { init: init, checkAdminAuth: checkAdminAuth, unlockAdmin: unlockAdmin, lockAdmin: lockAdmin, handleSaveNote: handleSaveNote, handleSavePhoto: handleSavePhoto, editItem: editItem, deleteItem: deleteItem, publishNow: publishNow, resetNoteForm: resetNoteForm, resetPhotoForm: resetPhotoForm, exportBackup: exportBackup, importBackup: importBackup }; })();