version a0.0.1

This commit is contained in:
netadmin committed 2026-10-22 20:45:13 +03:00
commit 9469ef10b6
19 files changed
+4281

No files matched your search

+261
View File
@@ -0,0 +1,261 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LAN Share — Текст в ОЗУ / Медиа на Диск</title>
<style>
:root { --bg: #0f172a; --card: #1e293b; --border: #334155; --accent: #3b82f6; --green: #10b981; --red: #ef4444; --text: #f8fafc; --muted: #94a3b8; }
* { box-sizing: border-box; font-family: system-ui, -apple-system, sans-serif; }
body { margin: 0; padding: 20px; background: var(--bg); color: var(--text); }
.container { max-width: 1100px; margin: 0 auto; display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
@media (max-width: 800px) { .container { grid-template-columns: 1fr; } }
.header { grid-column: 1 / -1; background: var(--card); border: 1px solid var(--border); padding: 16px 20px; border-radius: 12px; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 12px; }
.pin-badge { background: #1d4ed8; padding: 6px 14px; border-radius: 8px; font-weight: 700; letter-spacing: 1px; font-family: monospace; font-size: 1.1rem; }
.card { background: var(--card); border: 1px solid var(--border); border-radius: 12px; padding: 18px; display: flex; flex-direction: column; gap: 14px; }
h2 { margin: 0; font-size: 1.1rem; display: flex; justify-content: space-between; align-items: center; }
.tag { font-size: 0.75rem; padding: 3px 8px; border-radius: 6px; font-weight: 600; }
.tag-ram { background: rgba(16, 185, 129, 0.2); color: #34d399; }
.tag-disk { background: rgba(59, 130, 246, 0.2); color: #60a5fa; }
select, textarea, input[type="file"] { width: 100%; background: var(--bg); color: var(--text); border: 1px solid var(--border); border-radius: 8px; padding: 10px; font-size: 0.95rem; }
textarea { resize: vertical; min-height: 80px; }
button { background: var(--accent); color: white; border: none; border-radius: 8px; padding: 10px 16px; font-weight: 600; cursor: pointer; transition: opacity 0.15s; }
button:hover { opacity: 0.9; }
button.btn-green { background: var(--green); }
button.btn-red { background: var(--red); }
button.btn-sm { padding: 6px 12px; font-size: 0.82rem; }
.peer-item, .msg-item { background: var(--bg); border: 1px solid var(--border); border-radius: 8px; padding: 12px; display: flex; justify-content: space-between; align-items: center; gap: 10px; }
.msg-item { flex-direction: column; align-items: flex-start; }
.msg-meta { width: 100%; display: flex; justify-content: space-between; font-size: 0.8rem; color: var(--muted); }
.msg-text { font-size: 0.95rem; white-space: pre-wrap; word-break: break-word; width: 100%; margin: 4px 0; }
.path-box { font-family: monospace; font-size: 0.78rem; color: #93c5fd; background: rgba(59,130,246,0.1); padding: 4px 8px; border-radius: 4px; word-break: break-all; }
.preview-img { max-width: 100%; max-height: 180px; border-radius: 6px; margin-top: 6px; }
#status { grid-column: 1 / -1; padding: 10px 16px; border-radius: 8px; display: none; font-weight: 500; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<div>
<div style="font-size: 1.2rem; font-weight: 700;" id="my-name">Загрузка...</div>
<div style="font-size: 0.85rem; color: var(--muted);" id="my-ip"></div>
</div>
<div>
<span style="font-size: 0.85rem; color: var(--muted); margin-right: 8px;">Код сверки сертификата (PIN):</span>
<span class="pin-badge" id="my-pin">--------</span>
</div>
</div>
<div id="status"></div>
<div class="card">
<h2>1. Устройства в сети (mDNS) <span class="tag tag-disk">TLS Pinning</span></h2>
<div id="peers-list" style="display:flex; flex-direction:column; gap:8px;">
<div style="color:var(--muted); font-size:0.9rem;">Поиск устройств...</div>
</div>
</div>
<div class="card">
<h2>2. Отправка данных</h2>
<label style="font-size:0.85rem; color:var(--muted);">Получатель (должен быть заверен):</label>
<select id="target-peer"><option value="">-- Выберите устройство --</option></select>
<textarea id="text-input" placeholder="Введите текст (Enter — отправить, Shift+Enter — новая строка)..."></textarea>
<button onclick="sendText()">Отправить текст в ОЗУ</button>
<hr style="border:0; border-top:1px solid var(--border); width:100%; margin: 4px 0;">
<input type="file" id="media-input">
<button class="btn-green" onclick="sendMedia()">Отправить медиа на диск получателя</button>
</div>
<div class="card">
<h2>
<span>Входящий текст <span class="tag tag-ram">ТОЛЬКО В ОЗУ</span></span>
<button class="btn-red btn-sm" onclick="clearRam()">Очистить ОЗУ</button>
</h2>
<div id="ram-texts" style="display:flex; flex-direction:column; gap:10px; max-height: 400px; overflow-y: auto;"></div>
</div>
<div class="card">
<h2>Входящие медиафайлы <span class="tag tag-disk">НА ДИСКЕ (./received_media)</span></h2>
<div id="disk-media" style="display:flex; flex-direction:column; gap:10px; max-height: 400px; overflow-y: auto;"></div>
</div>
</div>
<script>
let knownPeersKey = "";
function showStatus(msg, isError = false) {
const el = document.getElementById('status');
el.style.display = 'block';
el.style.background = isError ? 'rgba(239, 68, 68, 0.2)' : 'rgba(16, 185, 129, 0.2)';
el.style.border = `1px solid ${isError ? '#ef4444' : '#10b981'}`;
el.textContent = msg;
setTimeout(() => { el.style.display = 'none'; }, 4000);
}
async function fetchState() {
try {
const res = await fetch('/ui/state');
const data = await res.json();
document.getElementById('my-name').textContent = `Устройство: ${data.device_name}`;
document.getElementById('my-ip').textContent = `HTTPS P2P: ${data.my_ip}:${data.p2p_port} | SHA-256: ${data.my_fingerprint.slice(0, 24)}...`;
document.getElementById('my-pin').textContent = data.my_pin;
const peersContainer = document.getElementById('peers-list');
if (data.peers.length === 0) {
peersContainer.innerHTML = '<div style="color:var(--muted); font-size:0.9rem;">Устройства в локальной сети пока не обнаружены</div>';
} else {
peersContainer.innerHTML = data.peers.map(p => `
<div class="peer-item">
<div>
<div style="font-weight:600;">${escapeHtml(p.name)} <span style="color:var(--muted); font-size:0.85rem;">(${p.ip}:${p.port})</span></div>
<div style="font-size:0.8rem; color:var(--muted); font-family:monospace;">PIN: <b style="color:#fff;">[${p.pin_code}]</b> | Статус: ${p.verified ? '<span style="color:#34d399;">Заверен</span>' : '<span style="color:#f87171;">Не заверен</span>'}</div>
</div>
<button class="btn-sm ${p.verified ? 'btn-red' : 'btn-green'}" onclick="toggleTrust('${p.ip}', ${!p.verified})">
${p.verified ? 'Отозвать' : 'Заверить'}
</button>
</div>
`).join('');
}
const currentSelectKey = data.peers.map(p => `${p.ip}:${p.verified}`).join('|');
if (currentSelectKey !== knownPeersKey) {
knownPeersKey = currentSelectKey;
const select = document.getElementById('target-peer');
const prevVal = select.value;
select.innerHTML = '<option value="">-- Выберите устройство --</option>' +
data.peers.map(p => `<option value="${p.ip}">${escapeHtml(p.name)} (${p.ip}) ${p.verified ? '✓' : '🔒 [Не заверен]'}</option>`).join('');
if (prevVal) select.value = prevVal;
}
const textsContainer = document.getElementById('ram-texts');
if (data.texts.length === 0) {
textsContainer.innerHTML = '<div style="color:var(--muted); font-size:0.9rem;">Буфер ОЗУ пуст</div>';
} else {
textsContainer.innerHTML = data.texts.map(t => `
<div class="msg-item">
<div class="msg-meta">
<span>От: <b>${escapeHtml(t.sender)}</b></span>
<button class="btn-sm" onclick="copyText(${JSON.stringify(t.text).replace(/"/g, '&quot;')})">Скопировать</button>
</div>
<div class="msg-text">${escapeHtml(t.text)}</div>
</div>
`).join('');
}
const mediaContainer = document.getElementById('disk-media');
if (data.media.length === 0) {
mediaContainer.innerHTML = '<div style="color:var(--muted); font-size:0.9rem;">Нет сохраненных медиафайлов</div>';
} else {
mediaContainer.innerHTML = data.media.map(m => {
const isImg = m.mime.startsWith('image/');
const fileUrl = `/ui/files/${encodeURIComponent(m.filename)}`;
return `
<div class="msg-item">
<div class="msg-meta">
<span>От: <b>${escapeHtml(m.sender)}</b></span>
<span>${(m.size / 1024).toFixed(1)} КБ</span>
</div>
<div style="font-weight:600; margin:2px 0;">
<a href="${fileUrl}" target="_blank" style="color:#60a5fa; text-decoration:none;">${escapeHtml(m.filename)} ↗</a>
</div>
<div class="path-box">Диск: ${escapeHtml(m.saved_path)}</div>
${isImg ? `<img src="${fileUrl}" class="preview-img">` : ''}
</div>
`;
}).join('');
}
} catch (e) {}
}
async function toggleTrust(ip, trust) {
const res = await fetch('/ui/trust', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ target_ip: ip, trust })
});
if (res.ok) {
showStatus(trust ? `Сертификат узла ${ip} заверен в ОЗУ` : `Доверие узлу ${ip} отозвано`);
fetchState();
}
}
async function sendText() {
const target_ip = document.getElementById('target-peer').value;
const input = document.getElementById('text-input');
const text = input.value.trim();
if (!target_ip) return showStatus('Выберите устройство-получатель!', true);
if (!text) return showStatus('Введите текст для отправки!', true);
const res = await fetch('/ui/send-text', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ target_ip, text })
});
if (res.ok) {
input.value = '';
showStatus(`Текст доставлен в ОЗУ узла ${target_ip}`);
} else {
const err = await res.text();
showStatus(err, true);
}
}
async function sendMedia() {
const target_ip = document.getElementById('target-peer').value;
const fileInput = document.getElementById('media-input');
if (!target_ip) return showStatus('Выберите устройство-получатель!', true);
if (!fileInput.files.length) return showStatus('Выберите медиафайл!', true);
const file = fileInput.files[0];
showStatus(`Отправка файла "${file.name}"...`);
const res = await fetch('/ui/send-media', {
method: 'POST',
headers: {
'X-Target-Ip': target_ip,
'X-Filename': encodeURIComponent(file.name),
'Content-Type': file.type || 'application/octet-stream'
},
body: file
});
if (res.ok) {
fileInput.value = '';
showStatus(`Файл "${file.name}" сохранен на диске узла ${target_ip}`);
} else {
const err = await res.text();
showStatus(err, true);
}
}
async function clearRam() {
await fetch('/ui/clear-ram', { method: 'POST' });
fetchState();
showStatus('Буфер текстовых сообщений в ОЗУ очищен');
}
function copyText(text) {
navigator.clipboard.writeText(text);
showStatus('Текст скопирован в буфер обмена');
}
function escapeHtml(str) {
return String(str).replace(/[&<>"']/g, s => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[s]));
}
document.getElementById('text-input').addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendText();
}
});
setInterval(fetchState, 1500);
fetchState();
</script>
</body>
</html>