Files
1c-lic-decoder-web/web/public/index.html
2026-05-25 14:16:59 +03:00

453 lines
15 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Lic Decoder</title>
<style>
:root {
--bg: #f5f5f5;
--surface: #fff;
--border: #ddd;
--fg: #222;
--fg2: #555;
--fg3: #888;
--accent: #2a6cb8;
--red: #c0392b;
--green: #27864a;
--radius: 6px;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: var(--bg);
color: var(--fg);
font-size: 14px;
line-height: 1.6;
}
.layout {
max-width: 720px;
margin: 0 auto;
padding: 32px 20px;
}
h1 {
font-size: 1.25rem;
font-weight: 700;
margin-bottom: 20px;
}
h2 {
font-size: 1rem;
font-weight: 600;
margin: 24px 0 8px;
color: var(--fg);
}
p, li {
color: var(--fg2);
font-size: 0.9rem;
}
ul { margin: 6px 0 0 20px; }
li { margin-bottom: 4px; }
.card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 16px;
margin-bottom: 16px;
}
.path-box {
font-family: "Cascadia Code", "Fira Code", Consolas, monospace;
font-size: 0.85rem;
background: #f0f0f0;
border: 1px solid var(--border);
border-radius: 4px;
padding: 8px 12px;
color: var(--fg);
display: inline-block;
margin: 6px 0;
}
.status-bar {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.82rem;
color: var(--fg3);
margin-bottom: 16px;
}
.status-dot {
width: 7px; height: 7px;
border-radius: 50%;
background: var(--fg3);
}
.status-dot.ok { background: var(--green); }
.status-dot.fail { background: var(--red); }
.upload-zone {
border: 2px dashed var(--border);
border-radius: var(--radius);
padding: 32px;
text-align: center;
cursor: pointer;
transition: border-color 0.15s;
}
.upload-zone:hover, .upload-zone.dragover {
border-color: var(--accent);
}
.upload-zone p {
color: var(--fg3);
font-size: 0.85rem;
margin-top: 6px;
}
.upload-zone strong { color: var(--fg2); }
#file-input { display: none; }
.options {
margin-top: 12px;
display: flex;
align-items: center;
gap: 6px;
}
.options label {
font-size: 0.85rem;
color: var(--fg2);
cursor: pointer;
}
.options input[type="checkbox"] {
accent-color: var(--accent);
width: 15px; height: 15px;
}
.btn {
margin-top: 12px;
padding: 8px 20px;
background: var(--accent);
color: #fff;
border: none;
border-radius: var(--radius);
font-size: 0.88rem;
font-weight: 500;
cursor: pointer;
}
.btn:hover { opacity: 0.9; }
.btn:disabled { opacity: 0.4; cursor: not-allowed; }
.btn.loading { background: var(--fg3); }
.file-name {
margin-left: 10px;
font-size: 0.85rem;
color: var(--fg3);
}
.error-box {
background: rgba(192,57,43,0.06);
border: 1px solid rgba(192,57,43,0.2);
border-radius: var(--radius);
padding: 12px;
color: var(--red);
font-size: 0.85rem;
margin-top: 12px;
display: none;
}
.error-box.visible { display: block; }
.results {
margin-top: 16px;
display: none;
}
.results.visible { display: block; }
.result-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
margin-bottom: 12px;
}
.result-card-header {
padding: 8px 12px;
border-bottom: 1px solid var(--border);
font-size: 0.8rem;
font-weight: 600;
color: var(--fg2);
text-transform: uppercase;
letter-spacing: 0.03em;
}
.result-card-body {
padding: 12px;
white-space: pre-wrap;
font-family: "Cascadia Code", "Fira Code", Consolas, monospace;
font-size: 0.82rem;
line-height: 1.5;
overflow-x: auto;
color: var(--fg);
}
.result-card-body.secondary { color: var(--fg2); }
.key-row {
display: flex;
align-items: center;
gap: 10px;
padding: 6px 0;
}
.key-label {
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--fg3);
min-width: 140px;
}
.key-badge {
display: inline-block;
padding: 3px 10px;
background: rgba(42,108,184,0.08);
border-radius: 4px;
font-family: monospace;
font-size: 0.85rem;
color: var(--accent);
letter-spacing: 1px;
}
.keys-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
margin-bottom: 12px;
padding: 12px 16px;
}
.hw-grid {
display: grid;
grid-template-columns: 1fr 1fr;
border-top: 1px solid var(--border);
}
.hw-grid .result-card {
margin: 0;
border-radius: 0;
border: none;
border-left: 1px solid var(--border);
}
.hw-grid .result-card:first-child { border-left: none; }
@media (max-width: 600px) {
.hw-grid { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<div class="layout">
<h1>Lic Decoder</h1>
<div class="status-bar" id="status-bar">
<div class="status-dot" id="status-dot"></div>
<span id="status-text">Проверка...</span>
</div>
<div class="card">
<h2>Описание</h2>
<p>Утилита для извлечения регистрационных данных из файлов лицензий 1С:Предприятие (.lic). Позволяет восстановить информацию о лицензии (компания, ФИО, почта, адрес, PIN-код), если файл LicData.txt был утерян.</p>
<h2>Источник файлов</h2>
<p>Файлы лицензий 1С хранятся в:</p>
<div class="path-box">C:\ProgramData\1C\licenses</div>
<p>Скопируйте нужный <strong>.lic</strong> файл и загрузите ниже.</p>
<h2>Что показывает</h2>
<ul>
<li>ORGANIZATION - Организация</li>
<li>FIRST NAME / MIDDLE NAME / LAST NAME - ФИО владельца</li>
<li>EMAIL - Электронная почта</li>
<li>COUNTRY / REGION / CITY / STREET - Адрес регистрации</li>
<li>PIN-CODE - PIN-код активации</li>
</ul>
<p style="margin-top:6px">При включении подробного режима также отображается аппаратная конфигурация лицензированной и текущей машины.</p>
</div>
<div class="upload-zone" id="drop-zone">
<strong>Перетащите .lic файл</strong>
<p>или нажмите для выбора</p>
<input type="file" id="file-input" accept=".lic">
</div>
<div class="options">
<input type="checkbox" id="detailed">
<label for="detailed">Подробный режим (аппаратная конфигурация)</label>
</div>
<div>
<button class="btn" id="decode-btn" disabled>Декодировать</button>
<span class="file-name" id="file-name-display"></span>
</div>
<div id="error-box" class="error-box"></div>
<div id="results" class="results">
<div class="keys-card" id="keys-card" style="display:none">
<div class="key-row" id="pin-row">
<span class="key-label">PIN-код</span>
<span class="key-badge" id="pin-value"></span>
</div>
<div class="key-row" id="reg-row" style="display:none">
<span class="key-label">Рег. Номер Комплекта</span>
<span class="key-badge" id="reg-value"></span>
</div>
</div>
<div class="result-card">
<div class="result-card-header">Данные лицензии</div>
<div class="result-card-body" id="lic-data"></div>
</div>
<div class="result-card">
<div class="result-card-header">Валидация</div>
<div class="result-card-body" id="validate-data"></div>
</div>
<div class="result-card" id="hw-card" style="display:none">
<div class="result-card-header">Аппаратная конфигурация</div>
<div class="result-card-body" style="padding:0">
<div class="hw-grid">
<div class="result-card">
<div class="result-card-header">Лицензированная машина</div>
<div class="result-card-body secondary" id="lic-hw"></div>
</div>
<div class="result-card">
<div class="result-card-header">Текущая машина</div>
<div class="result-card-body secondary" id="cur-hw"></div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
const dropZone = document.getElementById("drop-zone");
const fileInput = document.getElementById("file-input");
const decodeBtn = document.getElementById("decode-btn");
const detailedCb = document.getElementById("detailed");
const resultsDiv = document.getElementById("results");
const errorBox = document.getElementById("error-box");
const statusDot = document.getElementById("status-dot");
const statusText = document.getElementById("status-text");
const fileNameDisplay = document.getElementById("file-name-display");
let selectedFile = null;
async function checkStatus() {
try {
const res = await fetch("/api/status");
const data = await res.json();
let ready = true;
const parts = [];
if (data.jre.installed) {
if (!data.jre.valid) ready = false;
parts.push(data.jre.valid ? `JRE ${data.jre.version}` : `JRE ${data.jre.version} (устаревшая)`);
} else {
ready = false;
parts.push("JRE не найдена");
}
if (data.ring.installed) {
if (!data.ring.valid) ready = false;
parts.push(data.ring.valid ? `Ring ${data.ring.version}` : `Ring ${data.ring.version} (устаревший)`);
} else {
ready = false;
parts.push("Ring не найден");
}
statusText.textContent = parts.join(" \u00b7 ");
statusDot.className = "status-dot " + (ready ? "ok" : "fail");
} catch {
statusText.textContent = "Сервер недоступен";
statusDot.className = "status-dot fail";
}
}
dropZone.addEventListener("click", () => fileInput.click());
dropZone.addEventListener("dragover", (e) => { e.preventDefault(); dropZone.classList.add("dragover"); });
dropZone.addEventListener("dragleave", () => dropZone.classList.remove("dragover"));
dropZone.addEventListener("drop", (e) => {
e.preventDefault();
dropZone.classList.remove("dragover");
if (e.dataTransfer.files.length) handleFile(e.dataTransfer.files[0]);
});
fileInput.addEventListener("change", () => {
if (fileInput.files.length) handleFile(fileInput.files[0]);
});
function handleFile(file) {
if (!file.name.toLowerCase().endsWith(".lic")) {
showError("Поддерживаются только файлы .lic");
return;
}
selectedFile = file;
dropZone.querySelector("strong").textContent = file.name;
dropZone.querySelector("p").textContent = "Нажмите, чтобы выбрать другой";
fileNameDisplay.textContent = file.name;
decodeBtn.disabled = false;
resultsDiv.classList.remove("visible");
errorBox.classList.remove("visible");
}
decodeBtn.addEventListener("click", async () => {
if (!selectedFile) return;
decodeBtn.disabled = true;
decodeBtn.textContent = "Декодирование...";
decodeBtn.classList.add("loading");
resultsDiv.classList.remove("visible");
errorBox.classList.remove("visible");
const fd = new FormData();
fd.append("license", selectedFile);
fd.append("detailed", detailedCb.checked);
try {
const res = await fetch("/api/decode", { method: "POST", body: fd });
const data = await res.json();
if (res.status === 429) {
const retry = data.retryAfter ? ` Попробуйте через ${data.retryAfter}с.` : "";
throw new Error("Лимит превышен: не более 3 декодирований в час." + retry);
}
if (!res.ok) throw new Error(data.error || data.details || "Декодирование не удалось");
let licText = data.licData;
let regNumber = null;
const regMatch = licText.match(/РегНомерКомплекта:\s*(.+)/);
if (regMatch) {
regNumber = regMatch[1].trim();
licText = licText.replace(/РегНомерКомплекта:\s*.+\n?/, '');
}
document.getElementById("lic-data").textContent = licText.trim();
document.getElementById("pin-value").textContent = data.pinCode;
document.getElementById("keys-card").style.display = "";
if (regNumber) {
document.getElementById("reg-value").textContent = regNumber;
document.getElementById("reg-row").style.display = "";
} else {
document.getElementById("reg-row").style.display = "none";
}
document.getElementById("validate-data").textContent = data.validateData;
const hwCard = document.getElementById("hw-card");
if (data.licHWConfig || data.currentHWConfig) {
hwCard.style.display = "";
document.getElementById("lic-hw").textContent = data.licHWConfig || "N/A";
document.getElementById("cur-hw").textContent = data.currentHWConfig || "N/A";
} else {
hwCard.style.display = "none";
}
resultsDiv.classList.add("visible");
} catch (e) {
showError(e.message);
} finally {
decodeBtn.disabled = false;
decodeBtn.textContent = "Декодировать";
decodeBtn.classList.remove("loading");
}
});
function escapeHtml(text) {
const d = document.createElement('div');
d.textContent = text;
return d.innerHTML;
}
function showError(msg) {
errorBox.textContent = msg;
errorBox.classList.add("visible");
}
checkStatus();
</script>
</body>
</html>