Add Phase 2: Local SQLite database with sync functionality

Implements complete offline-first architecture with SQLite caching and MariaDB synchronization.

Key features:
- Local SQLite database for offline operation (data/quoteforge.db)
- Connection settings with encrypted credentials
- Component and pricelist caching with auto-sync
- Sync API endpoints (/api/sync/status, /components, /pricelists, /all)
- Real-time sync status indicator in UI with auto-refresh
- Offline mode detection middleware
- Migration tool for database initialization
- Setup wizard for initial configuration

New components:
- internal/localdb: SQLite repository layer (components, pricelists, sync)
- internal/services/sync: Synchronization service
- internal/handlers/sync: Sync API handlers
- internal/handlers/setup: Setup wizard handlers
- internal/middleware/offline: Offline detection
- cmd/migrate: Database migration tool

UI improvements:
- Setup page for database configuration
- Sync status indicator with online/offline detection
- Warning icons for pending synchronization
- Auto-refresh every 30 seconds

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-01 11:00:32 +03:00
parent 8b8d2f18f9
commit 143d217397
30 changed files with 3697 additions and 887 deletions

View File

@@ -269,9 +269,8 @@ async function loadCategoriesFromAPI() {
// Initialize
document.addEventListener('DOMContentLoaded', async function() {
const token = localStorage.getItem('token');
if (!token || !configUUID) {
// RBAC disabled - no token check required
if (!configUUID) {
window.location.href = '/configs';
return;
}
@@ -280,16 +279,9 @@ document.addEventListener('DOMContentLoaded', async function() {
await loadCategoriesFromAPI();
try {
const resp = await fetch('/api/configs/' + configUUID, {
headers: {'Authorization': 'Bearer ' + token}
});
const resp = await fetch('/api/configs/' + configUUID);
if (resp.status === 401) {
window.location.href = '/login';
return;
}
if (resp.status === 403 || resp.status === 404) {
if (resp.status === 404) {
showToast('Конфигурация не найдена', 'error');
window.location.href = '/configs';
return;
@@ -1119,8 +1111,8 @@ function triggerAutoSave() {
}
async function saveConfig(showNotification = true) {
const token = localStorage.getItem('token');
if (!token || !configUUID) return;
// RBAC disabled - no token check required
if (!configUUID) return;
// Get custom price if set
const customPriceInput = document.getElementById('custom-price-input');
@@ -1134,7 +1126,6 @@ async function saveConfig(showNotification = true) {
const resp = await fetch('/api/configs/' + configUUID, {
method: 'PUT',
headers: {
'Authorization': 'Bearer ' + token,
'Content-Type': 'application/json'
},
body: JSON.stringify({
@@ -1146,11 +1137,6 @@ async function saveConfig(showNotification = true) {
})
});
if (resp.status === 401) {
window.location.href = '/login';
return;
}
if (!resp.ok) {
if (showNotification) {
showToast('Ошибка сохранения', 'error');
@@ -1308,23 +1294,17 @@ async function exportCSVWithCustomPrice() {
}
async function refreshPrices() {
const token = localStorage.getItem('token');
if (!token || !configUUID) return;
// RBAC disabled - no token check required
if (!configUUID) return;
try {
const resp = await fetch('/api/configs/' + configUUID + '/refresh-prices', {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + token,
'Content-Type': 'application/json'
}
});
if (resp.status === 401) {
window.location.href = '/login';
return;
}
if (!resp.ok) {
showToast('Ошибка обновления цен', 'error');
return;