- 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>
265 lines
8.3 KiB
Python
265 lines
8.3 KiB
Python
import json
|
|
import logging
|
|
from datetime import datetime
|
|
from ..database import get_sqlite, get_sqlite_sync
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
async def create_sync_run(run_id: str, json_files: int = 0):
|
|
"""Create a new sync run record."""
|
|
db = await get_sqlite()
|
|
try:
|
|
await db.execute("""
|
|
INSERT INTO sync_runs (run_id, started_at, status, json_files)
|
|
VALUES (?, datetime('now'), 'running', ?)
|
|
""", (run_id, json_files))
|
|
await db.commit()
|
|
finally:
|
|
await db.close()
|
|
|
|
|
|
async def update_sync_run(run_id: str, status: str, total_orders: int = 0,
|
|
imported: int = 0, skipped: int = 0, errors: int = 0,
|
|
error_message: str = None):
|
|
"""Update sync run with results."""
|
|
db = await get_sqlite()
|
|
try:
|
|
await db.execute("""
|
|
UPDATE sync_runs SET
|
|
finished_at = datetime('now'),
|
|
status = ?,
|
|
total_orders = ?,
|
|
imported = ?,
|
|
skipped = ?,
|
|
errors = ?,
|
|
error_message = ?
|
|
WHERE run_id = ?
|
|
""", (status, total_orders, imported, skipped, errors, error_message, run_id))
|
|
await db.commit()
|
|
finally:
|
|
await db.close()
|
|
|
|
|
|
async def add_import_order(sync_run_id: str, order_number: str, order_date: str,
|
|
customer_name: str, status: str, id_comanda: int = None,
|
|
id_partener: int = None, error_message: str = None,
|
|
missing_skus: list = None, items_count: int = 0):
|
|
"""Record an individual order import result."""
|
|
db = await get_sqlite()
|
|
try:
|
|
await db.execute("""
|
|
INSERT INTO import_orders
|
|
(sync_run_id, order_number, order_date, customer_name, status,
|
|
id_comanda, id_partener, error_message, missing_skus, items_count)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
""", (sync_run_id, order_number, order_date, customer_name, status,
|
|
id_comanda, id_partener, error_message,
|
|
json.dumps(missing_skus) if missing_skus else None, items_count))
|
|
await db.commit()
|
|
finally:
|
|
await db.close()
|
|
|
|
|
|
async def track_missing_sku(sku: str, product_name: str = "",
|
|
order_count: int = 0, order_numbers: str = None,
|
|
customers: str = None):
|
|
"""Track a missing SKU with order context."""
|
|
db = await get_sqlite()
|
|
try:
|
|
await db.execute("""
|
|
INSERT OR IGNORE INTO missing_skus (sku, product_name)
|
|
VALUES (?, ?)
|
|
""", (sku, product_name))
|
|
# Update context columns (always update with latest data)
|
|
if order_count or order_numbers or customers:
|
|
await db.execute("""
|
|
UPDATE missing_skus SET
|
|
order_count = ?,
|
|
order_numbers = ?,
|
|
customers = ?
|
|
WHERE sku = ?
|
|
""", (order_count, order_numbers, customers, sku))
|
|
await db.commit()
|
|
finally:
|
|
await db.close()
|
|
|
|
|
|
async def resolve_missing_sku(sku: str):
|
|
"""Mark a missing SKU as resolved."""
|
|
db = await get_sqlite()
|
|
try:
|
|
await db.execute("""
|
|
UPDATE missing_skus SET resolved = 1, resolved_at = datetime('now')
|
|
WHERE sku = ?
|
|
""", (sku,))
|
|
await db.commit()
|
|
finally:
|
|
await db.close()
|
|
|
|
|
|
async def get_missing_skus_paginated(page: int = 1, per_page: int = 20, resolved: int = 0):
|
|
"""Get paginated missing SKUs."""
|
|
db = await get_sqlite()
|
|
try:
|
|
offset = (page - 1) * per_page
|
|
|
|
cursor = await db.execute(
|
|
"SELECT COUNT(*) FROM missing_skus WHERE resolved = ?", (resolved,)
|
|
)
|
|
total = (await cursor.fetchone())[0]
|
|
|
|
cursor = await db.execute("""
|
|
SELECT sku, product_name, first_seen, resolved, resolved_at,
|
|
order_count, order_numbers, customers
|
|
FROM missing_skus
|
|
WHERE resolved = ?
|
|
ORDER BY order_count DESC, first_seen DESC
|
|
LIMIT ? OFFSET ?
|
|
""", (resolved, per_page, offset))
|
|
rows = await cursor.fetchall()
|
|
|
|
return {
|
|
"missing_skus": [dict(row) for row in rows],
|
|
"total": total,
|
|
"page": page,
|
|
"per_page": per_page,
|
|
"pages": (total + per_page - 1) // per_page if total > 0 else 0
|
|
}
|
|
finally:
|
|
await db.close()
|
|
|
|
|
|
async def get_sync_runs(page: int = 1, per_page: int = 20):
|
|
"""Get paginated sync run history."""
|
|
db = await get_sqlite()
|
|
try:
|
|
offset = (page - 1) * per_page
|
|
|
|
cursor = await db.execute("SELECT COUNT(*) FROM sync_runs")
|
|
total = (await cursor.fetchone())[0]
|
|
|
|
cursor = await db.execute("""
|
|
SELECT * FROM sync_runs
|
|
ORDER BY started_at DESC
|
|
LIMIT ? OFFSET ?
|
|
""", (per_page, offset))
|
|
rows = await cursor.fetchall()
|
|
|
|
return {
|
|
"runs": [dict(row) for row in rows],
|
|
"total": total,
|
|
"page": page,
|
|
"pages": (total + per_page - 1) // per_page if total > 0 else 0
|
|
}
|
|
finally:
|
|
await db.close()
|
|
|
|
|
|
async def get_sync_run_detail(run_id: str):
|
|
"""Get details for a specific sync run including its orders."""
|
|
db = await get_sqlite()
|
|
try:
|
|
cursor = await db.execute(
|
|
"SELECT * FROM sync_runs WHERE run_id = ?", (run_id,)
|
|
)
|
|
run = await cursor.fetchone()
|
|
if not run:
|
|
return None
|
|
|
|
cursor = await db.execute("""
|
|
SELECT * FROM import_orders
|
|
WHERE sync_run_id = ?
|
|
ORDER BY created_at
|
|
""", (run_id,))
|
|
orders = await cursor.fetchall()
|
|
|
|
return {
|
|
"run": dict(run),
|
|
"orders": [dict(o) for o in orders]
|
|
}
|
|
finally:
|
|
await db.close()
|
|
|
|
|
|
async def get_dashboard_stats():
|
|
"""Get stats for the dashboard."""
|
|
db = await get_sqlite()
|
|
try:
|
|
# Total imported
|
|
cursor = await db.execute(
|
|
"SELECT COUNT(*) FROM import_orders WHERE status = 'IMPORTED'"
|
|
)
|
|
imported = (await cursor.fetchone())[0]
|
|
|
|
# Total skipped
|
|
cursor = await db.execute(
|
|
"SELECT COUNT(*) FROM import_orders WHERE status = 'SKIPPED'"
|
|
)
|
|
skipped = (await cursor.fetchone())[0]
|
|
|
|
# Total errors
|
|
cursor = await db.execute(
|
|
"SELECT COUNT(*) FROM import_orders WHERE status = 'ERROR'"
|
|
)
|
|
errors = (await cursor.fetchone())[0]
|
|
|
|
# Missing SKUs (unresolved)
|
|
cursor = await db.execute(
|
|
"SELECT COUNT(*) FROM missing_skus WHERE resolved = 0"
|
|
)
|
|
missing = (await cursor.fetchone())[0]
|
|
|
|
# Article stats from last sync
|
|
cursor = await db.execute("""
|
|
SELECT COUNT(DISTINCT sku) FROM missing_skus
|
|
""")
|
|
total_missing_skus = (await cursor.fetchone())[0]
|
|
|
|
cursor = await db.execute("""
|
|
SELECT COUNT(DISTINCT sku) FROM missing_skus WHERE resolved = 0
|
|
""")
|
|
unresolved_skus = (await cursor.fetchone())[0]
|
|
|
|
# Last sync run
|
|
cursor = await db.execute("""
|
|
SELECT * FROM sync_runs ORDER BY started_at DESC LIMIT 1
|
|
""")
|
|
last_run = await cursor.fetchone()
|
|
|
|
return {
|
|
"imported": imported,
|
|
"skipped": skipped,
|
|
"errors": errors,
|
|
"missing_skus": missing,
|
|
"total_tracked_skus": total_missing_skus,
|
|
"unresolved_skus": unresolved_skus,
|
|
"last_run": dict(last_run) if last_run else None
|
|
}
|
|
finally:
|
|
await db.close()
|
|
|
|
|
|
async def get_scheduler_config():
|
|
"""Get scheduler configuration from SQLite."""
|
|
db = await get_sqlite()
|
|
try:
|
|
cursor = await db.execute("SELECT key, value FROM scheduler_config")
|
|
rows = await cursor.fetchall()
|
|
return {row["key"]: row["value"] for row in rows}
|
|
finally:
|
|
await db.close()
|
|
|
|
|
|
async def set_scheduler_config(key: str, value: str):
|
|
"""Set a scheduler configuration value."""
|
|
db = await get_sqlite()
|
|
try:
|
|
await db.execute("""
|
|
INSERT OR REPLACE INTO scheduler_config (key, value)
|
|
VALUES (?, ?)
|
|
""", (key, value))
|
|
await db.commit()
|
|
finally:
|
|
await db.close()
|