feat(sync): add SSE live feed, unified logs page, fix Oracle connection
- Add SSE event bus in sync_service (subscribe/unsubscribe/_emit) - Add GET /api/sync/stream SSE endpoint for real-time sync progress - Rewrite logs.html: unified runs table + live feed + summary + filters - Rewrite logs.js: SSE EventSource client, run selection, pagination - Dashboard: clickable runs navigate to /logs?run=, sync started banner - Remove "Import Comenzi" nav item, delete sync_detail.html - Add error_message column to sync_runs table with migration - Fix: export TNS_ADMIN as OS env var so oracledb finds tnsnames.ora - Fix: use get_oracle_connection() instead of direct pool.acquire() - Fix: CRM_POLITICI_PRET_ART INSERT to match actual table schema Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -20,11 +20,6 @@
|
||||
<i class="bi bi-speedometer2"></i> Dashboard
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% block nav_sync %}{% endblock %}" href="/sync">
|
||||
<i class="bi bi-arrow-repeat"></i> Import Comenzi
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% block nav_mappings %}{% endblock %}" href="/mappings">
|
||||
<i class="bi bi-link-45deg"></i> Mapari SKU
|
||||
|
||||
@@ -105,6 +105,11 @@
|
||||
<small class="text-muted" id="syncProgressText"></small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 d-none" id="syncStartedBanner">
|
||||
<div class="alert alert-info alert-sm py-1 px-2 mb-0 d-inline-block">
|
||||
<small><i class="bi bi-broadcast"></i> Sync pornit — <a href="#" id="syncRunLink">vezi progresul live</a></small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -15,85 +15,125 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filter buttons -->
|
||||
<div class="mb-3" id="filterRow" style="display:none;">
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<button type="button" class="btn btn-outline-secondary active" data-filter="all">
|
||||
<i class="bi bi-list-ul"></i> Toate
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-success" data-filter="IMPORTED">
|
||||
<i class="bi bi-check-circle"></i> Importate
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-warning" data-filter="SKIPPED">
|
||||
<i class="bi bi-skip-forward"></i> Fara Mapare
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-danger" data-filter="ERROR">
|
||||
<i class="bi bi-x-circle"></i> Erori
|
||||
</button>
|
||||
<!-- Sync Runs Table (always visible) -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<span>Sync Runs</span>
|
||||
<div id="runsTablePagination" class="d-flex align-items-center gap-2"></div>
|
||||
</div>
|
||||
<small class="text-muted ms-3" id="filterCount"></small>
|
||||
</div>
|
||||
|
||||
<!-- Run summary bar -->
|
||||
<div class="row g-3 mb-3" id="runSummary" style="display:none;">
|
||||
<div class="col-auto">
|
||||
<div class="card stat-card px-3 py-2">
|
||||
<div class="stat-value text-primary" id="sum-total" style="font-size:1.25rem;">-</div>
|
||||
<div class="stat-label">Total</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<div class="card stat-card px-3 py-2">
|
||||
<div class="stat-value text-success" id="sum-imported" style="font-size:1.25rem;">-</div>
|
||||
<div class="stat-label">Importate</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<div class="card stat-card px-3 py-2">
|
||||
<div class="stat-value text-warning" id="sum-skipped" style="font-size:1.25rem;">-</div>
|
||||
<div class="stat-label">Omise</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<div class="card stat-card px-3 py-2">
|
||||
<div class="stat-value text-danger" id="sum-errors" style="font-size:1.25rem;">-</div>
|
||||
<div class="stat-label">Erori</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<div class="card stat-card px-3 py-2">
|
||||
<div class="stat-value text-secondary" id="sum-duration" style="font-size:1.25rem;">-</div>
|
||||
<div class="stat-label">Durata</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Orders table -->
|
||||
<div class="card">
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0" id="logsTable">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:140px;">Nr. Comanda</th>
|
||||
<th>Client</th>
|
||||
<th style="width:100px;" class="text-center">Nr. Articole</th>
|
||||
<th style="width:120px;">Status</th>
|
||||
<th>Eroare / Detalii</th>
|
||||
<th>Data</th>
|
||||
<th>Status</th>
|
||||
<th>Total</th>
|
||||
<th>OK</th>
|
||||
<th>Fara mapare</th>
|
||||
<th>Erori</th>
|
||||
<th>Durata</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="logsBody">
|
||||
<tr id="emptyState">
|
||||
<td colspan="5" class="text-center text-muted py-5">
|
||||
<i class="bi bi-journal-text fs-2 d-block mb-2 text-muted opacity-50"></i>
|
||||
Selecteaza un sync run din lista de sus
|
||||
</td>
|
||||
</tr>
|
||||
<tbody id="runsTableBody">
|
||||
<tr><td colspan="7" class="text-center text-muted py-3">Se incarca...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Run Detail Section (shown when run selected or live sync) -->
|
||||
<div id="runDetailSection" style="display:none;">
|
||||
|
||||
<!-- Run Summary Bar -->
|
||||
<div class="row g-3 mb-3" id="runSummary">
|
||||
<div class="col-auto">
|
||||
<div class="card stat-card px-3 py-2">
|
||||
<div class="stat-value text-primary" id="sum-total" style="font-size:1.25rem;">-</div>
|
||||
<div class="stat-label">Total</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<div class="card stat-card px-3 py-2">
|
||||
<div class="stat-value text-success" id="sum-imported" style="font-size:1.25rem;">-</div>
|
||||
<div class="stat-label">Importate</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<div class="card stat-card px-3 py-2">
|
||||
<div class="stat-value text-warning" id="sum-skipped" style="font-size:1.25rem;">-</div>
|
||||
<div class="stat-label">Omise</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<div class="card stat-card px-3 py-2">
|
||||
<div class="stat-value text-danger" id="sum-errors" style="font-size:1.25rem;">-</div>
|
||||
<div class="stat-label">Erori</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<div class="card stat-card px-3 py-2">
|
||||
<div class="stat-value text-secondary" id="sum-duration" style="font-size:1.25rem;">-</div>
|
||||
<div class="stat-label">Durata</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Live Feed (visible only during active sync) -->
|
||||
<div class="card mb-3" id="liveFeedCard" style="display:none;">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-broadcast"></i> Live Feed
|
||||
<span class="badge bg-danger ms-2 live-pulse">LIVE</span>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="live-feed" id="liveFeed"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filter buttons -->
|
||||
<div class="mb-3" id="filterRow">
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<button type="button" class="btn btn-outline-secondary active" data-filter="all">
|
||||
<i class="bi bi-list-ul"></i> Toate
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-success" data-filter="IMPORTED">
|
||||
<i class="bi bi-check-circle"></i> Importate
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-warning" data-filter="SKIPPED">
|
||||
<i class="bi bi-skip-forward"></i> Fara Mapare
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-danger" data-filter="ERROR">
|
||||
<i class="bi bi-x-circle"></i> Erori
|
||||
</button>
|
||||
</div>
|
||||
<small class="text-muted ms-3" id="filterCount"></small>
|
||||
</div>
|
||||
|
||||
<!-- Orders table -->
|
||||
<div class="card">
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0" id="logsTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:140px;">Nr. Comanda</th>
|
||||
<th>Client</th>
|
||||
<th style="width:100px;" class="text-center">Nr. Articole</th>
|
||||
<th style="width:120px;">Status</th>
|
||||
<th>Eroare / Detalii</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="logsBody">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hidden field for pre-selected run from URL/server -->
|
||||
<input type="hidden" id="preselectedRun" value="{{ selected_run }}">
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
|
||||
@@ -1,158 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Sync Run - GoMag Import{% endblock %}
|
||||
{% block nav_sync %}active{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<a href="/" class="text-decoration-none text-muted"><i class="bi bi-arrow-left"></i> Dashboard</a>
|
||||
<h4 class="mb-0 mt-1">Sync Run <small class="text-muted" id="runId">{{ run_id }}</small></h4>
|
||||
</div>
|
||||
<span class="badge bg-secondary fs-6" id="runStatusBadge">-</span>
|
||||
</div>
|
||||
|
||||
<!-- Run summary -->
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-3">
|
||||
<div class="card stat-card">
|
||||
<div class="stat-value" id="runTotal">-</div>
|
||||
<div class="stat-label">Total Comenzi</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card stat-card">
|
||||
<div class="stat-value text-success" id="runImported">-</div>
|
||||
<div class="stat-label">Imported</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card stat-card">
|
||||
<div class="stat-value text-warning" id="runSkipped">-</div>
|
||||
<div class="stat-label">Skipped</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card stat-card">
|
||||
<div class="stat-value text-danger" id="runErrors">-</div>
|
||||
<div class="stat-label">Errors</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<small class="text-muted" id="runTiming"></small>
|
||||
</div>
|
||||
|
||||
<!-- Orders table -->
|
||||
<div class="card">
|
||||
<div class="card-header">Comenzi</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Nr Comanda</th>
|
||||
<th>Data</th>
|
||||
<th>Client</th>
|
||||
<th>Articole</th>
|
||||
<th>Status</th>
|
||||
<th>Detalii</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="ordersBody">
|
||||
<tr><td colspan="7" class="text-center text-muted py-4">Se incarca...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
const RUN_ID = '{{ run_id }}';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', loadRunDetail);
|
||||
|
||||
async function loadRunDetail() {
|
||||
try {
|
||||
const res = await fetch(`/api/sync/run/${RUN_ID}`);
|
||||
const data = await res.json();
|
||||
|
||||
if (data.error) {
|
||||
document.getElementById('ordersBody').innerHTML =
|
||||
`<tr><td colspan="7" class="text-center text-danger">${data.error}</td></tr>`;
|
||||
return;
|
||||
}
|
||||
|
||||
const run = data.run;
|
||||
|
||||
// Update summary
|
||||
document.getElementById('runTotal').textContent = run.total_orders || 0;
|
||||
document.getElementById('runImported').textContent = run.imported || 0;
|
||||
document.getElementById('runSkipped').textContent = run.skipped || 0;
|
||||
document.getElementById('runErrors').textContent = run.errors || 0;
|
||||
|
||||
const badge = document.getElementById('runStatusBadge');
|
||||
badge.textContent = run.status;
|
||||
badge.className = 'badge fs-6 ' + (run.status === 'completed' ? 'bg-success' : run.status === 'running' ? 'bg-primary' : 'bg-danger');
|
||||
|
||||
// Timing
|
||||
if (run.started_at) {
|
||||
let timing = 'Start: ' + new Date(run.started_at).toLocaleString('ro-RO');
|
||||
if (run.finished_at) {
|
||||
const sec = Math.round((new Date(run.finished_at) - new Date(run.started_at)) / 1000);
|
||||
timing += ` | Durata: ${sec < 60 ? sec + 's' : Math.floor(sec/60) + 'm ' + (sec%60) + 's'}`;
|
||||
}
|
||||
document.getElementById('runTiming').textContent = timing;
|
||||
}
|
||||
|
||||
// Orders table
|
||||
const orders = data.orders || [];
|
||||
if (orders.length === 0) {
|
||||
document.getElementById('ordersBody').innerHTML =
|
||||
'<tr><td colspan="7" class="text-center text-muted py-4">Nicio comanda</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('ordersBody').innerHTML = orders.map((o, i) => {
|
||||
const statusClass = o.status === 'IMPORTED' ? 'badge-imported' : o.status === 'SKIPPED' ? 'badge-skipped' : 'badge-error';
|
||||
|
||||
let details = '';
|
||||
if (o.status === 'IMPORTED' && o.id_comanda) {
|
||||
details = `<small class="text-success">ID: ${o.id_comanda}</small>`;
|
||||
} else if (o.status === 'SKIPPED' && o.missing_skus) {
|
||||
try {
|
||||
const skus = JSON.parse(o.missing_skus);
|
||||
details = `<small class="text-warning">SKU lipsa: ${skus.map(s => '<code>' + esc(s) + '</code>').join(', ')}</small>`;
|
||||
} catch(e) {
|
||||
details = `<small class="text-warning">${esc(o.missing_skus)}</small>`;
|
||||
}
|
||||
} else if (o.status === 'ERROR' && o.error_message) {
|
||||
details = `<small class="text-danger">${esc(o.error_message).substring(0, 100)}</small>`;
|
||||
}
|
||||
|
||||
return `<tr>
|
||||
<td>${i + 1}</td>
|
||||
<td><strong>${esc(o.order_number)}</strong></td>
|
||||
<td><small>${o.order_date ? o.order_date.substring(0, 10) : '-'}</small></td>
|
||||
<td>${esc(o.customer_name)}</td>
|
||||
<td>${o.items_count || '-'}</td>
|
||||
<td><span class="badge ${statusClass}">${o.status}</span></td>
|
||||
<td>${details}</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
|
||||
} catch (err) {
|
||||
document.getElementById('ordersBody').innerHTML =
|
||||
`<tr><td colspan="7" class="text-center text-danger">Eroare: ${err.message}</td></tr>`;
|
||||
}
|
||||
}
|
||||
|
||||
function esc(s) {
|
||||
if (s == null) return '';
|
||||
return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user