Local-first runtime cleanup and recovery hardening
This commit is contained in:
@@ -1,82 +0,0 @@
|
||||
{{define "title"}}Вход - QuoteForge{{end}}
|
||||
|
||||
{{define "content"}}
|
||||
<div class="max-w-sm mx-auto mt-16">
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<h1 class="text-xl font-bold text-center mb-6">Вход в систему</h1>
|
||||
|
||||
<form id="login-form">
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Логин</label>
|
||||
<input type="text" name="username" id="username" required
|
||||
class="w-full px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
value="admin">
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Пароль</label>
|
||||
<input type="password" name="password" id="password" required
|
||||
class="w-full px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
value="admin123">
|
||||
</div>
|
||||
<div id="error" class="text-red-600 text-sm mb-4 hidden"></div>
|
||||
<button type="submit" id="submit-btn"
|
||||
class="w-full bg-blue-600 text-white py-2 rounded hover:bg-blue-700">
|
||||
Войти
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p class="text-center text-sm text-gray-500 mt-4">
|
||||
<a href="/" class="text-blue-600">← На главную</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const form = document.getElementById('login-form');
|
||||
if (!form) return;
|
||||
|
||||
form.addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const username = document.getElementById('username').value;
|
||||
const password = document.getElementById('password').value;
|
||||
const errorEl = document.getElementById('error');
|
||||
const btn = document.getElementById('submit-btn');
|
||||
|
||||
errorEl.classList.add('hidden');
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Вход...';
|
||||
|
||||
try {
|
||||
const resp = await fetch('/api/auth/login', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({username, password})
|
||||
});
|
||||
|
||||
const data = await resp.json();
|
||||
|
||||
if (resp.ok && data.access_token) {
|
||||
localStorage.setItem('token', data.access_token);
|
||||
localStorage.setItem('refresh_token', data.refresh_token);
|
||||
localStorage.setItem('user', JSON.stringify(data.user));
|
||||
window.location.href = '/configs';
|
||||
} else {
|
||||
errorEl.textContent = data.error || 'Неверный логин или пароль';
|
||||
errorEl.classList.remove('hidden');
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Войти';
|
||||
}
|
||||
} catch(err) {
|
||||
errorEl.textContent = 'Ошибка соединения с сервером';
|
||||
errorEl.classList.remove('hidden');
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Войти';
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{{end}}
|
||||
|
||||
{{template "base" .}}
|
||||
@@ -5,7 +5,7 @@
|
||||
<h1 class="text-2xl font-bold text-gray-900">Партномера</h1>
|
||||
|
||||
<!-- Summary cards -->
|
||||
<div id="summary-cards" class="grid grid-cols-2 md:grid-cols-4 gap-4 hidden">
|
||||
<div id="summary-cards" class="grid grid-cols-2 md:grid-cols-3 gap-4 hidden">
|
||||
<div class="bg-white rounded-lg shadow p-4">
|
||||
<div class="text-xs text-gray-500 mb-1">Активный лист</div>
|
||||
<div id="card-version" class="font-mono font-semibold text-gray-800 text-sm truncate">—</div>
|
||||
@@ -19,10 +19,6 @@
|
||||
<div class="text-xs text-gray-500 mb-1">Всего PN</div>
|
||||
<div id="card-pn-total" class="text-2xl font-bold text-gray-800">—</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg shadow p-4">
|
||||
<div class="text-xs text-gray-500 mb-1">Primary PN</div>
|
||||
<div id="card-pn-primary" class="text-2xl font-bold text-green-600">—</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="summary-empty" class="hidden bg-yellow-50 border border-yellow-200 rounded-lg p-4 text-sm text-yellow-800">
|
||||
@@ -84,7 +80,6 @@
|
||||
<tr>
|
||||
<th class="px-4 py-2 text-left">Partnumber</th>
|
||||
<th class="px-4 py-2 text-left">LOT</th>
|
||||
<th class="px-4 py-2 text-center w-24">Primary</th>
|
||||
<th class="px-4 py-2 text-left">Описание</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -225,7 +220,6 @@ async function loadActiveBookItemsPage(page = 1, search = '', book = null) {
|
||||
document.getElementById('card-date').textContent = targetBook.created_at;
|
||||
document.getElementById('card-lots').textContent = Number(data.lot_count || 0);
|
||||
document.getElementById('card-pn-total').textContent = Number(data.book_total || 0);
|
||||
document.getElementById('card-pn-primary').textContent = Number(data.primary_count || 0);
|
||||
document.getElementById('summary-cards').classList.remove('hidden');
|
||||
document.getElementById('active-book-section').classList.remove('hidden');
|
||||
document.getElementById('pn-search').value = itemsSearch;
|
||||
@@ -240,10 +234,11 @@ function renderItems(items) {
|
||||
items.forEach(item => {
|
||||
const tr = document.createElement('tr');
|
||||
tr.className = 'border-b hover:bg-gray-50';
|
||||
const lots = Array.isArray(item.lots_json) ? item.lots_json : [];
|
||||
const lotsText = lots.map(l => `${l.lot_name} x${l.qty}`).join(', ');
|
||||
tr.innerHTML = `
|
||||
<td class="px-4 py-1.5 font-mono text-xs">${item.partnumber}</td>
|
||||
<td class="px-4 py-1.5 text-xs font-medium text-blue-700">${item.lot_name}</td>
|
||||
<td class="px-4 py-1.5 text-center text-green-600 text-xs">${item.is_primary_pn ? '✓' : ''}</td>
|
||||
<td class="px-4 py-1.5 text-xs font-medium text-blue-700">${lotsText}</td>
|
||||
<td class="px-4 py-1.5 text-xs text-gray-500">${item.description || ''}</td>
|
||||
`;
|
||||
tbody.appendChild(tr);
|
||||
|
||||
Reference in New Issue
Block a user