- z51-pug-builder: Svelte 5 SPA, визуальный редактор Pug-писем - email-gen: Node.js рендерер Pug→HTML через email-templates + Juice - email-gen-api: HTTP сервер рендеринга (порт 8787) - coin-scout: сервис подбора монет из фидов - Docker Compose для dev/prod - Nginx конфиг с SSL для app.aspekter.ru
769 lines
56 KiB
HTML
769 lines
56 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Coin Scout</title>
|
||
<style>
|
||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0f1117; color: #e1e4e8; }
|
||
a { color: #58a6ff; text-decoration: none; }
|
||
a:hover { text-decoration: underline; }
|
||
|
||
.header { background: #161b22; border-bottom: 1px solid #30363d; padding: 16px 24px; display: flex; align-items: center; gap: 16px; }
|
||
.header h1 { font-size: 20px; font-weight: 600; }
|
||
.header .stats { font-size: 13px; color: #8b949e; margin-left: auto; }
|
||
|
||
.toolbar { background: #161b22; border-bottom: 1px solid #30363d; padding: 12px 24px; display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
|
||
.toolbar label { font-size: 13px; color: #8b949e; }
|
||
.toolbar input, .toolbar select { background: #0d1117; border: 1px solid #30363d; color: #e1e4e8; padding: 6px 10px; border-radius: 6px; font-size: 13px; }
|
||
.toolbar input:focus, .toolbar select:focus { border-color: #58a6ff; outline: none; }
|
||
.toolbar input[type="number"] { width: 80px; }
|
||
|
||
.btn { background: #21262d; border: 1px solid #30363d; color: #e1e4e8; padding: 6px 14px; border-radius: 6px; cursor: pointer; font-size: 13px; }
|
||
.btn:hover { background: #30363d; }
|
||
.btn-primary { background: #238636; border-color: #238636; }
|
||
.btn-primary:hover { background: #2ea043; }
|
||
.btn-scan { background: #1f6feb; border-color: #1f6feb; }
|
||
.btn-scan:hover { background: #388bfd; }
|
||
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
||
|
||
.tabs { display: flex; gap: 0; border-bottom: 1px solid #30363d; background: #161b22; padding: 0 24px; }
|
||
.tab { padding: 10px 16px; font-size: 14px; color: #8b949e; cursor: pointer; border-bottom: 2px solid transparent; }
|
||
.tab:hover { color: #e1e4e8; }
|
||
.tab.active { color: #e1e4e8; border-bottom-color: #f78166; }
|
||
|
||
.content { padding: 16px 24px; }
|
||
|
||
.coin-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); gap: 12px; }
|
||
.coin-card { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 14px; display: flex; gap: 12px; transition: border-color 0.2s; }
|
||
.coin-card:hover { border-color: #58a6ff; }
|
||
.coin-card.top { border-color: #f0883e; }
|
||
.coin-img { width: 80px; height: 80px; border-radius: 6px; object-fit: cover; background: #0d1117; flex-shrink: 0; }
|
||
.coin-info { flex: 1; min-width: 0; }
|
||
.coin-name { font-size: 14px; font-weight: 500; margin-bottom: 4px; overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
|
||
.coin-meta { font-size: 12px; color: #8b949e; display: flex; flex-wrap: wrap; gap: 6px; margin-top: 4px; }
|
||
.coin-meta .tag { background: #21262d; padding: 2px 6px; border-radius: 4px; }
|
||
.coin-meta .tag.silver { background: #1c3a5a; color: #79c0ff; }
|
||
.coin-meta .tag.gold { background: #3d2e00; color: #e3b341; }
|
||
.coin-meta .tag.grade-high { background: #1a4731; color: #56d364; }
|
||
.coin-meta .tag.grade-mid { background: #3d2e00; color: #e3b341; }
|
||
.coin-meta .tag.no-stock { background: #490202; color: #f85149; }
|
||
.coin-price { font-size: 16px; font-weight: 600; color: #56d364; margin-top: 6px; }
|
||
.coin-price .old { font-size: 12px; color: #8b949e; text-decoration: line-through; margin-left: 6px; font-weight: 400; }
|
||
.coin-score { font-size: 11px; color: #f0883e; font-weight: 600; margin-top: 4px; }
|
||
.coin-feed { font-size: 11px; color: #8b949e; }
|
||
|
||
.scan-bar { background: #1c2128; border: 1px solid #30363d; border-radius: 8px; padding: 14px; margin-bottom: 16px; display: flex; align-items: center; gap: 12px; }
|
||
.scan-bar .status { font-size: 13px; }
|
||
.scan-bar .dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
|
||
.dot.green { background: #56d364; }
|
||
.dot.yellow { background: #e3b341; animation: pulse 1s infinite; }
|
||
.dot.red { background: #f85149; }
|
||
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }
|
||
|
||
.settings-panel { max-width: 500px; }
|
||
.settings-panel .field { margin-bottom: 14px; }
|
||
.settings-panel .field label { display: block; font-size: 13px; color: #8b949e; margin-bottom: 4px; }
|
||
.settings-panel .field input, .settings-panel .field select { width: 100%; }
|
||
|
||
.empty { text-align: center; padding: 60px; color: #8b949e; }
|
||
.empty p { margin-top: 8px; }
|
||
|
||
.loading { text-align: center; padding: 40px; color: #8b949e; }
|
||
|
||
.modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.7); z-index: 1000; display: flex; align-items: center; justify-content: center; padding: 20px; }
|
||
.modal { background: #161b22; border: 1px solid #30363d; border-radius: 12px; max-width: 650px; width: 100%; max-height: 85vh; overflow-y: auto; padding: 24px; position: relative; }
|
||
.modal-close { position: absolute; top: 12px; right: 16px; background: none; border: none; color: #8b949e; font-size: 20px; cursor: pointer; }
|
||
.modal-close:hover { color: #e1e4e8; }
|
||
.modal h3 { color: #e1e4e8; margin-bottom: 12px; padding-right: 30px; }
|
||
.modal-score { font-size: 24px; font-weight: 700; color: #f0883e; margin-bottom: 16px; }
|
||
.analysis-section { margin-bottom: 4px; }
|
||
.analysis-section h4 { color: #58a6ff; font-size: 13px; margin: 12px 0 4px; }
|
||
.analysis-text { color: #c9d1d9; font-size: 13px; line-height: 1.6; white-space: pre-line; }
|
||
.analysis-tags { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 12px; }
|
||
.analysis-tags .tag { background: #21262d; padding: 3px 8px; border-radius: 4px; font-size: 11px; color: #8b949e; }
|
||
.coin-detail-btn { background: none; border: 1px solid #30363d; color: #58a6ff; font-size: 11px; padding: 2px 8px; border-radius: 4px; cursor: pointer; margin-top: 4px; }
|
||
.coin-detail-btn:hover { background: #21262d; }
|
||
|
||
.progress-panel { background: #1c2128; border: 1px solid #30363d; border-radius: 8px; padding: 16px; margin-bottom: 16px; display: none; }
|
||
.progress-panel.active { display: block; }
|
||
.progress-bar-wrap { background: #21262d; border-radius: 4px; height: 8px; margin: 8px 0; overflow: hidden; }
|
||
.progress-bar-fill { background: #1f6feb; height: 100%; border-radius: 4px; transition: width 0.3s; width: 0%; }
|
||
.progress-log { font-family: monospace; font-size: 12px; color: #8b949e; max-height: 200px; overflow-y: auto; margin-top: 8px; }
|
||
.progress-log div { padding: 2px 0; }
|
||
.progress-log .ok { color: #56d364; }
|
||
.progress-log .err { color: #f85149; }
|
||
.progress-log .info { color: #58a6ff; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="header">
|
||
<h1>Coin Scout</h1>
|
||
<div class="stats" id="stats"></div>
|
||
</div>
|
||
|
||
<div class="tabs">
|
||
<div class="tab active" data-tab="hot">Горячие</div>
|
||
<div class="tab" data-tab="new">Новые</div>
|
||
<div class="tab" data-tab="dashboard">Дашборд</div>
|
||
<div class="tab" data-tab="compare">Сравнение</div>
|
||
<div class="tab" data-tab="all">Все</div>
|
||
<div class="tab" data-tab="method">Методология</div>
|
||
<div class="tab" data-tab="settings">Настройки</div>
|
||
</div>
|
||
|
||
<div class="toolbar" id="toolbar">
|
||
<label>Макс. цена:
|
||
<input type="number" id="f-price" value="3000" step="100" min="0">
|
||
</label>
|
||
<label>Мин. грейд:
|
||
<select id="f-grade">
|
||
<option value="">Любой</option>
|
||
<option value="G">G</option>
|
||
<option value="VG">VG</option>
|
||
<option value="F">F</option>
|
||
<option value="VF" selected>VF</option>
|
||
<option value="XF">XF</option>
|
||
<option value="AU">AU</option>
|
||
<option value="UNC">UNC</option>
|
||
<option value="Proof">Proof</option>
|
||
</select>
|
||
</label>
|
||
<label>Материал:
|
||
<select id="f-material">
|
||
<option value="серебро,золото">Серебро/Золото</option>
|
||
<option value="любой">Любой</option>
|
||
<option value="серебро">Серебро</option>
|
||
<option value="золото">Золото</option>
|
||
</select>
|
||
</label>
|
||
<label>Магазин:
|
||
<select id="f-feed">
|
||
<option value="">Все</option>
|
||
<option value="AT">numizm.at</option>
|
||
<option value="KB">coinsbolhov.ru</option>
|
||
<option value="RU">numizmat.ru</option>
|
||
</select>
|
||
</label>
|
||
<label>
|
||
<input type="checkbox" id="f-stock" checked> В наличии
|
||
</label>
|
||
<label>
|
||
<input type="checkbox" id="f-dupes" checked> Без дублей
|
||
</label>
|
||
<button class="btn btn-primary" onclick="loadCoins()">Применить</button>
|
||
<button class="btn btn-scan" id="btn-scan" onclick="startScan()">Сканировать</button>
|
||
</div>
|
||
|
||
<div class="progress-panel" id="progress-panel">
|
||
<div style="font-size: 14px; font-weight: 500;" id="progress-title">Сканирование...</div>
|
||
<div class="progress-bar-wrap"><div class="progress-bar-fill" id="progress-bar"></div></div>
|
||
<div class="progress-log" id="progress-log"></div>
|
||
</div>
|
||
<div class="content" id="content">
|
||
<div class="loading">Загрузка...</div>
|
||
</div>
|
||
|
||
<script>
|
||
let currentTab = 'hot'
|
||
let settings = {}
|
||
const coinStore = new Map()
|
||
|
||
// Modal
|
||
document.addEventListener('click', (e) => {
|
||
const btn = e.target.closest('.coin-detail-btn')
|
||
if (btn) {
|
||
const coin = coinStore.get(btn.dataset.coinId)
|
||
if (coin) showAnalysisModal(coin)
|
||
return
|
||
}
|
||
if (e.target.classList.contains('modal-overlay')) {
|
||
e.target.remove()
|
||
}
|
||
})
|
||
document.addEventListener('keydown', (e) => {
|
||
if (e.key === 'Escape') {
|
||
const m = document.querySelector('.modal-overlay')
|
||
if (m) m.remove()
|
||
}
|
||
})
|
||
|
||
function showAnalysisModal(c) {
|
||
const overlay = document.createElement('div')
|
||
overlay.className = 'modal-overlay'
|
||
const tags = (c.reasons || []).map(r => `<span class="tag">${r}</span>`).join('')
|
||
overlay.innerHTML = `
|
||
<div class="modal">
|
||
<button class="modal-close" onclick="this.closest('.modal-overlay').remove()">×</button>
|
||
<h3>${c.name}</h3>
|
||
<div class="modal-score">Score: ${c.score}</div>
|
||
${c.image ? `<img src="${c.image}" style="width:120px;height:120px;object-fit:cover;border-radius:8px;margin-bottom:12px" onerror="this.style.display='none'">` : ''}
|
||
<div style="margin-bottom:8px">
|
||
<span style="color:#56d364;font-size:18px;font-weight:600">${c.price} ₽</span>
|
||
${c.old_price && c.old_price > c.price ? `<span style="color:#8b949e;text-decoration:line-through;margin-left:8px">${c.old_price} ₽</span>` : ''}
|
||
<span style="color:#8b949e;margin-left:12px;font-size:13px">${feedNames[c.feed] || c.feed}</span>
|
||
</div>
|
||
<div class="analysis-text">${c.analysis || ''}</div>
|
||
${tags ? `<div class="analysis-tags">${tags}</div>` : ''}
|
||
${c.emailCopy ? `
|
||
<div style="margin-top:20px;border-top:1px solid #30363d;padding-top:16px">
|
||
<div style="display:flex;align-items:center;gap:10px;margin-bottom:8px">
|
||
<h4 style="color:#f0883e;margin:0;font-size:14px">Текст для рассылки</h4>
|
||
<button class="btn" style="font-size:11px;padding:3px 10px" onclick="navigator.clipboard.writeText(this.closest('.modal').querySelector('.email-copy-text').innerText);this.textContent='Скопировано!';setTimeout(()=>this.textContent='Копировать',1500)">Копировать</button>
|
||
</div>
|
||
<div class="email-copy-text" style="background:#0d1117;border:1px solid #30363d;border-radius:8px;padding:14px;font-size:13px;color:#e1e4e8;line-height:1.7;white-space:pre-line">${c.emailCopy}</div>
|
||
</div>
|
||
` : ''}
|
||
<div style="margin-top:16px">
|
||
<a href="${c.url}" target="_blank" class="btn btn-primary" style="text-decoration:none;display:inline-block">Открыть на сайте</a>
|
||
</div>
|
||
</div>`
|
||
document.body.appendChild(overlay)
|
||
}
|
||
|
||
// Tabs
|
||
document.querySelectorAll('.tab').forEach(t => {
|
||
t.addEventListener('click', () => {
|
||
document.querySelectorAll('.tab').forEach(x => x.classList.remove('active'))
|
||
t.classList.add('active')
|
||
currentTab = t.dataset.tab
|
||
const noToolbar = ['settings', 'method', 'dashboard', 'compare']
|
||
document.getElementById('toolbar').style.display = noToolbar.includes(currentTab) ? 'none' : 'flex'
|
||
if (currentTab === 'settings') loadSettings()
|
||
else if (currentTab === 'method') loadMethodology()
|
||
else if (currentTab === 'dashboard') loadDashboard()
|
||
else if (currentTab === 'compare') loadCompare()
|
||
else if (currentTab === 'new') loadNew()
|
||
else if (currentTab === 'all') loadAll()
|
||
else loadCoins()
|
||
})
|
||
})
|
||
|
||
// Format helpers
|
||
const feedNames = { AT: 'numizm.at', KB: 'coinsbolhov.ru', RU: 'numizmat.ru' }
|
||
function materialClass(m) {
|
||
if (!m) return ''
|
||
const l = m.toLowerCase()
|
||
if (/золото|gold/.test(l)) return 'gold'
|
||
if (/серебро|silver|биллон/.test(l)) return 'silver'
|
||
return ''
|
||
}
|
||
function gradeClass(g) {
|
||
if (!g) return ''
|
||
const gs = ['G','AG','VG','F','VF','XF','EF','AU','UNC','BU','Proof']
|
||
const upper = g.toUpperCase()
|
||
for (let i = gs.length - 1; i >= 0; i--) {
|
||
if (upper.includes(gs[i])) return i >= 6 ? 'grade-high' : i >= 4 ? 'grade-mid' : ''
|
||
}
|
||
return ''
|
||
}
|
||
|
||
function coinCard(c, rank) {
|
||
const isTop = rank < 3
|
||
return `
|
||
<div class="coin-card ${isTop ? 'top' : ''}">
|
||
${c.image ? `<img class="coin-img" src="${c.image}" loading="lazy" onerror="this.style.display='none'">` : ''}
|
||
<div class="coin-info">
|
||
<div class="coin-name"><a href="${c.url}" target="_blank">${c.name}</a></div>
|
||
<div class="coin-meta">
|
||
${c.grade ? `<span class="tag ${gradeClass(c.grade)}">${c.grade}</span>` : '<span class="tag">? грейд</span>'}
|
||
${c.material ? `<span class="tag ${materialClass(c.material)}">${c.material}</span>` : ''}
|
||
${c.year_from ? `<span class="tag">${c.year_from}${c.year_to && c.year_to !== c.year_from ? '–' + c.year_to : ''}</span>` : ''}
|
||
${c.in_stock === 0 ? '<span class="tag no-stock">Нет в наличии</span>' : ''}
|
||
</div>
|
||
<div class="coin-price">${c.price} ₽${c.old_price && c.old_price > c.price ? `<span class="old">${c.old_price} ₽</span>` : ''}</div>
|
||
${c.score ? `<div class="coin-score">Score: ${c.score}</div>` : ''}
|
||
${c.summary ? `<div style="font-size:11px;color:#c9d1d9;margin-top:3px;line-height:1.4">${c.summary}</div>` : ''}
|
||
${c.analysis ? `<button class="coin-detail-btn" data-coin-id="${c.id}">Подробный анализ</button>` : ''}
|
||
<div class="coin-feed">${feedNames[c.feed] || c.feed}</div>
|
||
</div>
|
||
</div>`
|
||
}
|
||
|
||
async function loadCoins() {
|
||
const content = document.getElementById('content')
|
||
content.innerHTML = '<div class="loading">Загрузка...</div>'
|
||
const params = new URLSearchParams({
|
||
max_price: document.getElementById('f-price').value,
|
||
min_grade: document.getElementById('f-grade').value,
|
||
material: document.getElementById('f-material').value,
|
||
in_stock: document.getElementById('f-stock').checked ? '1' : '0',
|
||
hide_dupes: document.getElementById('f-dupes').checked ? '1' : '0',
|
||
feed: document.getElementById('f-feed').value,
|
||
})
|
||
try {
|
||
const res = await fetch('/api/coins?' + params)
|
||
const data = await res.json()
|
||
if (!data.coins.length) {
|
||
content.innerHTML = '<div class="empty"><h3>Ничего не найдено</h3><p>Попробуйте изменить фильтры или запустить сканирование</p></div>'
|
||
return
|
||
}
|
||
data.coins.forEach(c => coinStore.set(c.id, c))
|
||
content.innerHTML = `<div class="scan-bar"><span>Найдено: <b>${data.total}</b> монет</span></div>
|
||
<div class="coin-grid">${data.coins.map((c, i) => coinCard(c, i)).join('')}</div>`
|
||
} catch (e) {
|
||
content.innerHTML = `<div class="empty"><h3>Ошибка</h3><p>${e.message}</p></div>`
|
||
}
|
||
}
|
||
|
||
async function loadNew() {
|
||
const content = document.getElementById('content')
|
||
content.innerHTML = '<div class="loading">Загрузка...</div>'
|
||
try {
|
||
const res = await fetch('/api/coins/new?days=7')
|
||
const data = await res.json()
|
||
if (!data.coins.length) {
|
||
content.innerHTML = '<div class="empty"><h3>Новых поступлений нет</h3><p>Запустите сканирование или подождите</p></div>'
|
||
return
|
||
}
|
||
data.coins.forEach(c => coinStore.set(c.id, c))
|
||
content.innerHTML = `<div class="scan-bar"><span>Новых за 7 дней: <b>${data.total}</b></span></div>
|
||
<div class="coin-grid">${data.coins.map((c, i) => coinCard(c, i)).join('')}</div>`
|
||
} catch (e) {
|
||
content.innerHTML = `<div class="empty"><h3>Ошибка</h3><p>${e.message}</p></div>`
|
||
}
|
||
}
|
||
|
||
async function loadAll() {
|
||
const content = document.getElementById('content')
|
||
content.innerHTML = '<div class="loading">Загрузка...</div>'
|
||
try {
|
||
const res = await fetch('/api/coins/all?limit=200')
|
||
const data = await res.json()
|
||
data.coins.forEach(c => coinStore.set(c.id, c))
|
||
content.innerHTML = `<div class="scan-bar"><span>Всего: <b>${data.total}</b></span></div>
|
||
<div class="coin-grid">${data.coins.map((c, i) => coinCard(c, 999)).join('')}</div>`
|
||
} catch (e) {
|
||
content.innerHTML = `<div class="empty"><h3>Ошибка</h3><p>${e.message}</p></div>`
|
||
}
|
||
}
|
||
|
||
async function loadSettings() {
|
||
const content = document.getElementById('content')
|
||
try {
|
||
const res = await fetch('/api/settings')
|
||
settings = await res.json()
|
||
content.innerHTML = `
|
||
<div class="settings-panel">
|
||
<h3 style="margin-bottom: 16px;">Настройки Coin Scout</h3>
|
||
<div class="field">
|
||
<label>Максимальная цена по умолчанию (₽)</label>
|
||
<input type="number" id="s-price" value="${settings.max_price || 3000}" step="100">
|
||
</div>
|
||
<div class="field">
|
||
<label>Минимальный грейд</label>
|
||
<select id="s-grade">
|
||
${['G','VG','F','VF','XF','AU','UNC','Proof'].map(g =>
|
||
`<option value="${g}" ${g === settings.min_grade ? 'selected' : ''}>${g}</option>`
|
||
).join('')}
|
||
</select>
|
||
</div>
|
||
<div class="field">
|
||
<label>Предпочтительный материал</label>
|
||
<input type="text" id="s-material" value="${settings.preferred_material || 'серебро,золото'}">
|
||
</div>
|
||
<div class="field">
|
||
<label>Час автосканирования (0-23)</label>
|
||
<input type="number" id="s-hour" value="${settings.auto_scan_hour || 8}" min="0" max="23">
|
||
</div>
|
||
<div class="field">
|
||
<label><input type="checkbox" id="s-enabled" ${settings.scan_enabled === '1' ? 'checked' : ''}> Автосканирование включено</label>
|
||
</div>
|
||
<div class="field">
|
||
<label><input type="checkbox" id="s-dupes" ${settings.hide_dupes === '1' ? 'checked' : ''}> Скрывать дубликаты по умолчанию</label>
|
||
</div>
|
||
<button class="btn btn-primary" onclick="saveSettings()">Сохранить</button>
|
||
<span id="s-saved" style="color: #56d364; margin-left: 12px; display: none;">Сохранено!</span>
|
||
</div>`
|
||
} catch (e) {
|
||
content.innerHTML = `<div class="empty"><p>Ошибка: ${e.message}</p></div>`
|
||
}
|
||
}
|
||
|
||
async function saveSettings() {
|
||
await fetch('/api/settings', {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
max_price: document.getElementById('s-price').value,
|
||
min_grade: document.getElementById('s-grade').value,
|
||
preferred_material: document.getElementById('s-material').value,
|
||
auto_scan_hour: document.getElementById('s-hour').value,
|
||
scan_enabled: document.getElementById('s-enabled').checked ? '1' : '0',
|
||
hide_dupes: document.getElementById('s-dupes').checked ? '1' : '0',
|
||
}),
|
||
})
|
||
const el = document.getElementById('s-saved')
|
||
el.style.display = 'inline'
|
||
setTimeout(() => el.style.display = 'none', 2000)
|
||
}
|
||
|
||
async function startScan() {
|
||
const btn = document.getElementById('btn-scan')
|
||
btn.disabled = true
|
||
btn.textContent = 'Сканирование...'
|
||
|
||
const panel = document.getElementById('progress-panel')
|
||
const bar = document.getElementById('progress-bar')
|
||
const log = document.getElementById('progress-log')
|
||
const title = document.getElementById('progress-title')
|
||
panel.classList.add('active')
|
||
log.innerHTML = ''
|
||
bar.style.width = '0%'
|
||
|
||
function addLog(text, cls = '') {
|
||
const d = document.createElement('div')
|
||
d.className = cls
|
||
d.textContent = text
|
||
log.appendChild(d)
|
||
log.scrollTop = log.scrollHeight
|
||
}
|
||
|
||
// SSE
|
||
const es = new EventSource('/api/scan/progress')
|
||
es.onmessage = (e) => {
|
||
const data = JSON.parse(e.data)
|
||
title.textContent = data.message || 'Сканирование...'
|
||
|
||
if (data.stage === 'start') {
|
||
addLog(data.message, 'info')
|
||
} else if (data.stage === 'feed') {
|
||
addLog(data.message, 'info')
|
||
bar.style.width = ((data.feedIndex + 1) / (data.feedTotal * 2) * 100) + '%'
|
||
} else if (data.stage === 'feed_done') {
|
||
addLog(data.message, 'ok')
|
||
} else if (data.stage === 'feed_error') {
|
||
addLog(data.message, 'err')
|
||
} else if (data.stage === 'saving' || data.stage === 'details') {
|
||
if (data.total) bar.style.width = (50 + (data.current / data.total * 50)) + '%'
|
||
} else if (data.stage === 'done') {
|
||
addLog(data.message, 'ok')
|
||
bar.style.width = '100%'
|
||
es.close()
|
||
btn.disabled = false
|
||
btn.textContent = 'Сканировать'
|
||
setTimeout(() => { panel.classList.remove('active') }, 5000)
|
||
loadCoins()
|
||
loadStats()
|
||
} else if (data.stage === 'error') {
|
||
addLog(data.message, 'err')
|
||
es.close()
|
||
btn.disabled = false
|
||
btn.textContent = 'Сканировать'
|
||
}
|
||
}
|
||
es.onerror = () => {
|
||
es.close()
|
||
}
|
||
|
||
// Trigger scan
|
||
try {
|
||
await fetch('/api/scan', { method: 'POST' })
|
||
} catch (e) {
|
||
btn.disabled = false
|
||
btn.textContent = 'Сканировать'
|
||
addLog('Ошибка: ' + e.message, 'err')
|
||
}
|
||
}
|
||
|
||
async function loadStats() {
|
||
try {
|
||
const res = await fetch('/api/stats')
|
||
const data = await res.json()
|
||
document.getElementById('stats').textContent =
|
||
`${data.total} монет | ${data.withDetails} с деталями | ${data.feeds.map(f => `${feedNames[f.feed] || f.feed}: ${f.cnt}`).join(', ')}`
|
||
} catch (e) {}
|
||
}
|
||
|
||
// ─── Dashboard ───
|
||
async function loadDashboard() {
|
||
const content = document.getElementById('content')
|
||
content.innerHTML = '<div class="loading">Загрузка дашборда...</div>'
|
||
try {
|
||
const res = await fetch('/api/dashboard')
|
||
const d = await res.json()
|
||
const feedNames = { AT: 'numizm.at', KB: 'coinsbolhov.ru', RU: 'numizmat.ru' }
|
||
content.innerHTML = `
|
||
<div style="max-width:900px">
|
||
<h2 style="color:#e1e4e8;margin-bottom:16px">Дашборд</h2>
|
||
|
||
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin-bottom:20px">
|
||
<div style="background:#161b22;border:1px solid #30363d;border-radius:8px;padding:16px;text-align:center">
|
||
<div style="font-size:28px;font-weight:700;color:#56d364">${d.total.toLocaleString()}</div>
|
||
<div style="color:#8b949e;font-size:13px">Всего монет</div>
|
||
</div>
|
||
<div style="background:#161b22;border:1px solid #30363d;border-radius:8px;padding:16px;text-align:center">
|
||
<div style="font-size:28px;font-weight:700;color:#58a6ff">${d.newThisWeek}</div>
|
||
<div style="color:#8b949e;font-size:13px">Новых за 7 дней</div>
|
||
</div>
|
||
<div style="background:#161b22;border:1px solid #30363d;border-radius:8px;padding:16px;text-align:center">
|
||
<div style="font-size:28px;font-weight:700;color:#f0883e">${d.priceDrops.length}</div>
|
||
<div style="color:#8b949e;font-size:13px">Снижения цен</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:12px;margin-bottom:20px">
|
||
${d.feeds.map(f => `
|
||
<div style="background:#161b22;border:1px solid #30363d;border-radius:8px;padding:12px">
|
||
<div style="color:#e1e4e8;font-weight:600">${feedNames[f.feed] || f.feed}</div>
|
||
<div style="color:#8b949e;font-size:13px">${f.cnt.toLocaleString()} монет · средняя ${f.avg_price}₽</div>
|
||
</div>
|
||
`).join('')}
|
||
</div>
|
||
|
||
${d.priceDrops.length ? `
|
||
<h3 style="color:#f0883e;margin:20px 0 10px">Снижения цен</h3>
|
||
<table style="width:100%;border-collapse:collapse;font-size:13px">
|
||
<tr style="border-bottom:1px solid #30363d;color:#8b949e"><th style="text-align:left;padding:6px">Монета</th><th>Было</th><th>Стало</th><th>Δ</th><th>Магазин</th></tr>
|
||
${d.priceDrops.map(c => `
|
||
<tr style="border-bottom:1px solid #21262d">
|
||
<td style="padding:6px"><a href="${c.url}" target="_blank">${c.name.substring(0,50)}</a></td>
|
||
<td style="text-align:center;color:#8b949e">${c.prev_price}₽</td>
|
||
<td style="text-align:center;color:#56d364">${c.price}₽</td>
|
||
<td style="text-align:center;color:#f0883e">−${c.drop_pct}%</td>
|
||
<td style="text-align:center;color:#8b949e">${feedNames[c.feed] || c.feed}</td>
|
||
</tr>
|
||
`).join('')}
|
||
</table>
|
||
` : ''}
|
||
|
||
${d.priceRises.length ? `
|
||
<h3 style="color:#f85149;margin:20px 0 10px">Повышения цен</h3>
|
||
<table style="width:100%;border-collapse:collapse;font-size:13px">
|
||
<tr style="border-bottom:1px solid #30363d;color:#8b949e"><th style="text-align:left;padding:6px">Монета</th><th>Было</th><th>Стало</th><th>Δ</th><th>Магазин</th></tr>
|
||
${d.priceRises.slice(0,10).map(c => `
|
||
<tr style="border-bottom:1px solid #21262d">
|
||
<td style="padding:6px"><a href="${c.url}" target="_blank">${c.name.substring(0,50)}</a></td>
|
||
<td style="text-align:center;color:#8b949e">${c.prev_price}₽</td>
|
||
<td style="text-align:center;color:#f85149">${c.price}₽</td>
|
||
<td style="text-align:center;color:#f85149">+${c.rise_pct}%</td>
|
||
<td style="text-align:center;color:#8b949e">${feedNames[c.feed] || c.feed}</td>
|
||
</tr>
|
||
`).join('')}
|
||
</table>
|
||
` : ''}
|
||
|
||
${d.disappeared.length ? `
|
||
<h3 style="color:#8b949e;margin:20px 0 10px">Исчезли за 7 дней (проданы?)</h3>
|
||
<div style="font-size:13px;color:#8b949e">
|
||
${d.disappeared.map(c => `<div style="padding:3px 0">${c.name.substring(0,60)} · ${c.price}₽ · ${feedNames[c.feed] || c.feed}</div>`).join('')}
|
||
</div>
|
||
` : ''}
|
||
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-top:20px">
|
||
<div>
|
||
<h3 style="color:#58a6ff;margin-bottom:8px">По материалу</h3>
|
||
<table style="width:100%;font-size:12px;border-collapse:collapse">
|
||
${d.materials.map(m => `<tr style="border-bottom:1px solid #21262d"><td style="padding:3px">${m.material}</td><td style="text-align:right">${m.cnt}</td><td style="text-align:right;color:#8b949e">~${m.avg_price}₽</td></tr>`).join('')}
|
||
</table>
|
||
</div>
|
||
<div>
|
||
<h3 style="color:#58a6ff;margin-bottom:8px">По грейду</h3>
|
||
<table style="width:100%;font-size:12px;border-collapse:collapse">
|
||
${d.grades.map(g => `<tr style="border-bottom:1px solid #21262d"><td style="padding:3px">${g.grade}</td><td style="text-align:right">${g.cnt}</td><td style="text-align:right;color:#8b949e">~${g.avg_price}₽</td></tr>`).join('')}
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>`
|
||
} catch (e) {
|
||
content.innerHTML = `<div class="empty"><p>Ошибка: ${e.message}</p></div>`
|
||
}
|
||
}
|
||
|
||
// ─── Cross-store comparison ───
|
||
async function loadCompare() {
|
||
const content = document.getElementById('content')
|
||
content.innerHTML = '<div class="loading">Сравнение магазинов...</div>'
|
||
try {
|
||
const res = await fetch('/api/compare')
|
||
const d = await res.json()
|
||
const feedNames = { AT: 'numizm.at', KB: 'coinsbolhov.ru', RU: 'numizmat.ru' }
|
||
if (!d.comparisons.length) {
|
||
content.innerHTML = '<div class="empty"><h3>Совпадений не найдено</h3><p>Нужно больше сканов чтобы найти одинаковые монеты в разных магазинах</p></div>'
|
||
return
|
||
}
|
||
content.innerHTML = `
|
||
<div style="max-width:900px">
|
||
<h2 style="color:#e1e4e8;margin-bottom:16px">Сравнение магазинов</h2>
|
||
<p style="color:#8b949e;margin-bottom:16px;font-size:13px">Одинаковые монеты в разных магазинах, отсортированные по разнице в цене.</p>
|
||
<table style="width:100%;border-collapse:collapse;font-size:13px">
|
||
<tr style="border-bottom:1px solid #30363d;color:#8b949e">
|
||
<th style="text-align:left;padding:8px">Монета</th>
|
||
<th>Магазин 1</th><th>Цена 1</th>
|
||
<th>Магазин 2</th><th>Цена 2</th>
|
||
<th>Разница</th>
|
||
</tr>
|
||
${d.comparisons.map(c => {
|
||
const cheaper = c.price1 <= c.price2 ? 1 : 2
|
||
return `<tr style="border-bottom:1px solid #21262d">
|
||
<td style="padding:8px;max-width:250px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${c.name.substring(0,50)}</td>
|
||
<td style="text-align:center"><a href="${c.url1}" target="_blank">${feedNames[c.feed1] || c.feed1}</a></td>
|
||
<td style="text-align:center;${cheaper===1?'color:#56d364;font-weight:600':''}">${c.price1}₽</td>
|
||
<td style="text-align:center"><a href="${c.url2}" target="_blank">${feedNames[c.feed2] || c.feed2}</a></td>
|
||
<td style="text-align:center;${cheaper===2?'color:#56d364;font-weight:600':''}">${c.price2}₽</td>
|
||
<td style="text-align:center;color:#f0883e">${c.diff_pct}%</td>
|
||
</tr>`
|
||
}).join('')}
|
||
</table>
|
||
</div>`
|
||
} catch (e) {
|
||
content.innerHTML = `<div class="empty"><p>Ошибка: ${e.message}</p></div>`
|
||
}
|
||
}
|
||
|
||
function loadMethodology() {
|
||
document.getElementById('content').innerHTML = `
|
||
<div style="max-width:800px;line-height:1.7;font-size:14px">
|
||
<div style="display:flex;align-items:center;gap:16px;margin-bottom:16px">
|
||
<h2 style="margin:0;color:#e1e4e8">Методология оценки инвестиционной привлекательности монет</h2>
|
||
<a href="/methodology.html?print" target="_blank" class="btn btn-primary" style="text-decoration:none;white-space:nowrap">Скачать PDF</a>
|
||
</div>
|
||
<p style="color:#8b949e;margin-bottom:20px">Скоринг основан на анализе <b>40+ профессиональных источников</b> по нумизматике: PCGS, NGC, Forbes.ru, numizmatik.ru, coinweek.com, zolotoy-zapas.ru, numisdon.com, raritetus.ru и др. Цена серебра: ~200₽/грамм (ЦБ РФ, апрель 2026).</p>
|
||
|
||
<h3 style="color:#f0883e;margin:24px 0 12px">1. Сохранность / Грейд (до 30 очков)</h3>
|
||
<p style="color:#8b949e;margin-bottom:8px">Каждый шаг грейда может увеличить цену в 2-50 раз. Переход AU→UNC — самый большой скачок.</p>
|
||
<table style="width:100%;border-collapse:collapse;margin-bottom:12px">
|
||
<tr style="border-bottom:1px solid #30363d"><td style="padding:4px 8px;color:#8b949e">Proof</td><td style="padding:4px 8px;color:#56d364">+28</td><td style="padding:4px 8px;color:#8b949e">Зеркальная поверхность, высшее качество</td></tr>
|
||
<tr style="border-bottom:1px solid #30363d"><td style="padding:4px 8px;color:#8b949e">UNC</td><td style="padding:4px 8px;color:#56d364">+25</td><td style="padding:4px 8px;color:#8b949e">Не была в обращении</td></tr>
|
||
<tr style="border-bottom:1px solid #30363d"><td style="padding:4px 8px;color:#8b949e">AU</td><td style="padding:4px 8px;color:#56d364">+20</td><td style="padding:4px 8px;color:#8b949e">Почти без следов обращения</td></tr>
|
||
<tr style="border-bottom:1px solid #30363d"><td style="padding:4px 8px;color:#8b949e">XF</td><td style="padding:4px 8px;color:#56d364">+15</td><td style="padding:4px 8px;color:#8b949e">Лёгкий износ на выступающих частях</td></tr>
|
||
<tr><td style="padding:4px 8px;color:#8b949e">VF</td><td style="padding:4px 8px;color:#56d364">+8</td><td style="padding:4px 8px;color:#8b949e">Умеренный износ, все детали читаемы</td></tr>
|
||
</table>
|
||
<p style="color:#8b949e">Бонус: VF+ для монет до 1800 г. (+8), XF для XIX века (+5) — исключительная сохранность для возраста.</p>
|
||
|
||
<h3 style="color:#f0883e;margin:24px 0 12px">2. Материал и стоимость металла (до 25 очков)</h3>
|
||
<p style="color:#8b949e;margin-bottom:8px">Драгоценный металл создаёт «пол» стоимости — монета не может стоить дешевле металла.</p>
|
||
<table style="width:100%;border-collapse:collapse;margin-bottom:12px">
|
||
<tr style="border-bottom:1px solid #30363d"><td style="padding:4px 8px;color:#8b949e">Золото</td><td style="padding:4px 8px;color:#56d364">+22</td><td style="padding:4px 8px;color:#8b949e">Надёжный актив, всегда ликвиден</td></tr>
|
||
<tr style="border-bottom:1px solid #30363d"><td style="padding:4px 8px;color:#8b949e">Серебро</td><td style="padding:4px 8px;color:#56d364">+14</td><td style="padding:4px 8px;color:#8b949e">+ бонус если цена близка к стоимости металла</td></tr>
|
||
<tr style="border-bottom:1px solid #30363d"><td style="padding:4px 8px;color:#8b949e">Серебро < melt</td><td style="padding:4px 8px;color:#56d364">+10 доп.</td><td style="padding:4px 8px;color:#8b949e">Дешевле стоимости серебра — безрисковая покупка</td></tr>
|
||
<tr><td style="padding:4px 8px;color:#8b949e">Биллон</td><td style="padding:4px 8px;color:#56d364">+6</td><td style="padding:4px 8px;color:#8b949e">Сплав с серебром</td></tr>
|
||
</table>
|
||
|
||
<h3 style="color:#f0883e;margin:24px 0 12px">3. Возраст и историческая значимость (до 20 очков)</h3>
|
||
<p style="color:#8b949e;margin-bottom:8px">Античные монеты показывают 8-15% годового роста (numisdon.com). Возраст + сохранность = экспоненциальный рост ценности.</p>
|
||
<table style="width:100%;border-collapse:collapse;margin-bottom:12px">
|
||
<tr style="border-bottom:1px solid #30363d"><td style="padding:4px 8px;color:#8b949e">До н.э.</td><td style="padding:4px 8px;color:#56d364">+20</td><td style="padding:4px 8px;color:#8b949e">Античная монета</td></tr>
|
||
<tr style="border-bottom:1px solid #30363d"><td style="padding:4px 8px;color:#8b949e">500+ лет</td><td style="padding:4px 8px;color:#56d364">+18</td><td style="padding:4px 8px;color:#8b949e">Средневековье</td></tr>
|
||
<tr style="border-bottom:1px solid #30363d"><td style="padding:4px 8px;color:#8b949e">300+ лет</td><td style="padding:4px 8px;color:#56d364">+14</td><td style="padding:4px 8px;color:#8b949e">Раннее Новое время</td></tr>
|
||
<tr style="border-bottom:1px solid #30363d"><td style="padding:4px 8px;color:#8b949e">200+ лет</td><td style="padding:4px 8px;color:#56d364">+10</td><td style="padding:4px 8px;color:#8b949e"></td></tr>
|
||
<tr><td style="padding:4px 8px;color:#8b949e">100+ лет</td><td style="padding:4px 8px;color:#56d364">+6</td><td style="padding:4px 8px;color:#8b949e"></td></tr>
|
||
</table>
|
||
|
||
<h3 style="color:#f0883e;margin:24px 0 12px">4. Российские премиум-периоды (до 15 очков)</h3>
|
||
<p style="color:#8b949e;margin-bottom:8px">Царские монеты растут 10-15% в год (Forbes.ru). Особые периоды и правители:</p>
|
||
<table style="width:100%;border-collapse:collapse;margin-bottom:12px">
|
||
<tr style="border-bottom:1px solid #30363d"><td style="padding:4px 8px;color:#8b949e">СССР 1947, 1958</td><td style="padding:4px 8px;color:#56d364">+15</td><td style="padding:4px 8px;color:#8b949e">Не выпущены в обращение, раритет</td></tr>
|
||
<tr style="border-bottom:1px solid #30363d"><td style="padding:4px 8px;color:#8b949e">Пробные монеты</td><td style="padding:4px 8px;color:#56d364">+12</td><td style="padding:4px 8px;color:#8b949e">Коллекционная элита</td></tr>
|
||
<tr style="border-bottom:1px solid #30363d"><td style="padding:4px 8px;color:#8b949e">Смутное время (1610-1612)</td><td style="padding:4px 8px;color:#56d364">+10</td><td style="padding:4px 8px;color:#8b949e">Редчайший период русской нумизматики</td></tr>
|
||
<tr style="border-bottom:1px solid #30363d"><td style="padding:4px 8px;color:#8b949e">Перепутки</td><td style="padding:4px 8px;color:#56d364">+10</td><td style="padding:4px 8px;color:#8b949e">Монета на чужой заготовке</td></tr>
|
||
<tr style="border-bottom:1px solid #30363d"><td style="padding:4px 8px;color:#8b949e">Раннее СССР серебро (1921-31)</td><td style="padding:4px 8px;color:#56d364">+7</td><td style="padding:4px 8px;color:#8b949e">Малые тиражи серебра</td></tr>
|
||
<tr style="border-bottom:1px solid #30363d"><td style="padding:4px 8px;color:#8b949e">Николай II (1894-1917)</td><td style="padding:4px 8px;color:#56d364">+6</td><td style="padding:4px 8px;color:#8b949e">Культовый период, всегда в спросе</td></tr>
|
||
<tr style="border-bottom:1px solid #30363d"><td style="padding:4px 8px;color:#8b949e">Пётр I (1682-1725)</td><td style="padding:4px 8px;color:#56d364">+6</td><td style="padding:4px 8px;color:#8b949e">Реформатор, высокий спрос</td></tr>
|
||
<tr style="border-bottom:1px solid #30363d"><td style="padding:4px 8px;color:#8b949e">Медный бунт (1654-1656)</td><td style="padding:4px 8px;color:#56d364">+6</td><td style="padding:4px 8px;color:#8b949e">Историческое событие</td></tr>
|
||
<tr style="border-bottom:1px solid #30363d"><td style="padding:4px 8px;color:#8b949e">Сибирские монеты</td><td style="padding:4px 8px;color:#56d364">+6</td><td style="padding:4px 8px;color:#8b949e">Медь с примесью серебра и золота</td></tr>
|
||
<tr style="border-bottom:1px solid #30363d"><td style="padding:4px 8px;color:#8b949e">Георгий Победоносец</td><td style="padding:4px 8px;color:#56d364">+6</td><td style="padding:4px 8px;color:#8b949e">Максимальная ликвидность</td></tr>
|
||
<tr style="border-bottom:1px solid #30363d"><td style="padding:4px 8px;color:#8b949e">Александр II, III</td><td style="padding:4px 8px;color:#56d364">+5</td><td style="padding:4px 8px;color:#8b949e">Растущий спрос / короткое правление</td></tr>
|
||
<tr style="border-bottom:1px solid #30363d"><td style="padding:4px 8px;color:#8b949e">Павел I (1796-1801)</td><td style="padding:4px 8px;color:#56d364">+5</td><td style="padding:4px 8px;color:#8b949e">Короткое правление — мало монет</td></tr>
|
||
<tr style="border-bottom:1px solid #30363d"><td style="padding:4px 8px;color:#8b949e">1 рубль 1924 серебро</td><td style="padding:4px 8px;color:#56d364">+5</td><td style="padding:4px 8px;color:#8b949e">Недооценён относительно редкости</td></tr>
|
||
<tr style="border-bottom:1px solid #30363d"><td style="padding:4px 8px;color:#8b949e">Серебряный рубль Империи</td><td style="padding:4px 8px;color:#56d364">+5</td><td style="padding:4px 8px;color:#8b949e">Топ коллекционного спроса</td></tr>
|
||
<tr style="border-bottom:1px solid #30363d"><td style="padding:4px 8px;color:#8b949e">Екатерина II, Анна, Елизавета</td><td style="padding:4px 8px;color:#56d364">+4</td><td style="padding:4px 8px;color:#8b949e">Популярные периоды</td></tr>
|
||
<tr><td style="padding:4px 8px;color:#8b949e">Русская Финляндия серебро</td><td style="padding:4px 8px;color:#56d364">+4</td><td style="padding:4px 8px;color:#8b949e">Узкая серия</td></tr>
|
||
</table>
|
||
|
||
<h3 style="color:#f0883e;margin:24px 0 12px">5. Мировые монеты (до 10 очков)</h3>
|
||
<table style="width:100%;border-collapse:collapse;margin-bottom:12px">
|
||
<tr style="border-bottom:1px solid #30363d"><td style="padding:4px 8px;color:#8b949e">Древняя Греция</td><td style="padding:4px 8px;color:#56d364">+6</td><td style="padding:4px 8px;color:#8b949e">~15% годового роста</td></tr>
|
||
<tr style="border-bottom:1px solid #30363d"><td style="padding:4px 8px;color:#8b949e">Византия</td><td style="padding:4px 8px;color:#56d364">+5 (+4 золото)</td><td style="padding:4px 8px;color:#8b949e">Доступная античная империя</td></tr>
|
||
<tr style="border-bottom:1px solid #30363d"><td style="padding:4px 8px;color:#8b949e">Римская империя</td><td style="padding:4px 8px;color:#56d364">+5</td><td style="padding:4px 8px;color:#8b949e">Растущий глобальный рынок</td></tr>
|
||
<tr style="border-bottom:1px solid #30363d"><td style="padding:4px 8px;color:#8b949e">Боспорское царство</td><td style="padding:4px 8px;color:#56d364">+5</td><td style="padding:4px 8px;color:#8b949e">Крымская античность</td></tr>
|
||
<tr style="border-bottom:1px solid #30363d"><td style="padding:4px 8px;color:#8b949e">Сефевиды, Талеры, Панды</td><td style="padding:4px 8px;color:#56d364">+5</td><td style="padding:4px 8px;color:#8b949e">Недооценённые сегменты</td></tr>
|
||
<tr style="border-bottom:1px solid #30363d"><td style="padding:4px 8px;color:#8b949e">Османская империя</td><td style="padding:4px 8px;color:#56d364">+4</td><td style="padding:4px 8px;color:#8b949e">Недооценённый сегмент</td></tr>
|
||
<tr style="border-bottom:1px solid #30363d"><td style="padding:4px 8px;color:#8b949e">Соверены, Лунные серии</td><td style="padding:4px 8px;color:#56d364">+4</td><td style="padding:4px 8px;color:#8b949e">Стабильный глобальный спрос</td></tr>
|
||
<tr><td style="padding:4px 8px;color:#8b949e">Японские монеты 100+ лет</td><td style="padding:4px 8px;color:#56d364">+4</td><td style="padding:4px 8px;color:#8b949e">Бум азиатской нумизматики</td></tr>
|
||
</table>
|
||
|
||
<h3 style="color:#f0883e;margin:24px 0 12px">6. Ошибки чеканки и разновидности (до 15 очков)</h3>
|
||
<table style="width:100%;border-collapse:collapse;margin-bottom:12px">
|
||
<tr style="border-bottom:1px solid #30363d"><td style="padding:4px 8px;color:#8b949e">Мул / двойной аверс</td><td style="padding:4px 8px;color:#56d364">+15</td><td style="padding:4px 8px;color:#8b949e">Крайне редкий брак</td></tr>
|
||
<tr style="border-bottom:1px solid #30363d"><td style="padding:4px 8px;color:#8b949e">Брак чеканки (смещение, раскол, перечекан...)</td><td style="padding:4px 8px;color:#56d364">+12</td><td style="padding:4px 8px;color:#8b949e">Коллекционная редкость</td></tr>
|
||
<tr style="border-bottom:1px solid #30363d"><td style="padding:4px 8px;color:#8b949e">Ефимок / надчеканка</td><td style="padding:4px 8px;color:#56d364">+10</td><td style="padding:4px 8px;color:#8b949e">Нумизматическая элита</td></tr>
|
||
<tr style="border-bottom:1px solid #30363d"><td style="padding:4px 8px;color:#8b949e">Серия ЧЯП (10₽ биметалл)</td><td style="padding:4px 8px;color:#56d364">+10</td><td style="padding:4px 8px;color:#8b949e">Малотиражная, высокий спрос</td></tr>
|
||
<tr style="border-bottom:1px solid #30363d"><td style="padding:4px 8px;color:#8b949e">Без знака монетного двора</td><td style="padding:4px 8px;color:#56d364">+8</td><td style="padding:4px 8px;color:#8b949e">Редкий вариант</td></tr>
|
||
<tr><td style="padding:4px 8px;color:#8b949e">Широкий кант 1997/1998 ММД</td><td style="padding:4px 8px;color:#56d364">+8</td><td style="padding:4px 8px;color:#8b949e">Редкий производственный вариант</td></tr>
|
||
</table>
|
||
|
||
<h3 style="color:#f0883e;margin:24px 0 12px">7. Ценовая эффективность (до 12 очков)</h3>
|
||
<p style="color:#8b949e;margin-bottom:8px">Главное правило: покупай самую редкую монету в лучшем состоянии за доступную цену.</p>
|
||
<table style="width:100%;border-collapse:collapse;margin-bottom:12px">
|
||
<tr style="border-bottom:1px solid #30363d"><td style="padding:4px 8px;color:#8b949e">Скидка 30%+</td><td style="padding:4px 8px;color:#56d364">+8</td><td style="padding:4px 8px;color:#8b949e">Сильное снижение цены</td></tr>
|
||
<tr style="border-bottom:1px solid #30363d"><td style="padding:4px 8px;color:#8b949e">AU+ дешевле 500₽</td><td style="padding:4px 8px;color:#56d364">+6</td><td style="padding:4px 8px;color:#8b949e">Ниже рыночной стоимости</td></tr>
|
||
<tr style="border-bottom:1px solid #30363d"><td style="padding:4px 8px;color:#8b949e">Скидка 15%+</td><td style="padding:4px 8px;color:#56d364">+5</td><td style="padding:4px 8px;color:#8b949e"></td></tr>
|
||
<tr><td style="padding:4px 8px;color:#8b949e">UNC до 1000₽</td><td style="padding:4px 8px;color:#56d364">+4</td><td style="padding:4px 8px;color:#8b949e">Выгодная цена для грейда</td></tr>
|
||
</table>
|
||
|
||
<h3 style="color:#f85149;margin:24px 0 12px">8. Штрафы (негативные факторы)</h3>
|
||
<table style="width:100%;border-collapse:collapse;margin-bottom:12px">
|
||
<tr style="border-bottom:1px solid #30363d"><td style="padding:4px 8px;color:#8b949e">Копия / жетон / сувенир</td><td style="padding:4px 8px;color:#f85149">-20</td><td style="padding:4px 8px;color:#8b949e">Не оригинальная монета</td></tr>
|
||
<tr style="border-bottom:1px solid #30363d"><td style="padding:4px 8px;color:#8b949e">Массовые юбилейные СССР</td><td style="padding:4px 8px;color:#f85149">-12</td><td style="padding:4px 8px;color:#8b949e">Не Proof, не драгмет — нет вторичного рынка</td></tr>
|
||
<tr style="border-bottom:1px solid #30363d"><td style="padding:4px 8px;color:#8b949e">Чищеная монета</td><td style="padding:4px 8px;color:#f85149">-10</td><td style="padding:4px 8px;color:#8b949e">Потеря 50-90% коллекционной ценности (PCGS)</td></tr>
|
||
<tr style="border-bottom:1px solid #30363d"><td style="padding:4px 8px;color:#8b949e">Современные памятные ЦБ РФ</td><td style="padding:4px 8px;color:#f85149">-8</td><td style="padding:4px 8px;color:#8b949e">Слабый вторичный рынок</td></tr>
|
||
<tr style="border-bottom:1px solid #30363d"><td style="padding:4px 8px;color:#8b949e">Медно-никель после 1950</td><td style="padding:4px 8px;color:#f85149">-4</td><td style="padding:4px 8px;color:#8b949e">Массовый чекан, нет Proof</td></tr>
|
||
<tr><td style="padding:4px 8px;color:#8b949e">Проволочные монеты (чешуя)</td><td style="padding:4px 8px;color:#f85149">-2</td><td style="padding:4px 8px;color:#8b949e">Высокий риск подделки</td></tr>
|
||
</table>
|
||
|
||
<h3 style="color:#58a6ff;margin:24px 0 12px">Источники (40+)</h3>
|
||
<div style="color:#8b949e;font-size:12px;columns:2;column-gap:24px">
|
||
<p><a href="https://www.pcgs.com/prices/us" target="_blank">PCGS Price Guide</a></p>
|
||
<p><a href="https://www.ngccoin.com/price-guide/" target="_blank">NGC Price Guide</a></p>
|
||
<p><a href="https://www.forbes.ru/finansy-i-investicii/356677" target="_blank">Forbes.ru — Ценная мелочь</a></p>
|
||
<p><a href="https://www.numizmatik.ru/biblio/top-redkikh-monet-2025-2026" target="_blank">numizmatik.ru — Редкие монеты 2025-2026</a></p>
|
||
<p><a href="https://www.zolotoy-zapas.ru/why-gold-coins/useful/" target="_blank">Золотой Запас — гайды</a></p>
|
||
<p><a href="https://rarecoins.ru/stati/kriterii-ocenki-stoimosti-monet-v-numizmatike.html" target="_blank">Аукционный дом Редкие Монеты</a></p>
|
||
<p><a href="https://www.numisdon.com/ancient-coins-good-investment-2025/" target="_blank">NumisDon — Ancient coins 2025</a></p>
|
||
<p><a href="https://thebullionbank.com/blog/undervalued-silver-coins-2025" target="_blank">BullionBank — Undervalued Silver</a></p>
|
||
<p><a href="https://thecoinsexplorer.com/best-rare-coins-investment-2025/" target="_blank">CoinsExplorer — Best Rare Coins</a></p>
|
||
<p><a href="https://coinweek.com/patina-on-ancient-bronze-coins/" target="_blank">CoinWeek — Patina Guide</a></p>
|
||
<p><a href="https://www.raritetus.ru/stoimost-monet/" target="_blank">Raritetus — каталог цен</a></p>
|
||
<p><a href="https://www.cbr.ru/hd_base/metall/metall_base_new/" target="_blank">ЦБ РФ — цена серебра</a></p>
|
||
<p><a href="https://www.pcgs.com/news/how-to-determine-the-value-of-a-mint-error-coin" target="_blank">PCGS — Error Coin Guide</a></p>
|
||
<p><a href="https://www.numizmatik.ru/biblio/brak-na-monetakh" target="_blank">numizmatik.ru — Брак монет</a></p>
|
||
<p><a href="https://www.russian-money.ru/articles/1rub-1997-shirokii-kant" target="_blank">russian-money.ru — Широкий кант</a></p>
|
||
<p><a href="https://coins.tsbnk.ru/" target="_blank">ТрансСтройБанк — каталог</a></p>
|
||
<p><a href="https://www.usagold.com/gold-silver-ratio-guide/" target="_blank">USAGOLD — Gold/Silver Ratio</a></p>
|
||
<p><a href="https://www.rarecoins101.com/coin-investments.html" target="_blank">RareCoins101 — Returns</a></p>
|
||
<p><a href="https://lermontovgallery.ru/spravochnik-antikvariata/" target="_blank">Лермонтов — царские монеты</a></p>
|
||
<p><a href="https://www.numisdon.com/byzantine-coins-value/" target="_blank">NumisDon — Byzantine Value</a></p>
|
||
<p><a href="https://stonexbullion.com/en/blog/rise-of-asian-bullion-coins/" target="_blank">StoneX — Asian Bullion Coins</a></p>
|
||
<p><a href="https://www.greysheet.com/news/story/five-coin-collecting-investment-mistakes-to-avoid" target="_blank">Greysheet — 5 Mistakes to Avoid</a></p>
|
||
<p><a href="https://www.marketresearchfuture.com/reports/coin-collecting-market-22623" target="_blank">Market Research — 8% CAGR</a></p>
|
||
<p><a href="https://www.coingraderai.com/blog/natural-vs-artificial-coin-toning-grading" target="_blank">CoinGraderAI — Toning</a></p>
|
||
<p><a href="https://deigoldandsilvercoins.com/top-underappreciated-world-coins-with-hidden-long-term-value/" target="_blank">DEI — Underappreciated World Coins</a></p>
|
||
</div>
|
||
|
||
<h3 style="color:#58a6ff;margin:24px 0 12px">Статистика рынка</h3>
|
||
<ul style="color:#8b949e">
|
||
<li>Средний рост рынка монет: <b>8% в год</b> (CAGR, прогноз до 2035)</li>
|
||
<li>PCGS Key Dates Index: <b>4.8% годовых</b> за 25 лет (2000-2025)</li>
|
||
<li>Топовые монеты: <b>12-14% годовых</b> (PCGS3000 Index с 1971)</li>
|
||
<li>Царские монеты: <b>10-15% годовых</b> (Forbes.ru)</li>
|
||
<li>Античные монеты: <b>8-15% годовых</b> (numisdon.com)</li>
|
||
<li>Рекомендуемый горизонт инвестиций: <b>7-10 лет минимум</b></li>
|
||
<li>Дилерская наценка: 10-30% — монета должна вырасти на столько же для безубытка</li>
|
||
</ul>
|
||
|
||
<h3 style="color:#58a6ff;margin:24px 0 12px">Фильтрация</h3>
|
||
<p style="color:#8b949e">Из фидов автоматически исключаются: открытки, альбомы, капсулы, листы, подставки, футляры, лупы, пинцеты, рамки, планшеты, холдеры, книги.</p>
|
||
|
||
<p style="color:#8b949e;margin-top:24px;font-size:12px"><i>Disclaimer: скоринг является аналитическим инструментом, не финансовой рекомендацией. Всегда проверяйте монету лично перед покупкой.</i></p>
|
||
</div>`
|
||
}
|
||
|
||
// Init
|
||
loadStats()
|
||
loadCoins()
|
||
</script>
|
||
</body>
|
||
</html>
|