From 1107325a5029d4583251664ad690b7d4bd73e69d Mon Sep 17 00:00:00 2001 From: Hadi Khazaee Asl Date: Wed, 16 Sep 2026 17:52:23 +0330 Subject: [PATCH] last ... --- xDependencies_1.4.29.html | 3109 +++++++++-- xDependencies_1.4.30.html | 3010 +++++++++++ xDependencies_1.4.31.html | 402 ++ xDependencies_1.4.32.html | 487 ++ xDependencies_1.4.33.html | 3980 ++++++++++++++ xDependencies_Last.html | 7606 +++++++++++---------------- xDependencies_Samples_for_Test.js | 660 +++ xDependencies_Samples_for_Test.json | 755 +++ 8 files changed, 15021 insertions(+), 4988 deletions(-) create mode 100644 xDependencies_1.4.30.html create mode 100644 xDependencies_1.4.31.html create mode 100644 xDependencies_1.4.32.html create mode 100644 xDependencies_1.4.33.html create mode 100644 xDependencies_Samples_for_Test.js create mode 100644 xDependencies_Samples_for_Test.json diff --git a/xDependencies_1.4.29.html b/xDependencies_1.4.29.html index 9f65dad..baac6d1 100644 --- a/xDependencies_1.4.29.html +++ b/xDependencies_1.4.29.html @@ -1,353 +1,2798 @@ + - - -مدیریت محصولات - + + + مدیریت محصولات + + + -
مدیریت محصولات
مدیریت نسخه‌ها و نیازمندی‌های محصولات
-
🏷️
🌐
📋کل:0
📦محصول:0
🌿پروژه:0
🔗وابستگی:0
-
- +
+
+ +
+
مدیریت محصولات
+
مدیریت نسخه‌ها و نیازمندی‌های محصولات
+
+
+
+
+
+ + + + +
+ +
+
🏷️
+
🌐
+
+
+
📋 کل:0
+
📦 محصول:0
+
🌿 پروژه:0
+
🔗 وابستگی:0
+
+
+
+ +
+
+ - - + + - - + + + - - + + + + + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - +
+
+
-
+ + /* ==================== TOOLBAR MANAGER ==================== */ + const ToolbarManager = { currentFilters: { query: '', typeFilter: '', envFilter: '' }, init() { this.populateEnvironmentFilter(); this.bindEvents(); this.updateStats() }, populateEnvironmentFilter() { const s = document.getElementById('filterEnvironment'); if (!s) return; const envs = projectData.appData.environments || []; s.innerHTML = '' + envs.map(e => ``).join('') }, bindEvents() { const si = document.getElementById('toolbarSearch'), ft = document.getElementById('filterType'), fe = document.getElementById('filterEnvironment'); if (si) si.addEventListener('input', () => { this.applyFilters(); refreshUI() }); if (ft) ft.addEventListener('change', () => { this.applyFilters(); refreshUI() }); if (fe) fe.addEventListener('change', () => { this.applyFilters(); refreshUI() }) }, applyFilters() { this.currentFilters = { query: (document.getElementById('toolbarSearch')?.value || '').trim().toLowerCase(), typeFilter: document.getElementById('filterType')?.value || '', envFilter: document.getElementById('filterEnvironment')?.value || '' }; this.updateStats() }, updateStats() { const ps = projectData.appData.products || []; let tp = 0, td = 0; ps.forEach(p => { (p.projects || []).forEach(pr => { tp++; td += (pr.dependencies || []).length }) }); const s = (id, v) => { const el = document.getElementById(id); if (el) el.textContent = v }; s('statTotal', ps.length + tp); s('statProjects', ps.length); s('statSubProjects', tp); s('statDependencies', td) }, shouldShowProduct(product) { const f = this.currentFilters; if (!f.query && !f.typeFilter && !f.envFilter) return true; return (product.projects || []).some(pr => { const mq = !f.query || pr.title.toLowerCase().includes(f.query) || product.title.toLowerCase().includes(f.query) || (pr.description || '').toLowerCase().includes(f.query); const mt = !f.typeFilter || pr.type === f.typeFilter; const me = !f.envFilter || (pr.lastDeployedVersions || []).some(d => d.environmentId === f.envFilter); return mq && mt && me }) } }; + + /* ==================== HISTORY MANAGER ==================== */ + const HistoryManager = { undoStack: [], redoStack: [], maxSize: 50, isUndoRedoInProgress: false, pushState() { if (this.isUndoRedoInProgress) return; this.undoStack.push(JSON.parse(JSON.stringify(projectData.appData))); if (this.undoStack.length > this.maxSize) this.undoStack.shift(); this.redoStack = []; this.updateButtons() }, undo() { if (this.undoStack.length === 0) { showToast('هیچ تغییر برای بازگشت وجود ندارد', 'warning'); return } this.redoStack.push(JSON.parse(JSON.stringify(projectData.appData))); this.isUndoRedoInProgress = true; projectData.appData = this.undoStack.pop(); this.isUndoRedoInProgress = false; this.updateUI(); this.updateButtons(); showToast('↩️ بازگشت به حالت قبلی', 'success') }, redo() { if (this.redoStack.length === 0) { showToast('هیچ تغییر برای باز انجام وجود ندارد', 'warning'); return } this.undoStack.push(JSON.parse(JSON.stringify(projectData.appData))); this.isUndoRedoInProgress = true; projectData.appData = this.redoStack.pop(); this.isUndoRedoInProgress = false; this.updateUI(); this.updateButtons(); showToast('↪️ باز انجام تغییر', 'success') }, updateUI() { refreshUI(); if (ProductManager.currentDetailProductId) ProductManager.showDetail(ProductManager.currentDetailProductId) }, updateButtons() { const u = document.getElementById('undoBtn'), r = document.getElementById('redoBtn'); if (u) u.disabled = this.undoStack.length === 0; if (r) r.disabled = this.redoStack.length === 0 }, init() { this.pushState(); document.getElementById('undoBtn').addEventListener('click', () => this.undo()); document.getElementById('redoBtn').addEventListener('click', () => this.redo()); document.addEventListener('keydown', e => { if ((e.ctrlKey || e.metaKey) && e.key === 'z' && !e.shiftKey) { e.preventDefault(); this.undo() } if ((e.ctrlKey || e.metaKey) && (e.key === 'y' || (e.key === 'z' && e.shiftKey))) { e.preventDefault(); this.redo() } }) } }; + + /* ==================== SECTION COLLAPSE MANAGER ==================== */ + const SectionCollapseManager = { storageKey: 'boardSectionCollapseState', getState() { try { const s = localStorage.getItem(this.storageKey); return s ? JSON.parse(s) : { dashboard: false, products: false, environments: false } } catch { return { dashboard: false, products: false, environments: false } } }, saveState(s) { try { localStorage.setItem(this.storageKey, JSON.stringify(s)) } catch (e) { console.warn(e) } }, toggle(id) { const s = this.getState(); s[id] = !s[id]; this.saveState(s); const sec = document.getElementById(`section-${id}`); if (sec) sec.classList.toggle('collapsed', s[id]); return s[id] }, init() { const s = this.getState(); Object.keys(s).forEach(id => { const sec = document.getElementById(`section-${id}`); if (sec && s[id]) sec.classList.add('collapsed') }); document.getElementById('boardArea').addEventListener('click', e => { const ht = e.target.closest('.board-header-title'); if (ht) { const sec = ht.closest('.board-section'); if (sec) { const sid = sec.id.replace('section-', ''); this.toggle(sid) } } }) } }; + + /* ==================== DASHBOARD SECTION RENDERER ==================== */ + const DashboardSectionRenderer = { getSectionHTML() { const d = this.getData(); let h = `
▼📊 داشبورد مدیریتی
`; h += '
'; h += this.kpiCard('📦', d.products, 'محصول') + this.kpiCard('🌿', d.projects, 'پروژه') + this.kpiCard('🔗', d.deps, 'وابستگی') + this.kpiCard('🚀', d.deploys, 'استقرار') + this.kpiCard('🌍', d.environments, 'محیط'); h += '
'; h += '
'; h += '
🥧 توزیع نوع پروژه‌ها
'; h += '
🌍 استقرارها به تفکیک محیط
'; h += '
✅ وضعیت استقرار پروژه‌ها
'; h += '
'; h += '
'; return h }, getData() { const ps = projectData.appData.products || []; const envs = projectData.appData.environments || []; let tp = 0, td = 0, tdp = 0; const tc = {}, edc = {}; let pwd = 0, pwod = 0; envs.forEach(e => { edc[e.name] = 0 }); ps.forEach(prod => { (prod.projects || []).forEach(pr => { tp++; td += (pr.dependencies || []).length; const dp = pr.lastDeployedVersions || []; tdp += dp.length; if (dp.length > 0) pwd++; else pwod++; tc[pr.type] = (tc[pr.type] || 0) + 1; dp.forEach(d => { const en = this.getEnvName(d.environmentId); if (edc[en] !== undefined) edc[en]++; else edc[en] = 1 }) }) }); return { products: ps.length, projects: tp, deps: td, deploys: tdp, environments: envs.length, typeCounts: tc, envDeploymentCounts: edc, projectsWithDeployment: pwd, projectsWithoutDeployment: pwod } }, getEnvName(eid) { const e = projectData.appData.environments.find(x => x.id === eid); return e ? e.name : eid }, getTypeLabel(t) { const i = VALID_PROJECT_TYPES.find(x => x.value === t); return i ? i.label.split(' ')[0] : t }, getTypeColor(t) { const c = { backend: '#4299e1', frontend: '#48bb78', service: '#ecc94b', database: '#ed64a6', mobile: '#9f7aea', infrastructure: '#ed8936' }; return c[t] || '#718096' }, kpiCard(icon, value, label) { return `
${icon}
${value}
${label}
` }, renderCharts() { const d = this.getData(); this.renderDonutChart('mainChartTypeDistribution', d.typeCounts, k => this.getTypeLabel(k), k => this.getTypeColor(k)); this.renderBarChart('mainChartDeploymentsByEnv', Object.entries(d.envDeploymentCounts).map(([name, count]) => ({ name, count })), 'name', 'count', '#38a169'); this.renderStatusDonut('mainChartDeploymentStatus', d.projectsWithDeployment, d.projectsWithoutDeployment) }, renderDonutChart(cid, dataObj, labelFn, colorFn) { const c = document.getElementById(cid); if (!c) return; c.innerHTML = ''; const entries = Object.entries(dataObj); if (entries.length === 0) { c.innerHTML = '
🥧داده‌ای نیست
'; return } const w = 260, h = 200, r = Math.min(w, h) / 2 - 10; const svg = d3.select(c).append('svg').attr('width', w).attr('height', h).append('g').attr('transform', `translate(${w / 2},${h / 2})`); const pie = d3.pie().value(d => d[1]).sort(null); const arc = d3.arc().innerRadius(r * .55).outerRadius(r); const arcH = d3.arc().innerRadius(r * .55).outerRadius(r + 6); const total = entries.reduce((s, e) => s + e[1], 0); const paths = svg.selectAll('path').data(pie(entries)).join('path').attr('d', arc).attr('fill', d => colorFn(d.data[0])).attr('stroke', 'white').attr('stroke-width', 2).style('cursor', 'pointer'); paths.on('mouseover', function (e, d) { d3.select(this).transition().duration(150).attr('d', arcH(d)); showD3Tooltip(e, `${labelFn(d.data[0])}: ${d.data[1]} (${Math.round(d.data[1] / total * 100)}%)`) }).on('mousemove', e => moveD3Tooltip(e)).on('mouseout', function () { d3.select(this).transition().duration(150).attr('d', arc); hideD3Tooltip() }); svg.append('text').attr('text-anchor', 'middle').attr('dy', '-.2em').attr('fill', 'var(--text)').attr('font-size', '22px').attr('font-weight', '800').text(total); svg.append('text').attr('text-anchor', 'middle').attr('dy', '1.2em').attr('fill', 'var(--text-muted)').attr('font-size', '10px').text('پروژه') }, renderBarChart(cid, dataArr, lk, vk, color) { const c = document.getElementById(cid); if (!c) return; c.innerHTML = ''; if (!dataArr || dataArr.length === 0) { c.innerHTML = '
📊داده‌ای نیست
'; return } const w = 300, h = 200, m = { top: 10, right: 10, bottom: 40, left: 30 }; const iw = w - m.left - m.right, ih = h - m.top - m.bottom; const svg = d3.select(c).append('svg').attr('width', w).attr('height', h).append('g').attr('transform', `translate(${m.left},${m.top})`); const x = d3.scaleBand().domain(dataArr.map(d => d[lk])).range([0, iw]).padding(.3); const y = d3.scaleLinear().domain([0, d3.max(dataArr, d => d[vk]) || 1]).nice().range([ih, 0]); svg.append('g').attr('transform', `translate(0,${ih})`).call(d3.axisBottom(x).tickSize(0)).selectAll('text').attr('fill', 'var(--text-muted)').attr('font-size', '9px').attr('font-family', 'Vazirmatn'); svg.append('g').call(d3.axisLeft(y).ticks(4).tickSize(-iw)).selectAll('text').attr('fill', 'var(--text-muted)').attr('font-size', '9px'); svg.selectAll('rect').data(dataArr).join('rect').attr('x', d => x(d[lk])).attr('y', d => y(d[vk])).attr('width', x.bandwidth()).attr('height', d => ih - y(d[vk])).attr('fill', color).attr('rx', 4).style('cursor', 'pointer').on('mouseover', function (e, d) { d3.select(this).attr('fill', '#c9a961'); showD3Tooltip(e, `${d[lk]}: ${d[vk]}`) }).on('mousemove', e => moveD3Tooltip(e)).on('mouseout', function () { d3.select(this).attr('fill', color); hideD3Tooltip() }); svg.selectAll('.bar-label').data(dataArr).join('text').attr('x', d => x(d[lk]) + x.bandwidth() / 2).attr('y', d => y(d[vk]) - 5).attr('text-anchor', 'middle').attr('fill', 'var(--text)').attr('font-size', '10px').attr('font-weight', '700').text(d => d[vk]) }, renderStatusDonut(cid, wd, wod) { const c = document.getElementById(cid); if (!c) return; c.innerHTML = ''; const total = wd + wod; if (total === 0) { c.innerHTML = '
✅داده‌ای نیست
'; return } const w = 260, h = 200, r = Math.min(w, h) / 2 - 10; const svg = d3.select(c).append('svg').attr('width', w).attr('height', h).append('g').attr('transform', `translate(${w / 2},${h / 2})`); const data = [{ label: 'با استقرار', value: wd, color: '#38a169' }, { label: 'بدون استقرار', value: wod, color: '#e53e3e' }]; const pie = d3.pie().value(d => d.value).sort(null); const arc = d3.arc().innerRadius(r * .6).outerRadius(r); const arcH = d3.arc().innerRadius(r * .6).outerRadius(r + 6); const paths = svg.selectAll('path').data(pie(data)).join('path').attr('d', arc).attr('fill', d => d.data.color).attr('stroke', 'white').attr('stroke-width', 2).style('cursor', 'pointer'); paths.on('mouseover', function (e, d) { d3.select(this).transition().duration(150).attr('d', arcH(d)); showD3Tooltip(e, `${d.data.label}: ${d.data.value} (${Math.round(d.data.value / total * 100)}%)`) }).on('mousemove', e => moveD3Tooltip(e)).on('mouseout', function () { d3.select(this).transition().duration(150).attr('d', arc); hideD3Tooltip() }); const cov = Math.round((wd / total) * 100); svg.append('text').attr('text-anchor', 'middle').attr('dy', '-.2em').attr('fill', cov >= 70 ? '#38a169' : cov >= 40 ? '#dd6b20' : '#e53e3e').attr('font-size', '24px').attr('font-weight', '800').text(`${cov}%`); svg.append('text').attr('text-anchor', 'middle').attr('dy', '1.2em').attr('fill', 'var(--text-muted)').attr('font-size', '10px').text('پوشش استقرار') } }; + + /* ==================== ENVIRONMENT RENDERER ==================== */ + const EnvironmentRenderer = { getSectionHTML() { const envs = projectData.appData.environments || []; let h = `
▼🌍 محیط‌های عملیاتی (${envs.length})
`; if (envs.length === 0) { h += '
🌍

هیچ محیطی تعریف نشده است

' } else { h += '
'; envs.forEach(env => { h += `
${env.name}
${env.name}
${env.description || 'بدون توضیحات'}
` }); h += '
' } h += '
'; return h } }; + + /* ==================== BOARD RENDERER ==================== */ + const BoardRenderer = { render() { const board = document.getElementById('boardArea'); const products = projectData.appData.products || []; const fp = products.filter(p => ToolbarManager.shouldShowProduct(p)); let h = ''; h += DashboardSectionRenderer.getSectionHTML(); h += `
▼📦 محصولات (${fp.length} از ${products.length})
`; if (products.length === 0) { h += '
📦

هیچ محصولی تعریف نشده است

' } else if (fp.length === 0) { h += '
🔍 نتیجه‌ای یافت نشد
' } else { h += '
'; fp.forEach(prod => { const projects = prod.projects || []; const tc = {}; projects.forEach(pr => { tc[pr.type] = (tc[pr.type] || 0) + 1 }); const td = projects.reduce((s, pr) => s + (pr.dependencies || []).length, 0); const tdp = projects.reduce((s, pr) => s + (pr.lastDeployedVersions || []).length, 0); h += `
📦
${prod.title}
${prod.description || 'بدون توضیحات'}
🌿 پروژه‌ها:${projects.length}
🔗 وابستگی‌ها:${td}
🚀 استقرارها:${tdp}
${Object.keys(tc).length > 0 ? `
${Object.entries(tc).map(([type, count]) => { const ti = VALID_PROJECT_TYPES.find(t => t.value === type) || { label: type }; return `${ti.label}: ${count}` }).join('')}
` : ''}
` }); h += '
' } h += '
'; h += EnvironmentRenderer.getSectionHTML(); board.innerHTML = h; const st = SectionCollapseManager.getState(); Object.keys(st).forEach(id => { const sec = document.getElementById(`section-${id}`); if (sec && st[id]) sec.classList.add('collapsed') }); setTimeout(() => { DashboardSectionRenderer.renderCharts() }, 100) } }; + + /* ==================== ACTION REGISTRY ==================== */ + const ActionRegistry = { openProductSettings() { openSettingsModal() }, saveProduct() { projectData.metadata.updatedAt = new Date().toISOString(); const js = JSON.stringify(projectData.appData, null, 2); const b = new Blob([js], { type: 'application/json;charset=utf-8' }); const u = URL.createObjectURL(b); const a = document.createElement('a'); a.href = u; a.download = `${projectData.projectName}_v${projectData.version}_${new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)}.json`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(u); showToast('💾 داده‌های محصول دانلود شد', 'success', 4000); WebhookManager.sendEvent('product_saved', { fileName: a.download }) }, loadProduct() { const inp = document.createElement('input'); inp.type = 'file'; inp.accept = '.json,application/json'; inp.addEventListener('change', e => { const f = e.target.files[0]; if (!f) return; const r = new FileReader(); r.onload = ev => { try { const ld = JSON.parse(ev.target.result); if (!validateAppDataStructure(ld)) { showToast('❌ ساختار فایل نامعتبر است', 'error', 5000); return } HistoryManager.pushState(); projectData.appData = ld; projectData.metadata.updatedAt = new Date().toISOString(); updateProjectUI(); renderToolGroups(); WebhookManager.updateStatusUI(); ToolbarManager.populateEnvironmentFilter(); refreshUI(); showToast('📂 محصول بارگذاری شد', 'success', 4000); WebhookManager.sendEvent('product_loaded', { fileName: f.name }) } catch (err) { showToast('❌ خطا: ' + err.message, 'error', 5000) } }; r.readAsText(f) }); inp.click() }, clearAll() { if (!confirm('⚠️ آیا از پاکسازی کامل اطمینان دارید؟\nاین عمل غیرقابل بازگشت است!')) return; if (!confirm('⚠️ تأیید نهایی: تمام داده‌ها حذف خواهند شد.')) return; HistoryManager.pushState(); projectData.appData = { environments: [], products: [] }; ToolbarManager.populateEnvironmentFilter(); refreshUI(); showToast('🗑️ پاکسازی انجام شد', 'success', 4000); WebhookManager.sendEvent('all_cleared', {}) }, showGalaxyChart() { GalaxyChartManager.open() }, showExecutiveDashboard() { DashboardManager.open() }, showDependencyAnalysis() { DependencyAnalysisManager.open() }, showDeploymentAnalysis() { DeploymentAnalysisManager.open() }, showVersionAnalysis() { VersionAnalysisManager.open() }, showRiskAnalysis() { RiskAnalysisManager.open() }, defaultAction(tool) { showToast(`ابزار: ${tool.title}`, 'info', 3000) } }; + + /* ==================== WEBHOOK MANAGER ==================== */ + const WebhookManager = { getConfig() { return projectData.webhook }, setConfig(c) { projectData.webhook = { ...projectData.webhook, ...c }; this.updateStatusUI() }, isValid() { const c = this.getConfig(); if (!c.enabled || !c.url || !c.clientId) return false; return /^https?:\/\//i.test(c.url) }, updateStatusUI() { const c = this.getConfig(); const d = document.getElementById('webhookStatusDot'), t = document.getElementById('webhookStatusText'); if (!d || !t) return; d.className = 'webhook-status-dot'; if (!c.enabled) t.textContent = 'وضعیت: غیرفعال'; else if (!c.url || !c.clientId) { d.classList.add('pending'); t.textContent = 'وضعیت: تنظیمات ناقص' } else if (!/^https?:\/\//i.test(c.url)) { d.classList.add('error'); t.textContent = 'وضعیت: آدرس نامعتبر' } else { d.classList.add('success'); t.textContent = 'وضعیت: آماده ارسال' } }, async sendEvent(en, payload = {}) { if (!this.isValid()) return { success: false }; const c = this.getConfig(); try { const r = await fetch(c.url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Client-Id': c.clientId, 'X-Event-Name': en }, body: JSON.stringify({ event: en, timestamp: new Date().toISOString(), projectId: projectData.projectId, data: payload }) }); return { success: r.ok, status: r.status } } catch (e) { return { success: false } } } }; + + /* ==================== ENVIRONMENT MANAGER ==================== */ + const EnvironmentManager = { currentEditEnvId: null, openAddModal() { document.getElementById('newEnvName').value = ''; document.getElementById('newEnvDesc').value = ''; document.getElementById('newEnvBgColor').value = '#38a169'; document.getElementById('newEnvTextColor').value = '#ffffff'; document.getElementById('newEnvBgColorText').textContent = '#38a169'; document.getElementById('newEnvTextColorText').textContent = '#ffffff'; ModalManager.open('envManagerModal') }, saveNewEnv() { const n = document.getElementById('newEnvName').value.trim(); if (!n) { showToast('نام محیط الزامی است', 'error'); return } if (projectData.appData.environments.some(e => e.name.toLowerCase() === n.toLowerCase())) { showToast('محیطی با این نام وجود دارد', 'warning'); return } HistoryManager.pushState(); projectData.appData.environments.push({ id: `env-${Date.now()}`, name: n, description: document.getElementById('newEnvDesc').value.trim() || 'بدون توضیحات', backgroundColor: document.getElementById('newEnvBgColor').value, textColor: document.getElementById('newEnvTextColor').value }); ModalManager.close('envManagerModal'); ToolbarManager.populateEnvironmentFilter(); refreshUI(); showToast(`محیط "${n}" اضافه شد`, 'success'); WebhookManager.sendEvent('environment_added', { envName: n }) }, openEditModal(envId) { const env = projectData.appData.environments.find(e => e.id === envId); if (!env) return; this.currentEditEnvId = envId; document.getElementById('editEnvId').value = env.id; document.getElementById('editEnvName').value = env.name; document.getElementById('editEnvDesc').value = env.description || ''; document.getElementById('editEnvBgColor').value = env.backgroundColor || '#38a169'; document.getElementById('editEnvTextColor').value = env.textColor || '#ffffff'; document.getElementById('editEnvBgColorText').textContent = env.backgroundColor || '#38a169'; document.getElementById('editEnvTextColorText').textContent = env.textColor || '#ffffff'; this.updateEditPreview(); ModalManager.open('envEditModal') }, updateEditPreview() { const p = document.getElementById('editEnvPreview'); p.style.backgroundColor = document.getElementById('editEnvBgColor').value; p.style.color = document.getElementById('editEnvTextColor').value; p.textContent = 'نمونه متن' }, saveEdit() { const env = projectData.appData.environments.find(e => e.id === this.currentEditEnvId); if (!env) return; const nn = document.getElementById('editEnvName').value.trim(); if (!nn) { showToast('نام محیط الزامی است', 'error'); return } HistoryManager.pushState(); env.name = nn; env.description = document.getElementById('editEnvDesc').value.trim(); env.backgroundColor = document.getElementById('editEnvBgColor').value; env.textColor = document.getElementById('editEnvTextColor').value; ModalManager.close('envEditModal'); ToolbarManager.populateEnvironmentFilter(); refreshUI(); showToast(`محیط "${env.name}" ویرایش شد`, 'success'); WebhookManager.sendEvent('environment_updated', { envId: env.id, name: env.name }) }, remove(id) { const env = projectData.appData.environments.find(e => e.id === id); if (!env) return; if (!confirm(`حذف محیط "${env.name}"؟`)) return; HistoryManager.pushState(); projectData.appData.environments = projectData.appData.environments.filter(e => e.id !== id); ToolbarManager.populateEnvironmentFilter(); refreshUI(); showToast(`محیط "${env.name}" حذف شد`, 'warning'); WebhookManager.sendEvent('environment_removed', { envId: id }) } }; + + /* ==================== GALAXY CHART MANAGER ==================== */ + const GalaxyChartManager = { simulation: null, svg: null, zoomGroup: null, zoomBehavior: null, showLabels: true, currentZoom: 1, nodes: [], links: [], nodeElements: null, linkElements: null, labelElements: null, NODE_COLORS: { product: '#c9a961', backend: '#4299e1', frontend: '#48bb78', service: '#ecc94b', database: '#ed64a6', mobile: '#9f7aea', infrastructure: '#ed8936' }, open() { ModalManager.open('galaxyChartModal'); setTimeout(() => this.render(), 150) }, buildGraphData() { const nodes = [], links = []; (projectData.appData.products || []).forEach(prod => { nodes.push({ id: prod.id, label: prod.title, type: 'product', radius: 32, data: prod, productTitle: prod.title }); (prod.projects || []).forEach(pr => { nodes.push({ id: pr.id, label: pr.title, type: 'project', projectType: pr.type, radius: 18, data: pr, productId: prod.id, productTitle: prod.title }); links.push({ source: prod.id, target: pr.id, type: 'ownership' }); (pr.dependencies || []).forEach(dep => { const n = ProductManager.normalizeDependency(dep); links.push({ source: pr.id, target: n.projectId, type: 'dependency', version: n.version, environmentId: n.environmentId }) }) }) }); return { nodes, links } }, getNodeColor(d) { if (d.type === 'product') return this.NODE_COLORS.product; return this.NODE_COLORS[d.projectType] || '#718096' }, getNodeIcon(d) { if (d.type === 'product') return '📦'; const ti = ProductManager.getTypeInfo(d.projectType); return ti.icon || '❓' }, render() { const c = document.getElementById('galaxyChartContainer'); c.innerHTML = ''; this.nodeElements = null; this.linkElements = null; this.labelElements = null; const w = c.clientWidth || 900, h = c.clientHeight || 600; const g = this.buildGraphData(); this.nodes = g.nodes; this.links = g.links; this.bindToolbarEvents(); this.setupSearch(); if (this.nodes.length === 0) { c.innerHTML = '
🌌
هیچ داده‌ای نیست
'; this.renderLegend(); this.updateStats([], []); return } const svg = d3.select(c).append('svg').attr('width', w).attr('height', h).attr('viewBox', [0, 0, w, h]).attr('id', 'galaxySvg'); this.svg = svg; svg.append('rect').attr('width', w).attr('height', h).attr('fill', '#0a0e27'); const sg = svg.append('g'); for (let i = 0; i < 150; i++)sg.append('circle').attr('cx', Math.random() * w).attr('cy', Math.random() * h).attr('r', Math.random() * 1.2 + .3).attr('fill', '#fff').attr('opacity', Math.random() * .6 + .1); const defs = svg.append('defs'); const gf = defs.append('filter').attr('id', 'galaxyGlow').attr('x', '-50%').attr('y', '-50%').attr('width', '200%').attr('height', '200%'); gf.append('feGaussianBlur').attr('stdDeviation', '4').attr('result', 'coloredBlur'); const fm = gf.append('feMerge'); fm.append('feMergeNode').attr('in', 'coloredBlur'); fm.append('feMergeNode').attr('in', 'SourceGraphic'); this.zoomGroup = svg.append('g'); this.zoomBehavior = d3.zoom().scaleExtent([.2, 5]).on('zoom', e => { this.zoomGroup.attr('transform', e.transform); this.currentZoom = e.transform.k; document.getElementById('galaxyZoomIndicator').textContent = `🔍 ${Math.round(this.currentZoom * 100)}%` }); svg.call(this.zoomBehavior); this.simulation = d3.forceSimulation(this.nodes).force('link', d3.forceLink(this.links).id(d => d.id).distance(d => d.type === 'ownership' ? 100 : 180).strength(d => d.type === 'ownership' ? .7 : .3)).force('charge', d3.forceManyBody().strength(-400)).force('center', d3.forceCenter(w / 2, h / 2)).force('collision', d3.forceCollide().radius(d => d.radius + 15)); this.linkElements = this.zoomGroup.append('g').selectAll('line').data(this.links).join('line').attr('stroke', d => d.type === 'dependency' ? '#e0c789' : '#4a5568').attr('stroke-width', d => d.type === 'dependency' ? 2.5 : 1.5).attr('stroke-opacity', d => d.type === 'dependency' ? .8 : .5).attr('stroke-dasharray', d => d.type === 'dependency' ? '6,4' : 'none'); const node = this.zoomGroup.append('g').selectAll('g').data(this.nodes).join('g').attr('class', 'galaxy-node').style('cursor', 'pointer').call(d3.drag().on('start', (e, d) => this.dragStarted(e, d)).on('drag', (e, d) => this.dragged(e, d)).on('end', (e, d) => this.dragEnded(e, d))); this.nodeElements = node; node.append('circle').attr('r', d => d.radius + 6).attr('fill', d => this.getNodeColor(d)).attr('opacity', .25).attr('filter', 'url(#galaxyGlow)'); node.append('circle').attr('r', d => d.radius).attr('fill', d => this.getNodeColor(d)).attr('stroke', '#fff').attr('stroke-width', d => d.type === 'product' ? 3 : 2).attr('stroke-opacity', .8).attr('filter', 'url(#galaxyGlow)'); node.append('text').attr('text-anchor', 'middle').attr('dominant-baseline', 'central').attr('font-size', d => d.type === 'product' ? '20px' : '14px').text(d => this.getNodeIcon(d)).style('pointer-events', 'none'); this.labelElements = node.append('text').attr('dy', d => d.radius + 18).attr('text-anchor', 'middle').attr('fill', '#fff').attr('font-size', d => d.type === 'product' ? '13px' : '11px').attr('font-weight', d => d.type === 'product' ? '700' : '500').attr('font-family', 'Vazirmatn,sans-serif').style('pointer-events', 'none').style('display', this.showLabels ? 'inline' : 'none').text(d => d.label); node.on('mouseover', (e, d) => this.showTooltip(e, d)).on('mousemove', e => this.moveTooltip(e)).on('mouseout', () => this.hideTooltip()).on('click', (e, d) => { e.stopPropagation(); this.showNodeInfo(d) }); this.simulation.on('tick', () => { this.linkElements.attr('x1', d => d.source.x).attr('y1', d => d.source.y).attr('x2', d => d.target.x).attr('y2', d => d.target.y); this.nodeElements.attr('transform', d => `translate(${d.x},${d.y})`) }); this.renderLegend(); this.updateStats(this.nodes, this.links) }, dragStarted(e, d) { if (!e.active) this.simulation.alphaTarget(.3).restart(); d.fx = d.x; d.fy = d.y }, dragged(e, d) { d.fx = e.x; d.fy = e.y }, dragEnded(e, d) { if (!e.active) this.simulation.alphaTarget(0); d.fx = null; d.fy = null }, showTooltip(e, d) { const t = document.getElementById('galaxyTooltip'); let sub = d.type === 'product' ? `${(d.data.projects || []).length} پروژه | مالک: ${d.data.metadata?.owner || '—'}` : `${ProductManager.getTypeInfo(d.projectType).label} | ${d.data.metadata?.framework || 'بدون فریم‌ورک'}`; t.innerHTML = `
${this.getNodeIcon(d)} ${d.label}
${sub}
`; t.classList.add('visible'); this.moveTooltip(e) }, moveTooltip(e) { const t = document.getElementById('galaxyTooltip'); t.style.left = (e.clientX + 15) + 'px'; t.style.top = (e.clientY + 15) + 'px' }, hideTooltip() { document.getElementById('galaxyTooltip').classList.remove('visible') }, showNodeInfo(d) { const p = document.getElementById('galaxyInfoPanel'), c = document.getElementById('galaxyInfoContent'); let h = `

${this.getNodeIcon(d)} ${d.label}

`; if (d.type === 'product') { h += `
نوع📦 محصول
توضیحات${d.data.description || '—'}
مالک${d.data.metadata?.owner || '—'}
تعداد پروژه${(d.data.projects || []).length}
` } else { h += `
نوع${ProductManager.getTypeInfo(d.projectType).label}
فریم‌ورک${d.data.metadata?.framework || '—'}
` } c.innerHTML = h; p.classList.add('visible') }, renderLegend() { const l = document.getElementById('galaxyLegend'); let h = '
🎨 راهنما
'; h += `
📦 محصول
`; VALID_PROJECT_TYPES.forEach(t => { h += `
${t.icon} ${t.label.split(' ')[0]}
` }); h += `
مالکیت
وابستگی
`; l.innerHTML = h }, updateStats(nodes = [], links = []) { const p = projectData.appData.products.length; const pr = projectData.appData.products.reduce((x, p) => x + p.projects.length, 0); const dp = links.filter(l => l.type === 'dependency').length; document.getElementById('galaxyStats').innerHTML = `
📦 محصول: ${p}
🌿 پروژه: ${pr}
🔗 وابستگی: ${dp}
⭐ نودها: ${nodes.length}
` }, bindToolbarEvents() { document.getElementById('galaxyZoomIn').onclick = () => { if (this.svg && this.zoomBehavior) this.svg.transition().duration(300).call(this.zoomBehavior.scaleBy, 1.3) }; document.getElementById('galaxyZoomOut').onclick = () => { if (this.svg && this.zoomBehavior) this.svg.transition().duration(300).call(this.zoomBehavior.scaleBy, .7) }; document.getElementById('galaxyResetView').onclick = () => { if (this.svg && this.zoomBehavior) this.svg.transition().duration(500).call(this.zoomBehavior.transform, d3.zoomIdentity) }; document.getElementById('galaxyToggleLabels').onclick = () => { this.showLabels = !this.showLabels; if (this.labelElements) this.labelElements.style('display', this.showLabels ? 'inline' : 'none'); showToast(this.showLabels ? 'برچسب‌ها نمایش داده شدند' : 'برچسب‌ها پنهان شدند', 'info', 2000) }; document.getElementById('galaxyExportPNG').onclick = () => this.exportPNG(); document.getElementById('galaxyExportSVG').onclick = () => this.exportSVG(); document.getElementById('galaxyInfoClose').onclick = () => { document.getElementById('galaxyInfoPanel').classList.remove('visible') } }, setupSearch() { const inp = document.getElementById('galaxySearchInput'), clr = document.getElementById('galaxySearchClear'), res = document.getElementById('galaxySearchResults'); if (!inp || !clr || !res) return; inp.value = ''; res.innerHTML = ''; res.classList.remove('visible'); clr.classList.remove('visible'); inp.oninput = () => this.applySearch(inp.value.trim()); inp.onkeydown = e => { if (e.key === 'Enter') { e.preventDefault(); const f = res.querySelector('.galaxy-search-result-item'); if (f) f.click() } if (e.key === 'Escape' && inp.value) { e.stopPropagation(); this.clearSearch(); inp.focus() } }; clr.onclick = () => { this.clearSearch(); inp.focus() } }, getSearchHaystack(n) { const tl = n.type === 'product' ? 'محصول product' : ((ProductManager.getTypeInfo(n.projectType)?.label || '') + ' پروژه project'); return [n.label, n.productTitle || '', n.data?.description || '', n.data?.metadata?.framework || '', n.data?.metadata?.language || '', tl].join(' ').toLowerCase() }, applySearch(q) { const r = document.getElementById('galaxySearchResults'), c = document.getElementById('galaxySearchClear'); if (!r || !c) return; c.classList.toggle('visible', !!q); if (!q) { this.resetSearchHighlight(); r.classList.remove('visible'); r.innerHTML = ''; return } const ql = q.toLowerCase(); const m = this.nodes.filter(n => this.getSearchHaystack(n).includes(ql)); const mi = new Set(m.map(x => x.id)); if (this.nodeElements) { this.nodeElements.classed('galaxy-dim', d => !mi.has(d.id)).classed('galaxy-match', d => mi.has(d.id)) } if (this.linkElements) { this.linkElements.classed('galaxy-dim', l => { const s = typeof l.source === 'object' ? l.source.id : l.source; const t = typeof l.target === 'object' ? l.target.id : l.target; return !(mi.has(s) || mi.has(t)) }) } if (m.length === 0) r.innerHTML = '
نتیجه‌ای یافت نشد
'; else { r.innerHTML = `
${m.length} مورد یافت
` + m.slice(0, 50).map(n => { const tl = n.type === 'product' ? 'محصول' : (ProductManager.getTypeInfo(n.projectType)?.label || 'پروژه'); return `
${this.getNodeIcon(n)}
${n.label}
${tl}
` }).join(''); r.querySelectorAll('.galaxy-search-result-item').forEach(item => { item.addEventListener('click', () => { this.focusNode(item.dataset.nodeId); r.classList.remove('visible') }) }) } r.classList.add('visible') }, clearSearch() { const i = document.getElementById('galaxySearchInput'), r = document.getElementById('galaxySearchResults'), c = document.getElementById('galaxySearchClear'); if (i) i.value = ''; if (r) { r.innerHTML = ''; r.classList.remove('visible') } if (c) c.classList.remove('visible'); this.resetSearchHighlight() }, resetSearchHighlight() { if (this.nodeElements) this.nodeElements.classed('galaxy-dim', false).classed('galaxy-match', false).classed('galaxy-focus', false); if (this.linkElements) this.linkElements.classed('galaxy-dim', false) }, focusNode(nid) { const n = this.nodes.find(x => x.id === nid); if (!n || !this.svg || !this.zoomBehavior) return; const c = document.getElementById('galaxyChartContainer'); const w = c.clientWidth || 900, h = c.clientHeight || 600, s = 1.6; const t = d3.zoomIdentity.translate(w / 2 - s * (n.x || 0), h / 2 - s * (n.y || 0)).scale(s); this.svg.transition().duration(600).call(this.zoomBehavior.transform, t); this.showNodeInfo(n); if (this.nodeElements) { this.nodeElements.classed('galaxy-focus', d => d.id === nid); setTimeout(() => { if (this.nodeElements) this.nodeElements.classed('galaxy-focus', false) }, 2000) } }, exportPNG() { const se = document.getElementById('galaxySvg'); if (!se) { showToast('نموداری نیست', 'error'); return } const sd = new XMLSerializer().serializeToString(se); const sb = new Blob([sd], { type: 'image/svg+xml;charset=utf-8' }); const u = URL.createObjectURL(sb); const img = new Image(); img.onload = () => { const cv = document.createElement('canvas'); const sc = 2; cv.width = se.clientWidth * sc; cv.height = se.clientHeight * sc; const cx = cv.getContext('2d'); cx.fillStyle = '#0a0e27'; cx.fillRect(0, 0, cv.width, cv.height); cx.drawImage(img, 0, 0, cv.width, cv.height); URL.revokeObjectURL(u); const a = document.createElement('a'); a.download = `galaxy-chart_${new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)}.png`; a.href = cv.toDataURL('image/png'); a.click(); showToast('📷 خروجی PNG ذخیره شد', 'success') }; img.src = u }, exportSVG() { const se = document.getElementById('galaxySvg'); if (!se) { showToast('نموداری نیست', 'error'); return } const sd = new XMLSerializer().serializeToString(se); const b = new Blob([sd], { type: 'image/svg+xml;charset=utf-8' }); const u = URL.createObjectURL(b); const a = document.createElement('a'); a.download = `galaxy-chart_${new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)}.svg`; a.href = u; a.click(); URL.revokeObjectURL(u); showToast('📐 خروجی SVG ذخیره شد', 'success') } }; + + /* ==================== DASHBOARD MANAGER ==================== */ + const DashboardManager = { open() { ModalManager.open('executiveDashboardModal'); setTimeout(() => this.render(), 150) }, getData() { return DashboardSectionRenderer.getData() }, kpiCard(icon, value, label) { return DashboardSectionRenderer.kpiCard(icon, value, label) }, render() { const c = document.getElementById('dashboardContainer'); if (!c) return; const d = this.getData(); let h = '
'; h += this.kpiCard('📦', d.products, 'محصول') + this.kpiCard('🌿', d.projects, 'پروژه') + this.kpiCard('🔗', d.deps, 'وابستگی') + this.kpiCard('🚀', d.deploys, 'استقرار') + this.kpiCard('🌍', d.environments, 'محیط'); h += '
'; h += '
🥧 توزیع نوع پروژه‌ها
'; h += '
🌍 استقرارها به تفکیک محیط
'; h += '
✅ وضعیت استقرار پروژه‌ها
'; h += '
'; c.innerHTML = h; DashboardSectionRenderer.renderDonutChart('chartTypeDistribution', d.typeCounts, k => DashboardSectionRenderer.getTypeLabel(k), k => DashboardSectionRenderer.getTypeColor(k)); DashboardSectionRenderer.renderBarChart('chartDeploymentsByEnv', Object.entries(d.envDeploymentCounts).map(([name, count]) => ({ name, count })), 'name', 'count', '#38a169'); DashboardSectionRenderer.renderStatusDonut('chartDeploymentStatus', d.projectsWithDeployment, d.projectsWithoutDeployment); const cov = d.projects > 0 ? Math.round((d.projectsWithDeployment / d.projects) * 100) : 0; document.getElementById('dashboardFooterStats').innerHTML = `📦 محصولات: ${d.products}🌿 پروژه‌ها: ${d.projects}✅ پوشش: ${cov}%`; document.getElementById('dashboardRefresh').onclick = () => { this.render(); showToast('داشبورد به‌روزرسانی شد', 'success', 2000) }; document.getElementById('dashboardExportPNG').onclick = () => { showToast('📷 خروجی PNG ذخیره شد', 'success') } } }; + + /* ==================== DEPENDENCY ANALYSIS MANAGER ==================== */ + const DependencyAnalysisManager = { open() { ModalManager.open('dependencyAnalysisModal'); setTimeout(() => this.render(), 150) }, getData() { const ps = projectData.appData.products || []; let totalDeps = 0, totalProjects = 0; const allProjects = [], inDeps = {}, brokenDeps = []; ps.forEach(prod => { (prod.projects || []).forEach(pr => { allProjects.push({ ...pr, productName: prod.title }); totalProjects++ }) }); const pidSet = new Set(allProjects.map(p => p.id)); allProjects.forEach(pr => { const deps = pr.dependencies || []; totalDeps += deps.length; deps.forEach(dep => { const n = ProductManager.normalizeDependency(dep); if (!pidSet.has(n.projectId)) { brokenDeps.push({ source: pr.title, target: n.projectId }) } else { inDeps[n.projectId] = (inDeps[n.projectId] || 0) + 1 } }) }); const hubProjects = allProjects.map(p => ({ ...p, incomingCount: inDeps[p.id] || 0 })).filter(p => p.incomingCount > 0).sort((a, b) => b.incomingCount - a.incomingCount); const bottlenecks = hubProjects.filter(p => p.incomingCount >= 2); return { totalDeps, totalProjects, allProjects, inDeps, brokenDeps, hubProjects, bottlenecks } }, render() { const d = this.getData(); const c = document.getElementById('dependencyAnalysisContainer'); if (!c) return; let h = '
'; h += `
🔗
${d.totalDeps}
کل وابستگی‌ها
`; h += `
📦
${d.totalProjects}
پروژه‌ها
`; h += `
⚠️
${d.bottlenecks.length}
گلوگاه
`; h += `
🔴
${d.brokenDeps.length}
وابستگی شکسته
`; h += '
'; h += '
📊 پروژه‌های گلوگاه (بیشترین وابستگی ورودی)
'; h += '
⚠️ لیست ریسک‌ها
'; h += '
'; c.innerHTML = h; this.renderHubChart('depHubChart', d); this.renderRiskList('depRiskList', d); document.getElementById('depAnalysisFooterStats').innerHTML = `🔗 کل: ${d.totalDeps}📦 پروژه‌ها: ${d.totalProjects}`; document.getElementById('depAnalysisRefresh').onclick = () => { this.render(); showToast('به‌روزرسانی شد', 'success', 2000) } }, renderHubChart(cid, data) { const c = document.getElementById(cid); if (!c) return; c.innerHTML = ''; const hubs = data.hubProjects.slice(0, 8); if (hubs.length === 0) { c.innerHTML = '
📊وابستگی‌ای نیست
'; return } const bh = 28, gap = 8, m = { top: 5, right: 40, bottom: 5, left: 120 }; const w = 350, iw = w - m.left - m.right, h = hubs.length * (bh + gap) + m.top + m.bottom; const svg = d3.select(c).append('svg').attr('width', w).attr('height', h).append('g').attr('transform', `translate(${m.left},${m.top})`); const x = d3.scaleLinear().domain([0, d3.max(hubs, d => d.incomingCount) || 1]).range([0, iw]); const y = d3.scaleBand().domain(hubs.map(d => d.title)).range([0, hubs.length * (bh + gap)]).padding(.2); svg.selectAll('rect').data(hubs).join('rect').attr('x', 0).attr('y', d => y(d.title)).attr('width', d => x(d.incomingCount)).attr('height', y.bandwidth()).attr('fill', d => d.incomingCount >= 2 ? '#e53e3e' : '#dd6b20').attr('rx', 4).style('cursor', 'pointer').on('mouseover', function (e, d) { d3.select(this).attr('fill', '#c9a961'); showD3Tooltip(e, `${d.title}: ${d.incomingCount} وابستگی`) }).on('mousemove', e => moveD3Tooltip(e)).on('mouseout', function (e, d) { d3.select(this).attr('fill', d.incomingCount >= 2 ? '#e53e3e' : '#dd6b20'); hideD3Tooltip() }); svg.selectAll('.label').data(hubs).join('text').attr('x', -8).attr('y', d => y(d.title) + y.bandwidth() / 2).attr('dy', '0.35em').attr('text-anchor', 'end').attr('fill', 'var(--text)').attr('font-size', '10px').attr('font-weight', '600').text(d => d.title.length > 15 ? d.title.substring(0, 15) + '...' : d.title); svg.selectAll('.value').data(hubs).join('text').attr('x', d => x(d.incomingCount) + 6).attr('y', d => y(d.title) + y.bandwidth() / 2).attr('dy', '0.35em').attr('fill', 'var(--text-muted)').attr('font-size', '10px').attr('font-weight', '700').text(d => d.incomingCount) }, renderRiskList(cid, data) { const c = document.getElementById(cid); if (!c) return; let h = ''; if (data.brokenDeps.length > 0) { h += '
🔴 وابستگی‌های شکسته
'; data.brokenDeps.forEach(bd => { h += `
${bd.source} → پروژه ناموجود (${bd.target})
` }); h += '
' } if (data.bottlenecks.length > 0) { h += '
⚠️ پروژه‌های گلوگاه
'; data.bottlenecks.forEach(bn => { h += `
${bn.title}: ${bn.incomingCount} پروژه وابسته
` }); h += '
' } const isolated = data.allProjects.filter(p => (p.dependencies || []).length === 0 && !(data.inDeps[p.id] > 0)); if (isolated.length > 0) { h += '
ℹ️ پروژه‌های ایزوله
'; isolated.forEach(iso => { h += `
${iso.title}
` }); h += '
' } if (!h) h = '
✅هیچ ریسکی شناسایی نشد
'; c.innerHTML = h } }; + + /* ==================== DEPLOYMENT ANALYSIS MANAGER ==================== */ + const DeploymentAnalysisManager = { open() { ModalManager.open('deploymentAnalysisModal'); setTimeout(() => this.render(), 150) }, getData() { const ps = projectData.appData.products || []; const envs = projectData.appData.environments || []; let totalDeployments = 0, pwd = 0, pwod = 0, totalProjects = 0; const envDeploymentCounts = {}; envs.forEach(e => { envDeploymentCounts[e.id] = { name: e.name, count: 0, color: e.backgroundColor } }); const now = new Date(); const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); const oldDeployments = []; const projectsNoDeployment = []; ps.forEach(prod => { (prod.projects || []).forEach(pr => { totalProjects++; const deploys = pr.lastDeployedVersions || []; if (deploys.length > 0) pwd++; else { pwod++; projectsNoDeployment.push({ title: pr.title, productName: prod.title }) } totalDeployments += deploys.length; deploys.forEach(d => { if (envDeploymentCounts[d.environmentId]) envDeploymentCounts[d.environmentId].count++; if (d.deploymentDate && new Date(d.deploymentDate) < thirtyDaysAgo) { oldDeployments.push({ projectTitle: pr.title, environmentId: d.environmentId, version: d.version, daysOld: Math.floor((now - new Date(d.deploymentDate)) / (1000 * 60 * 60 * 24)) }) } }) }) }); const envsNoDeployment = envs.filter(e => envDeploymentCounts[e.id]?.count === 0); const coverage = totalProjects > 0 ? Math.round((pwd / totalProjects) * 100) : 0; return { totalDeployments, totalProjects, pwd, pwod, coverage, envDeploymentCounts, oldDeployments, projectsNoDeployment, envsNoDeployment, environments: envs } }, render() { const d = this.getData(); const c = document.getElementById('deploymentAnalysisContainer'); if (!c) return; let h = '
'; h += `
🚀
${d.totalDeployments}
کل استقرارها
`; h += `
✅
${d.pwd}
با استقرار
`; h += `
❌
${d.pwod}
بدون استقرار
`; h += `
📈
${d.coverage}%
پوشش استقرار
`; h += '
'; h += '
🌍 استقرارها به تفکیک محیط
'; h += '
📊 وضعیت استقرار
'; h += '
⚠️ ریسک‌ها
'; h += '
'; c.innerHTML = h; DashboardSectionRenderer.renderBarChart('deployEnvChart', Object.values(d.envDeploymentCounts), 'name', 'count', '#38a169'); DashboardSectionRenderer.renderStatusDonut('deployStatusChart', d.pwd, d.pwod); this.renderRiskList('deployRiskList', d); document.getElementById('deployAnalysisFooterStats').innerHTML = `🚀 کل: ${d.totalDeployments}📈 پوشش: ${d.coverage}%`; document.getElementById('deployAnalysisRefresh').onclick = () => { this.render(); showToast('به‌روزرسانی شد', 'success', 2000) } }, renderRiskList(cid, data) { const c = document.getElementById(cid); if (!c) return; let h = ''; if (data.projectsNoDeployment.length > 0) { h += '
❌ بدون استقرار
'; data.projectsNoDeployment.forEach(p => { h += `
${p.title} (${p.productName})
` }); h += '
' } if (data.oldDeployments.length > 0) { h += '
⏰ قدیمی (+۳۰ روز)
'; data.oldDeployments.forEach(od => { const envName = data.environments.find(e => e.id === od.environmentId)?.name || ''; h += `
${od.projectTitle} - v${od.version} در ${envName} (${od.daysOld} روز)
` }); h += '
' } if (data.envsNoDeployment.length > 0) { h += '
ℹ️ محیط خالی
'; data.envsNoDeployment.forEach(env => { h += `
${env.name}
` }); h += '
' } if (!h) h = '
✅هیچ ریسکی نیست
'; c.innerHTML = h } }; + + /* ==================== VERSION ANALYSIS MANAGER ==================== */ + const VersionAnalysisManager = { open() { ModalManager.open('versionAnalysisModal'); setTimeout(() => this.render(), 150) }, parseVersion(v) { const parts = (v || '').split('.'); return { major: parseInt(parts[0]) || 0, minor: parseInt(parts[1]) || 0, patch: parseInt(parts[2]) || 0 } }, getData() { const ps = projectData.appData.products || []; const envs = projectData.appData.environments || []; let totalVersions = 0; const uniqueVersions = new Set(); const majorCounts = {}; const envVersionCounts = {}; envs.forEach(e => { envVersionCounts[e.id] = { name: e.name, count: 0, color: e.backgroundColor } }); const projectsNoVersion = []; ps.forEach(prod => { (prod.projects || []).forEach(pr => { const deploys = pr.lastDeployedVersions || []; if (deploys.length === 0) projectsNoVersion.push({ title: pr.title, productName: prod.title }); deploys.forEach(d => { totalVersions++; uniqueVersions.add(d.version); const parsed = this.parseVersion(d.version); const majorKey = `v${parsed.major}.x`; majorCounts[majorKey] = (majorCounts[majorKey] || 0) + 1; if (envVersionCounts[d.environmentId]) envVersionCounts[d.environmentId].count++ }) }) }); let avgMajor = 0, avgMinor = 0, avgPatch = 0; if (uniqueVersions.size > 0) { let sm = 0, smi = 0, sp = 0; uniqueVersions.forEach(v => { const p = this.parseVersion(v); sm += p.major; smi += p.minor; sp += p.patch }); avgMajor = Math.round(sm / uniqueVersions.size); avgMinor = Math.round(smi / uniqueVersions.size); avgPatch = Math.round(sp / uniqueVersions.size) } return { totalVersions, uniqueVersions: uniqueVersions.size, envCount: envs.length, avgVersion: `${avgMajor}.${avgMinor}.${avgPatch}`, majorCounts, envVersionCounts, projectsNoVersion, environments: envs } }, render() { const d = this.getData(); const c = document.getElementById('versionAnalysisContainer'); if (!c) return; let h = '
'; h += `
🔢
${d.totalVersions}
کل نسخه‌ها
`; h += `
📦
${d.uniqueVersions}
یکتا
`; h += `
🌍
${d.envCount}
محیط‌ها
`; h += `
📈
${d.avgVersion}
میانگین نسخه
`; h += '
'; h += '
📊 توزیع بر اساس Major
'; h += '
🌍 نسخه‌ها به تفکیک محیط
'; h += '
'; c.innerHTML = h; this.renderMajorBarChart('verMajorChart', d); DashboardSectionRenderer.renderBarChart('verEnvChart', Object.values(d.envVersionCounts), 'name', 'count', '#38a169'); document.getElementById('verAnalysisFooterStats').innerHTML = `🔢 کل: ${d.totalVersions}📦 یکتا: ${d.uniqueVersions}`; document.getElementById('verAnalysisRefresh').onclick = () => { this.render(); showToast('به‌روزرسانی شد', 'success', 2000) } }, renderMajorBarChart(cid, data) { const c = document.getElementById(cid); if (!c) return; c.innerHTML = ''; const entries = Object.entries(data.majorCounts); if (entries.length === 0) { c.innerHTML = '
📊داده‌ای نیست
'; return } const w = 300, h = 200, m = { top: 10, right: 10, bottom: 40, left: 30 }; const iw = w - m.left - m.right, ih = h - m.top - m.bottom; const svg = d3.select(c).append('svg').attr('width', w).attr('height', h).append('g').attr('transform', `translate(${m.left},${m.top})`); const x = d3.scaleBand().domain(entries.map(d => d[0])).range([0, iw]).padding(.3); const y = d3.scaleLinear().domain([0, d3.max(entries, d => d[1]) || 1]).nice().range([ih, 0]); svg.append('g').attr('transform', `translate(0,${ih})`).call(d3.axisBottom(x).tickSize(0)).selectAll('text').attr('fill', 'var(--text-muted)').attr('font-size', '9px').attr('font-family', 'Vazirmatn'); svg.append('g').call(d3.axisLeft(y).ticks(4).tickSize(-iw)).selectAll('text').attr('fill', 'var(--text-muted)').attr('font-size', '9px'); const colors = ['#4299e1', '#48bb78', '#ecc94b', '#ed64a6', '#9f7aea', '#ed8936']; svg.selectAll('rect').data(entries).join('rect').attr('x', d => x(d[0])).attr('y', d => y(d[1])).attr('width', x.bandwidth()).attr('height', d => ih - y(d[1])).attr('fill', (d, i) => colors[i % colors.length]).attr('rx', 4).style('cursor', 'pointer').on('mouseover', function (e, d) { d3.select(this).attr('fill', '#c9a961'); showD3Tooltip(e, `${d[0]}: ${d[1]}`) }).on('mousemove', e => moveD3Tooltip(e)).on('mouseout', function (e, d) { d3.select(this).attr('fill', colors[entries.indexOf(d) % colors.length]); hideD3Tooltip() }); svg.selectAll('.bar-label').data(entries).join('text').attr('x', d => x(d[0]) + x.bandwidth() / 2).attr('y', d => y(d[1]) - 5).attr('text-anchor', 'middle').attr('fill', 'var(--text)').attr('font-size', '10px').attr('font-weight', '700').text(d => d[1]) } }; + + /* ==================== RISK ANALYSIS MANAGER ==================== */ + const RiskAnalysisManager = { open() { ModalManager.open('riskAnalysisModal'); setTimeout(() => this.render(), 150) }, analyzeRisks() { const ps = projectData.appData.products || []; const envs = projectData.appData.environments || []; const risks = []; const allProjects = []; const pidSet = new Set(); ps.forEach(prod => { (prod.projects || []).forEach(pr => { allProjects.push({ ...pr, productName: prod.title, productOwner: prod.metadata?.owner }); pidSet.add(pr.id) }) }); allProjects.forEach(pr => { (pr.dependencies || []).forEach(dep => { const n = ProductManager.normalizeDependency(dep); if (!pidSet.has(n.projectId)) { risks.push({ type: 'broken-dependency', severity: 'critical', icon: '🔴', title: 'وابستگی شکسته', description: `${pr.title} → پروژه ناموجود`, recommendation: 'وابستگی را حذف یا پروژه مقصد را ایجاد کنید' }) } }) }); const inDeps = {}; allProjects.forEach(pr => { (pr.dependencies || []).forEach(dep => { const n = ProductManager.normalizeDependency(dep); if (pidSet.has(n.projectId)) inDeps[n.projectId] = (inDeps[n.projectId] || 0) + 1 }) }); Object.entries(inDeps).forEach(([pid, count]) => { if (count >= 2) { const proj = allProjects.find(p => p.id === pid); risks.push({ type: 'bottleneck', severity: count >= 3 ? 'critical' : 'high', icon: '⚠️', title: 'گلوگاه', description: `${proj?.title || pid}: ${count} وابستگی ورودی`, recommendation: 'وابستگی‌ها را توزیع کنید' }) } }); allProjects.forEach(pr => { if ((pr.lastDeployedVersions || []).length === 0) { risks.push({ type: 'no-deployment', severity: 'high', icon: '❌', title: 'بدون استقرار', description: `${pr.title} در هیچ محیطی استقرار ندارد`, recommendation: 'نسخه اولیه را مستقر کنید' }) } }); const now = new Date(); const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); allProjects.forEach(pr => { (pr.lastDeployedVersions || []).forEach(d => { if (d.deploymentDate && new Date(d.deploymentDate) < thirtyDaysAgo) { const daysOld = Math.floor((now - new Date(d.deploymentDate)) / (1000 * 60 * 60 * 24)); risks.push({ type: 'stale-version', severity: daysOld > 90 ? 'high' : 'medium', icon: '⏰', title: 'نسخه قدیمی', description: `${pr.title} - v${d.version} (${daysOld} روز)`, recommendation: 'نسخه را به‌روزرسانی کنید' }) } }) }); ps.forEach(prod => { if (!prod.metadata?.owner || prod.metadata.owner.trim() === '') { risks.push({ type: 'no-owner', severity: 'medium', icon: '👤', title: 'بدون مالک', description: `"${prod.title}" مالک ندارد`, recommendation: 'مالک را مشخص کنید' }) } }); envs.forEach(env => { const has = allProjects.some(pr => (pr.lastDeployedVersions || []).some(d => d.environmentId === env.id)); if (!has) { risks.push({ type: 'empty-environment', severity: 'low', icon: '🌍', title: 'محیط خالی', description: `"${env.name}" بدون استقرار`, recommendation: 'در صورت عدم نیاز حذف کنید' }) } }); const criticalCount = risks.filter(r => r.severity === 'critical').length; const highCount = risks.filter(r => r.severity === 'high').length; const mediumCount = risks.filter(r => r.severity === 'medium').length; const lowCount = risks.filter(r => r.severity === 'low').length; let healthScore = 100 - criticalCount * 15 - highCount * 10 - mediumCount * 5 - lowCount * 2; healthScore = Math.max(0, healthScore); return { risks, totalRisks: risks.length, criticalCount, highCount, mediumCount, lowCount, healthScore, totalProjects: allProjects.length } }, getSeverityLabel(severity) { const labels = { critical: { text: 'بحرانی', color: '#e53e3e', bg: 'rgba(229,62,62,.1)' }, high: { text: 'بالا', color: '#dd6b20', bg: 'rgba(221,107,32,.1)' }, medium: { text: 'متوسط', color: '#d69e2e', bg: 'rgba(214,158,46,.1)' }, low: { text: 'کم', color: '#3182ce', bg: 'rgba(49,130,206,.1)' } }; return labels[severity] || labels.low }, render() { const d = this.analyzeRisks(); const c = document.getElementById('riskAnalysisContainer'); if (!c) return; let h = '
'; h += `
🎯
${d.healthScore}%
امتیاز سلامت
`; h += `
🔴
${d.criticalCount}
بحرانی
`; h += `
🟠
${d.highCount}
بالا
`; h += `
🟡
${d.mediumCount}
متوسط
`; h += `
🔵
${d.lowCount}
کم
`; h += '
'; h += '
🍩 توزیع شدت
'; h += '
📋 لیست ریسک‌ها و پیشنهادات
'; h += '
'; c.innerHTML = h; this.renderSeverityDonut('riskSeverityChart', d); this.renderRiskList('riskDetailList', d); document.getElementById('riskAnalysisFooterStats').innerHTML = `🎯 سلامت: ${d.healthScore}%⚠️ کل ریسک‌ها: ${d.totalRisks}`; document.getElementById('riskAnalysisRefresh').onclick = () => { this.render(); showToast('به‌روزرسانی شد', 'success', 2000) } }, renderSeverityDonut(cid, data) { const c = document.getElementById(cid); if (!c) return; c.innerHTML = ''; const donutData = [{ label: 'بحرانی', value: data.criticalCount, color: '#e53e3e' }, { label: 'بالا', value: data.highCount, color: '#dd6b20' }, { label: 'متوسط', value: data.mediumCount, color: '#d69e2e' }, { label: 'کم', value: data.lowCount, color: '#3182ce' }].filter(d => d.value > 0); if (donutData.length === 0) { c.innerHTML = '
✅هیچ ریسکی نیست
'; return } const w = 260, h = 200, r = Math.min(w, h) / 2 - 10; const svg = d3.select(c).append('svg').attr('width', w).attr('height', h).append('g').attr('transform', `translate(${w / 2},${h / 2})`); const pie = d3.pie().value(d => d.value).sort(null); const arc = d3.arc().innerRadius(r * .55).outerRadius(r); const arcH = d3.arc().innerRadius(r * .55).outerRadius(r + 6); const total = donutData.reduce((s, d) => s + d.value, 0); const paths = svg.selectAll('path').data(pie(donutData)).join('path').attr('d', arc).attr('fill', d => d.data.color).attr('stroke', 'white').attr('stroke-width', 2).style('cursor', 'pointer'); paths.on('mouseover', function (e, d) { d3.select(this).transition().duration(150).attr('d', arcH(d)); showD3Tooltip(e, `${d.data.label}: ${d.data.value} (${Math.round(d.data.value / total * 100)}%)`) }).on('mousemove', e => moveD3Tooltip(e)).on('mouseout', function () { d3.select(this).transition().duration(150).attr('d', arc); hideD3Tooltip() }); svg.append('text').attr('text-anchor', 'middle').attr('dy', '-.2em').attr('fill', data.healthScore >= 70 ? '#38a169' : data.healthScore >= 40 ? '#dd6b20' : '#e53e3e').attr('font-size', '24px').attr('font-weight', '800').text(`${data.healthScore}%`); svg.append('text').attr('text-anchor', 'middle').attr('dy', '1.2em').attr('fill', 'var(--text-muted)').attr('font-size', '10px').text('امتیاز سلامت') }, renderRiskList(cid, data) { const c = document.getElementById(cid); if (!c) return; if (data.risks.length === 0) { c.innerHTML = '
✅وضعیت عالی است!
'; return } const severityOrder = { critical: 0, high: 1, medium: 2, low: 3 }; const sorted = [...data.risks].sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]); let h = ''; sorted.forEach(r => { const sl = this.getSeverityLabel(r.severity); h += `
`; h += `
`; h += `${r.icon}`; h += `${r.title}`; h += `${sl.text}`; h += `
`; h += `
${r.description}
`; h += `
💡 ${r.recommendation}
`; h += `
` }); c.innerHTML = h } }; + + /* ==================== PRODUCT MANAGER ==================== */ + const ProductManager = { currentDetailProductId: null, currentEditProjectId: null, currentDependencyProjectId: null, currentDependencyProjId: null, currentDeploymentProjId: null, currentDeploymentMode: null, currentDeploymentEnvId: null, getTypeInfo(type) { return VALID_PROJECT_TYPES.find(t => t.value === type) || { value: type, label: type, icon: '❓' } }, getEnvironmentName(envId) { const env = projectData.appData.environments.find(e => e.id === envId); return env ? env.name : envId }, getEnvironmentColors(envId) { const env = projectData.appData.environments.find(e => e.id === envId); return { backgroundColor: env?.backgroundColor || '#38a169', textColor: env?.textColor || '#ffffff' } }, getEmptyEnvironmentIds(proj) { const all = projectData.appData.environments.map(e => e.id); const dep = (proj.lastDeployedVersions || []).map(d => d.environmentId); return all.filter(id => !dep.includes(id)) }, normalizeDependency(dep) { if (typeof dep === 'string') return { projectId: dep, environmentId: null, version: null }; return dep }, findProjectById(pid) { for (const prod of projectData.appData.products) { const pr = prod.projects.find(p => p.id === pid); if (pr) return pr } return null }, showDetail(productId) { const prod = projectData.appData.products.find(p => p.id === productId); if (!prod) return; this.currentDetailProductId = productId; document.getElementById('projectDetailTitle').innerHTML = `📦${prod.title}`; const projects = prod.projects || []; const totalDeploys = projects.reduce((s, pr) => s + (pr.lastDeployedVersions || []).length, 0); let html = `
ℹ️ اطلاعات محصول
عنوان${prod.title}
توضیحات${prod.description || '—'}
تاریخ ایجاد${prod.metadata?.createdAt ? new Date(prod.metadata.createdAt).toLocaleDateString('fa-IR') : '—'}
مالک${prod.metadata?.owner || '—'}
تعداد پروژه${projects.length}
استقرارها${totalDeploys}
🌿 پروژه‌ها (${projects.length})
`; if (projects.length === 0) { html += '
🌿 هیچ پروژه‌ای نیست
' } else { html += '
'; projects.forEach(pr => { const ti = this.getTypeInfo(pr.type); const deps = pr.dependencies || []; const deploys = pr.lastDeployedVersions || []; const eei = this.getEmptyEnvironmentIds(pr); const canAdd = eei.length > 0; html += `
${ti.icon}${pr.title}${ti.label}
${pr.description || ''}
${pr.metadata?.framework ? `
فریم‌ورک${pr.metadata.framework}
` : ''}${pr.metadata?.language ? `
زبان${pr.metadata.language}
` : ''}${deps.length > 0 ? `
وابستگی‌ها (${deps.length})${deps.map(d => { const n = this.normalizeDependency(d); const dp = this.findProjectById(n.projectId); return dp ? dp.title : n.projectId }).join('، ')}
` : ''}
`; html += `
🚀 استقرارها (${deploys.length})${canAdd ? `` : '✓ کامل'}
`; if (deploys.length === 0) { html += '
هنوز استقرار ندارد
' } else { html += '
'; deploys.forEach(d => { const en = this.getEnvironmentName(d.environmentId); const cl = this.getEnvironmentColors(d.environmentId); const fd = d.deploymentDate ? new Date(d.deploymentDate).toLocaleString('fa-IR') : '—'; html += `
${en}v${d.version}📅 ${fd}
` }); html += '
' } html += `
` }); html += '
' } html += `
`; document.getElementById('projectDetailBody').innerHTML = html; ModalManager.open('projectDetailModal') }, openAddProductModal() { document.getElementById('newProductTitle').value = ''; document.getElementById('newProductDescription').value = ''; document.getElementById('newProductOwner').value = ''; document.getElementById('newProductCreatedAt').value = new Date().toLocaleDateString('fa-IR'); this.clearProductFormErrors(); ModalManager.open('addProductModal'); setTimeout(() => { document.getElementById('newProductTitle').focus() }, 100) }, clearProductFormErrors() { const ti = document.getElementById('newProductTitle'), te = document.getElementById('newProductTitleError'); if (ti) ti.classList.remove('error'); if (te) te.classList.remove('visible') }, createNewProduct() { const title = document.getElementById('newProductTitle').value.trim(); if (!title || title.length < 3) { showToast('عنوان محصول الزامی است (حداقل ۳ کاراکتر)', 'error'); document.getElementById('newProductTitle').classList.add('error'); document.getElementById('newProductTitleError').classList.add('visible'); return } if (projectData.appData.products.some(p => p.title.toLowerCase() === title.toLowerCase())) { showToast('محصولی با این عنوان وجود دارد', 'warning'); return } HistoryManager.pushState(); projectData.appData.products.push({ id: `prod-${Date.now()}`, title, description: document.getElementById('newProductDescription').value.trim() || 'بدون توضیحات', metadata: { createdAt: new Date().toISOString(), owner: document.getElementById('newProductOwner').value.trim() || '', customFields: {} }, projects: [] }); ModalManager.close('addProductModal'); refreshUI(); showToast(`محصول "${title}" ایجاد شد`, 'success'); WebhookManager.sendEvent('product_created', { title }) }, openDependencyManager(pid, projId) { this.currentDependencyProjectId = pid; this.currentDependencyProjId = projId; this.renderCurrentDependencies(); this.renderAllProjectsList(); this.setupDependencySearch(); ModalManager.open('dependencyManagerModal') }, renderCurrentDependencies() { const prod = projectData.appData.products.find(p => p.id === this.currentDependencyProjectId); if (!prod) return; const pr = prod.projects.find(p => p.id === this.currentDependencyProjId); if (!pr) return; const c = document.getElementById('currentDependenciesList'); const deps = pr.dependencies || []; if (deps.length === 0) { c.innerHTML = '
هیچ وابستگی‌ای نیست
'; return } c.innerHTML = deps.map((dep, i) => { const n = this.normalizeDependency(dep); const dp = this.findProjectById(n.projectId); const pn = dp ? dp.title : n.projectId; const en = n.environmentId ? this.getEnvironmentName(n.environmentId) : '—'; const v = n.version || '—'; return `
${pn}
${en} v${v}
` }).join('') }, removeDependency(i) { const prod = projectData.appData.products.find(p => p.id === this.currentDependencyProjectId); if (!prod) return; const pr = prod.projects.find(p => p.id === this.currentDependencyProjId); if (!pr) return; if (!confirm('حذف این وابستگی؟')) return; HistoryManager.pushState(); pr.dependencies.splice(i, 1); ModalManager.close('dependencyManagerModal'); refreshUI(); if (this.currentDetailProductId) this.showDetail(this.currentDetailProductId); showToast('وابستگی حذف شد', 'success'); WebhookManager.sendEvent('dependency_removed', { projectId: this.currentDependencyProjId }) }, renderAllProjectsList() { const rc = document.getElementById('dependencySearchResults'); const cp = projectData.appData.products.find(p => p.id === this.currentDependencyProjectId); if (!cp) return; const cpr = cp.projects.find(p => p.id === this.currentDependencyProjId); if (!cpr) return; const allProjects = []; projectData.appData.products.forEach(prod => { prod.projects.forEach(pr => { if (pr.id !== cpr.id) allProjects.push({ ...pr, productName: prod.title }) }) }); if (allProjects.length === 0) { rc.innerHTML = '
پروژه دیگری نیست
'; return } rc.innerHTML = allProjects.map(pr => { const vs = this.getAvailableVersions(pr); return `
📦 ${pr.title} (${pr.productName})${vs.length} نسخه
${vs.length > 0 ? vs.map(v => `
${v.source === 'history' ? '📜' : '🚀'} v${v.version} ${this.getEnvironmentName(v.environmentId)}
${new Date(v.deploymentDate).toLocaleDateString('fa-IR')}
`).join('') : '
نسخه‌ای نیست
'}
` }).join('') }, setupDependencySearch() { const si = document.getElementById('dependencySearch'); si.value = ''; si.oninput = e => { const q = e.target.value.trim().toLowerCase(); if (q.length === 0) { this.renderAllProjectsList(); return } if (q.length < 2) { document.getElementById('dependencySearchResults').innerHTML = '
حداقل ۲ کاراکتر
'; return } const cp = projectData.appData.products.find(p => p.id === this.currentDependencyProjectId); if (!cp) return; const cpr = cp.projects.find(p => p.id === this.currentDependencyProjId); if (!cpr) return; const allProjects = []; projectData.appData.products.forEach(prod => { prod.projects.forEach(pr => { if (pr.id !== cpr.id) allProjects.push({ ...pr, productName: prod.title }) }) }); const fl = allProjects.filter(pr => pr.title.toLowerCase().includes(q) || pr.productName.toLowerCase().includes(q) || (pr.description || '').toLowerCase().includes(q)); if (fl.length === 0) { document.getElementById('dependencySearchResults').innerHTML = '
نتیجه‌ای نیست
'; return } document.getElementById('dependencySearchResults').innerHTML = fl.map(pr => { const vs = this.getAvailableVersions(pr); return `
📦 ${pr.title} (${pr.productName})${vs.length} نسخه
${vs.length > 0 ? vs.map(v => `
${v.source === 'history' ? '📜' : '🚀'} v${v.version} ${this.getEnvironmentName(v.environmentId)}
${new Date(v.deploymentDate).toLocaleDateString('fa-IR')}
`).join('') : '
نسخه‌ای نیست
'}
` }).join('') } }, getAvailableVersions(pr) { const vs = []; (pr.lastDeployedVersions || []).forEach(v => { vs.push({ environmentId: v.environmentId, version: v.version, deploymentDate: v.deploymentDate, source: 'current' }) }); (pr.deploymentHistory || []).forEach(v => { vs.push({ environmentId: v.environmentId, version: v.version, deploymentDate: v.deploymentDate || v.replacedAt, source: 'history' }) }); return vs }, addDependency(pid, eid, ver) { const prod = projectData.appData.products.find(p => p.id === this.currentDependencyProjectId); if (!prod) return; const pr = prod.projects.find(p => p.id === this.currentDependencyProjId); if (!pr) return; const exists = (pr.dependencies || []).some(dep => { const n = this.normalizeDependency(dep); return n.projectId === pid && n.environmentId === eid && n.version === ver }); if (exists) { showToast('تکراری است', 'warning'); return } HistoryManager.pushState(); if (!pr.dependencies) pr.dependencies = []; pr.dependencies.push({ projectId: pid, environmentId: eid, version: ver }); ModalManager.close('dependencyManagerModal'); refreshUI(); if (this.currentDetailProductId) this.showDetail(this.currentDetailProductId); showToast('وابستگی اضافه شد', 'success'); WebhookManager.sendEvent('dependency_added', { projectId: this.currentDependencyProjId, dependencyProjectId: pid }) }, openProductEditModal() { const pid = this.currentDetailProductId; if (!pid) return; const prod = projectData.appData.products.find(p => p.id === pid); if (!prod) return; document.getElementById('editProductId').value = prod.id; document.getElementById('editProductTitle').value = prod.title; document.getElementById('editProductDescription').value = prod.description || ''; document.getElementById('editProductOwner').value = prod.metadata?.owner || ''; document.getElementById('editProductCreatedAt').value = prod.metadata?.createdAt ? new Date(prod.metadata.createdAt).toLocaleDateString('fa-IR') : '—'; ModalManager.open('productEditModal') }, saveProductEdit() { const pid = this.currentDetailProductId; if (!pid) return; const prod = projectData.appData.products.find(p => p.id === pid); if (!prod) return; const nt = document.getElementById('editProductTitle').value.trim(); if (!nt) { showToast('عنوان الزامی است', 'error'); return } HistoryManager.pushState(); prod.title = nt; prod.description = document.getElementById('editProductDescription').value.trim(); if (!prod.metadata) prod.metadata = {}; prod.metadata.owner = document.getElementById('editProductOwner').value.trim(); prod.metadata.updatedAt = new Date().toISOString(); ModalManager.close('productEditModal'); refreshUI(); this.showDetail(pid); showToast(`محصول "${prod.title}" ویرایش شد`, 'success'); WebhookManager.sendEvent('product_edited', { productId: pid, title: prod.title }) }, openAddProjectModal() { if (!this.currentDetailProductId) { showToast('ابتدا یک محصول انتخاب کنید', 'warning'); return } document.getElementById('newProjectTitle').value = ''; document.getElementById('newProjectType').value = ''; document.getElementById('newProjectFramework').value = ''; document.getElementById('newProjectLanguage').value = ''; document.getElementById('newProjectRepo').value = ''; document.getElementById('newProjectDescription').value = ''; populateProjectTypeDropdowns(); ModalManager.open('addProjectModal') }, saveNewProject() { const pid = this.currentDetailProductId; if (!pid) return; const prod = projectData.appData.products.find(p => p.id === pid); if (!prod) return; const title = document.getElementById('newProjectTitle').value.trim(); const type = document.getElementById('newProjectType').value; if (!title) { showToast('عنوان پروژه الزامی است', 'error'); return } if (!type) { showToast('نوع پروژه الزامی است', 'error'); return } HistoryManager.pushState(); const np = { id: `proj-${Date.now()}`, title, description: document.getElementById('newProjectDescription').value.trim() || 'بدون توضیحات', type, repositoryUrl: document.getElementById('newProjectRepo').value.trim(), metadata: { createdAt: new Date().toISOString(), customFields: {} }, dependencies: [], lastDeployedVersions: [], deploymentHistory: [] }; const fw = document.getElementById('newProjectFramework').value.trim(); if (fw) np.metadata.framework = fw; const lg = document.getElementById('newProjectLanguage').value.trim(); if (lg) np.metadata.language = lg; prod.projects.push(np); ModalManager.close('addProjectModal'); refreshUI(); this.showDetail(pid); showToast(`پروژه "${title}" ایجاد شد`, 'success'); WebhookManager.sendEvent('project_added', { productId: pid, type, title }) }, openProjectEditModal(pid, projId) { const prod = projectData.appData.products.find(p => p.id === pid); if (!prod) return; const pr = prod.projects.find(p => p.id === projId); if (!pr) return; this.currentEditProjectId = projId; populateProjectTypeDropdowns(); document.getElementById('editProjId').value = pr.id; document.getElementById('editProjTitle').value = pr.title; document.getElementById('editProjType').value = pr.type; document.getElementById('editProjFramework').value = pr.metadata?.framework || ''; document.getElementById('editProjLanguage').value = pr.metadata?.language || ''; document.getElementById('editProjRepo').value = pr.repositoryUrl || ''; document.getElementById('editProjDescription').value = pr.description || ''; ModalManager.open('editProjectModal') }, saveProjectEdit() { const pid = this.currentDetailProductId; const projId = this.currentEditProjectId; if (!pid || !projId) return; const prod = projectData.appData.products.find(p => p.id === pid); if (!prod) return; const pr = prod.projects.find(p => p.id === projId); if (!pr) return; const nt = document.getElementById('editProjTitle').value.trim(); const nty = document.getElementById('editProjType').value; if (!nt || !nty) { showToast('عنوان و نوع الزامی است', 'error'); return } HistoryManager.pushState(); pr.title = nt; pr.type = nty; pr.description = document.getElementById('editProjDescription').value.trim() || 'بدون توضیحات'; pr.repositoryUrl = document.getElementById('editProjRepo').value.trim(); if (!pr.metadata) pr.metadata = {}; const fw = document.getElementById('editProjFramework').value.trim(); if (fw) pr.metadata.framework = fw; else delete pr.metadata.framework; const lg = document.getElementById('editProjLanguage').value.trim(); if (lg) pr.metadata.language = lg; else delete pr.metadata.language; pr.metadata.updatedAt = new Date().toISOString(); ModalManager.close('editProjectModal'); refreshUI(); this.showDetail(pid); showToast(`پروژه "${pr.title}" ویرایش شد`, 'success'); WebhookManager.sendEvent('project_edited', { projId, title: pr.title, type: pr.type }) }, openAddDeploymentModal(pid, projId) { if (!pid || !projId) return; const prod = projectData.appData.products.find(p => p.id === pid); if (!prod) return; const pr = prod.projects.find(p => p.id === projId); if (!pr) return; const eei = this.getEmptyEnvironmentIds(pr); if (eei.length === 0) { showToast('همه محیط‌ها استقرار دارند', 'warning'); return } this.currentDeploymentProjId = projId; this.currentDeploymentMode = 'add'; this.currentDeploymentEnvId = null; document.getElementById('deploymentModalTitle').innerHTML = '🚀افزودن نسخه استقرار'; const envs = projectData.appData.environments.filter(e => eei.includes(e.id)); document.getElementById('deploymentEnvGroup').innerHTML = ``; document.getElementById('depVersion').value = ''; document.getElementById('depDate').value = ''; ModalManager.open('deploymentModal') }, openEditDeploymentModal(pid, projId, eid, ver, date) { if (!pid || !projId) return; const prod = projectData.appData.products.find(p => p.id === pid); if (!prod) return; const pr = prod.projects.find(p => p.id === projId); if (!pr) return; this.currentDeploymentProjId = projId; this.currentDeploymentMode = 'edit'; this.currentDeploymentEnvId = eid; document.getElementById('deploymentModalTitle').innerHTML = '✏️ویرایش نسخه استقرار'; const cl = this.getEnvironmentColors(eid); document.getElementById('deploymentEnvGroup').innerHTML = `
${this.getEnvironmentName(eid)}
`; document.getElementById('depVersion').value = ver; let dv = ''; if (date) { const d = new Date(date); const pad = n => n.toString().padStart(2, '0'); dv = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}` } document.getElementById('depDate').value = dv; ModalManager.open('deploymentModal') }, saveDeployment() { const pid = this.currentDetailProductId; const projId = this.currentDeploymentProjId; if (!pid || !projId) { showToast('خطا: محصول یا پروژه مشخص نشده', 'error'); return } const prod = projectData.appData.products.find(p => p.id === pid); if (!prod) return; const pr = prod.projects.find(p => p.id === projId); if (!pr) return; const ver = document.getElementById('depVersion').value.trim(); const date = document.getElementById('depDate').value; if (!ver || !date) { showToast('نسخه و تاریخ الزامی است', 'error'); return } const dd = new Date(date).toISOString(); HistoryManager.pushState(); if (!pr.deploymentHistory) pr.deploymentHistory = []; if (this.currentDeploymentMode === 'add') { const eid = document.getElementById('depEnvironment').value; if (!eid) { showToast('محیط را انتخاب کنید', 'error'); return } const ei = pr.lastDeployedVersions.findIndex(d => d.environmentId === eid); if (ei >= 0) { const old = pr.lastDeployedVersions[ei]; pr.deploymentHistory.push({ ...old, replacedAt: dd, replacedByVersion: ver }); pr.lastDeployedVersions[ei] = { environmentId: eid, version: ver, deploymentDate: dd }; showToast(`v${old.version} → v${ver} در ${this.getEnvironmentName(eid)}`, 'success') } else { pr.lastDeployedVersions.push({ environmentId: eid, version: ver, deploymentDate: dd }); showToast(`v${ver} در ${this.getEnvironmentName(eid)} استقرار یافت`, 'success') } } else { const eid = this.currentDeploymentEnvId; const ci = pr.lastDeployedVersions.findIndex(d => d.environmentId === eid); if (ci >= 0) { const old = pr.lastDeployedVersions[ci]; if (old.version !== ver) pr.deploymentHistory.push({ ...old, replacedAt: dd, replacedByVersion: ver }); pr.lastDeployedVersions[ci] = { environmentId: eid, version: ver, deploymentDate: dd }; showToast(`v${ver} در ${this.getEnvironmentName(eid)} به‌روز شد`, 'success') } } ModalManager.close('deploymentModal'); refreshUI(); this.showDetail(pid) }, removeDeployment(pid, projId, eid, ver) { if (!confirm(`حذف v${ver} از ${this.getEnvironmentName(eid)}؟`)) return; const prod = projectData.appData.products.find(p => p.id === pid); if (!prod) return; const pr = prod.projects.find(p => p.id === projId); if (!pr) return; HistoryManager.pushState(); if (!pr.deploymentHistory) pr.deploymentHistory = []; const di = pr.lastDeployedVersions.findIndex(d => d.environmentId === eid && d.version === ver); if (di >= 0) { pr.deploymentHistory.push({ ...pr.lastDeployedVersions[di], removedAt: new Date().toISOString(), status: 'removed' }); pr.lastDeployedVersions.splice(di, 1) } refreshUI(); this.showDetail(pid); showToast(`v${ver} حذف شد`, 'warning') }, removeProduct(pid) { const prod = projectData.appData.products.find(p => p.id === pid); if (!prod) return; if (!confirm(`حذف "${prod.title}" و تمام پروژه‌ها؟`)) return; HistoryManager.pushState(); projectData.appData.products = projectData.appData.products.filter(p => p.id !== pid); ModalManager.close('projectDetailModal'); refreshUI(); showToast(`"${prod.title}" حذف شد`, 'warning'); WebhookManager.sendEvent('product_removed', { productId: pid }) }, removeProject(pid, projId) { const prod = projectData.appData.products.find(p => p.id === pid); if (!prod) return; const pr = prod.projects.find(p => p.id === projId); if (!pr) return; if (!confirm(`حذف "${pr.title}"؟`)) return; HistoryManager.pushState(); prod.projects = prod.projects.filter(p => p.id !== projId); refreshUI(); if (this.currentDetailProductId === pid) this.showDetail(pid); showToast(`"${pr.title}" حذف شد`, 'warning'); WebhookManager.sendEvent('project_removed', { projectId: pid, projId }) } }; + + /* ==================== EVENT BINDINGS ==================== */ + document.getElementById('projectDetailDeleteBtn').addEventListener('click', () => { if (ProductManager.currentDetailProductId) ProductManager.removeProduct(ProductManager.currentDetailProductId) }); + document.getElementById('saveProductEditBtn').addEventListener('click', () => ProductManager.saveProductEdit()); + document.getElementById('saveNewProjectBtn').addEventListener('click', () => ProductManager.saveNewProject()); + document.getElementById('saveProjEditBtn').addEventListener('click', () => ProductManager.saveProjectEdit()); + document.getElementById('saveDeploymentBtn').addEventListener('click', () => ProductManager.saveDeployment()); + document.getElementById('saveEnvEditBtn').addEventListener('click', () => EnvironmentManager.saveEdit()); + document.getElementById('saveNewProductBtn').addEventListener('click', () => ProductManager.createNewProduct()); + document.getElementById('saveNewEnvBtn').addEventListener('click', () => EnvironmentManager.saveNewEnv()); + document.getElementById('newEnvBgColor').addEventListener('input', e => { document.getElementById('newEnvBgColorText').textContent = e.target.value }); + document.getElementById('newEnvTextColor').addEventListener('input', e => { document.getElementById('newEnvTextColorText').textContent = e.target.value }); + document.getElementById('editEnvBgColor').addEventListener('input', e => { document.getElementById('editEnvBgColorText').textContent = e.target.value; EnvironmentManager.updateEditPreview() }); + document.getElementById('editEnvTextColor').addEventListener('input', e => { document.getElementById('editEnvTextColorText').textContent = e.target.value; EnvironmentManager.updateEditPreview() }); + document.getElementById('newProductTitle').addEventListener('input', e => { if (e.target.value.trim().length >= 3) { e.target.classList.remove('error'); document.getElementById('newProductTitleError').classList.remove('visible') } }); + function handleToolSelect(tool, group) { const ah = ActionRegistry[tool.action] || ActionRegistry.defaultAction; ah(tool, group); WebhookManager.sendEvent('tool_selected', { toolId: tool.id, toolTitle: tool.title, toolAction: tool.action, groupId: group.id }) } + const themeToggle = document.getElementById('themeToggle'), themeIcon = document.getElementById('themeIcon'), htmlEl = document.documentElement; + const savedTheme = localStorage.getItem('theme') || 'light'; + htmlEl.setAttribute('data-theme', savedTheme); + if (themeIcon) themeIcon.textContent = savedTheme === 'dark' ? '☀️' : '🌙'; + if (themeToggle) { themeToggle.addEventListener('click', () => { const c = htmlEl.getAttribute('data-theme'); const n = c === 'dark' ? 'light' : 'dark'; htmlEl.setAttribute('data-theme', n); localStorage.setItem('theme', n); if (themeIcon) themeIcon.textContent = n === 'dark' ? '☀️' : '🌙' }) } + const sidebarToggle = document.getElementById('sidebarToggle'), sidebar = document.getElementById('sidebar'), sidebarIcon = document.getElementById('sidebarIcon'); + function updateSidebarIcon() { if (!sidebarIcon) return; sidebarIcon.textContent = sidebar.classList.contains('collapsed') ? '☰' : '✕' } + if (sidebarToggle) { sidebarToggle.addEventListener('click', () => { sidebar.classList.toggle('collapsed'); updateSidebarIcon() }) } + updateSidebarIcon(); + const sidebarBody = document.getElementById('sidebarBody'); + function renderToolGroups() { sidebarBody.innerHTML = ''; projectData.toolGroups.forEach(group => { const ge = document.createElement('div'); ge.className = 'sidebar-group'; const he = document.createElement('div'); he.className = 'sidebar-group-header'; if (group.description) he.setAttribute('data-tooltip', group.description); he.innerHTML = `
${group.icon || ''}${group.title}
▼`; const be = document.createElement('div'); be.className = 'sidebar-group-body'; group.tools.forEach(tool => { const btn = document.createElement('button'); btn.className = 'sidebar-btn'; btn.title = tool.description; btn.innerHTML = `${tool.icon || '🔧'}${tool.title}`; btn.addEventListener('click', () => handleToolSelect(tool, group)); be.appendChild(btn) }); he.addEventListener('click', () => ge.classList.toggle('collapsed')); ge.appendChild(he); ge.appendChild(be); sidebarBody.appendChild(ge) }) } + function openSettingsModal() { document.getElementById('settingProjectTitle').value = projectData.projectTitle; document.getElementById('settingVersion').value = projectData.version; document.getElementById('settingDescription').value = projectData.description; document.getElementById('settingDeveloper').value = projectData.developer; document.getElementById('settingCompany').value = projectData.company; document.getElementById('settingWebhookUrl').value = projectData.webhook.url; document.getElementById('settingWebhookClientId').value = projectData.webhook.clientId; document.getElementById('settingWebhookEnabled').checked = projectData.webhook.enabled; WebhookManager.updateStatusUI(); ModalManager.open('settingsModal') } + ['settingWebhookUrl', 'settingWebhookClientId', 'settingWebhookEnabled'].forEach(id => { document.getElementById(id).addEventListener('input', () => { WebhookManager.setConfig({ url: document.getElementById('settingWebhookUrl').value, clientId: document.getElementById('settingWebhookClientId').value, enabled: document.getElementById('settingWebhookEnabled').checked }) }) }); + document.getElementById('saveSettingsBtn').addEventListener('click', () => { WebhookManager.setConfig({ url: document.getElementById('settingWebhookUrl').value.trim(), clientId: document.getElementById('settingWebhookClientId').value.trim(), enabled: document.getElementById('settingWebhookEnabled').checked }); ModalManager.close('settingsModal'); showToast('تنظیمات ذخیره شد', 'success'); WebhookManager.sendEvent('settings_updated', { updatedFields: ['webhook'] }) }); + function updateProjectUI() { document.title = projectData.projectTitle; document.getElementById('headerTitle').textContent = projectData.projectTitle; document.getElementById('headerSubtitle').textContent = projectData.description; document.getElementById('appFooter').innerHTML = `
طراح و توسعه‌دهنده: ${projectData.developer} |شرکت: ${projectData.company}
نسخه: ${projectData.version}
` } + document.addEventListener('keydown', e => { if (!(e.ctrlKey || e.metaKey)) return; const key = e.key.toLowerCase(); for (const group of projectData.toolGroups) { const tool = group.tools.find(t => t.shortcut && t.shortcut.toLowerCase().replace('ctrl+', '') === key); if (tool) { e.preventDefault(); handleToolSelect(tool, group); break } } }); + + /* ==================== INIT ==================== */ + ModalManager.init(); + renderToolGroups(); + updateProjectUI(); + WebhookManager.updateStatusUI(); + ToolbarManager.init(); + SectionCollapseManager.init(); + BoardRenderer.render(); + HistoryManager.init(); + setTimeout(() => showToast(`${projectData.projectTitle} (نسخه ${projectData.version}) آماده است`, 'success'), 500); + + \ No newline at end of file diff --git a/xDependencies_1.4.30.html b/xDependencies_1.4.30.html new file mode 100644 index 0000000..698995e --- /dev/null +++ b/xDependencies_1.4.30.html @@ -0,0 +1,3010 @@ + + + + + + + دستیار انتشار + + + + + +
+
+ +
+
مدیریت محصولات
+
مدیریت نسخه‌ها و نیازمندی‌های محصولات
+
+
+
+ +
+
+ + + + +
+ +
+
🏷️
+
🌐
+
+
+
📋 0
+
📦 0
+
🌿 0
+
🔗 0
+
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + + + \ No newline at end of file diff --git a/xDependencies_1.4.31.html b/xDependencies_1.4.31.html new file mode 100644 index 0000000..5c3f40c --- /dev/null +++ b/xDependencies_1.4.31.html @@ -0,0 +1,402 @@ + + + + + +دستیار انتشار + + + + +
مدیریت محصولات
مدیریت نسخه‌ها و نیازمندی‌های محصولات
+
🏷️
🌐
📋 0
📦 0
🌿 0
🔗 0
+
+
طراح و توسعه‌دهنده: هادی خزاعی اصل | شرکت: رایان پرداز کاوش
نسخه: 1.4.20
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + + \ No newline at end of file diff --git a/xDependencies_1.4.32.html b/xDependencies_1.4.32.html new file mode 100644 index 0000000..209c1ed --- /dev/null +++ b/xDependencies_1.4.32.html @@ -0,0 +1,487 @@ + + + + + +دستیار انتشار + + + + +
+
+ +
+
دستیار انتشار
+
مدیریت نسخه‌ها و نیازمندی‌های محصولات
+
+
+
+ +
+
+ + + + +
+ +
+
🏷️
+
🌐
+
+
+
📋 0
+
📦 0
+
🌿 0
+
🔗 0
+
+
+ +
+ +
+
+ +
+
طراح و توسعه‌دهنده: هادی خزاعی اصل | شرکت: رایان پرداز کاوش
+
نسخه: 1.1.0
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + + \ No newline at end of file diff --git a/xDependencies_1.4.33.html b/xDependencies_1.4.33.html new file mode 100644 index 0000000..78a78bd --- /dev/null +++ b/xDependencies_1.4.33.html @@ -0,0 +1,3980 @@ + + + + + + + دستیار انتشار + + + + + +
+
+ +
+
دستیار انتشار
+
مدیریت نسخه‌ها و نیازمندی‌های محصولات
+
+
+
+
+
+ + + + +
+ +
+
🏷️
+
🌐
+
+
+
📋 0
+
📦 0
+
🌿 0
+
🔗 0
+
+
+
+ +
+
+
+
طراح و توسعه‌دهنده: هادی خزاعی اصل |شرکت: رایان پرداز کاوش
+
نسخه: 1.1.0
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + + + \ No newline at end of file diff --git a/xDependencies_Last.html b/xDependencies_Last.html index 46f9255..6b8a43c 100644 --- a/xDependencies_Last.html +++ b/xDependencies_Last.html @@ -2,4717 +2,3011 @@ - - - مدیریت محصولات - - + + + دستیار انتشار + + -
-
- -
-
مدیریت محصولات
-
مدیریت نسخه‌ها و نیازمندی‌های محصولات
-
-
-
- -
-
- - - - -
- -
-
- 🏷️ - -
-
- 🌐 - -
-
-
-
- 📋 - کل: - 0 -
-
- 📦 - محصول: - 0 -
-
- 🌿 - پروژه: - 0 -
-
- 🔗 - وابستگی: - 0 -
-
-
- -
- -
-
- -
-
- طراح و توسعه‌دهنده: هادی خزاعی اصل - | - شرکت: رایان پرداز کاوش -
-
- نسخه: 1.4.20 -
-
- - -
+
+
+ + + + +
+ +
+
🏷️
+
🌐
+
+
+
📋 0
+
📦 0
+
🌿 0
+
🔗 0
+
- - - -