before claude

This commit is contained in:
Gilles Soulier
2026-01-18 06:26:17 +01:00
parent dc19315e5d
commit 740c3d7516
60 changed files with 3815 additions and 354 deletions

View File

@@ -76,6 +76,81 @@ def _serialize_decimal(value):
return value
def fetch_product_history(product_id: int) -> Tuple[List[Dict[str, Any]], Optional[str]]:
"""Récupère l'historique complet des scraps pour un produit."""
rows: List[Dict[str, Any]] = []
try:
with get_db_connection() as conn:
with conn.cursor(cursor_factory=RealDictCursor) as cur:
cur.execute(
"""
SELECT
ph.id,
ph.price,
ph.shipping_cost,
ph.stock_status,
ph.fetch_method,
ph.fetch_status,
ph.fetched_at
FROM price_history ph
WHERE ph.product_id = %s
ORDER BY ph.fetched_at DESC
""",
(product_id,),
)
fetched = cur.fetchall()
for item in fetched:
serialized = {key: _serialize_decimal(value) for key, value in item.items()}
if serialized.get("fetched_at"):
serialized["fetched_at"] = serialized["fetched_at"].strftime(
"%Y-%m-%d %H:%M:%S"
)
rows.append(serialized)
return rows, None
except Exception as exc:
return rows, str(exc)
def fetch_all_price_history(limit: int = 500) -> Tuple[List[Dict[str, Any]], Optional[str]]:
"""Récupère toutes les entrées de price_history avec infos produit."""
rows: List[Dict[str, Any]] = []
try:
with get_db_connection() as conn:
with conn.cursor(cursor_factory=RealDictCursor) as cur:
cur.execute(
"""
SELECT
ph.id,
ph.product_id,
p.source,
p.reference,
p.title,
ph.price,
ph.shipping_cost,
ph.stock_status,
ph.fetch_method,
ph.fetch_status,
ph.fetched_at
FROM price_history ph
LEFT JOIN products p ON p.id = ph.product_id
ORDER BY ph.fetched_at DESC
LIMIT %s
""",
(limit,),
)
fetched = cur.fetchall()
for item in fetched:
serialized = {key: _serialize_decimal(value) for key, value in item.items()}
if serialized.get("fetched_at"):
serialized["fetched_at"] = serialized["fetched_at"].strftime(
"%Y-%m-%d %H:%M:%S"
)
rows.append(serialized)
return rows, None
except Exception as exc:
return rows, str(exc)
def fetch_products_list(limit: int = 200) -> Tuple[List[Dict[str, Any]], Optional[str]]:
rows: List[Dict[str, Any]] = []
try:
@@ -260,6 +335,68 @@ TEMPLATE = """
</dl>
</div>
</section>
<section>
<h2>Historique complet des scraps</h2>
<div class="browser-panel">
<div class="browser-controls">
<button id="load-history">Charger l'historique du produit sélectionné</button>
<span class="muted" id="history-message"></span>
</div>
<div class="history-table-container" style="max-height: 400px; overflow-y: auto; margin-top: 12px;">
<table id="history-table">
<thead>
<tr>
<th>Date</th>
<th>Prix</th>
<th>Frais port</th>
<th>Stock</th>
<th>Méthode</th>
<th>Statut</th>
</tr>
</thead>
<tbody id="history-body">
<tr><td colspan="6" class="muted">Sélectionnez un produit puis cliquez sur "Charger l'historique"</td></tr>
</tbody>
</table>
</div>
</div>
</section>
<section>
<h2>Parcourir la table price_history</h2>
<div class="browser-panel">
<div class="browser-controls">
<button id="load-price-history">Charger price_history</button>
<button id="ph-prev" disabled>Précédent</button>
<button id="ph-next" disabled>Suivant</button>
<strong class="browser-indicator" id="ph-indicator">0 / 0</strong>
<span class="muted" id="ph-message"></span>
</div>
<dl class="browser-display" id="ph-details">
<dt>ID</dt>
<dd id="ph-id">-</dd>
<dt>Product ID</dt>
<dd id="ph-product-id">-</dd>
<dt>Store</dt>
<dd id="ph-source">-</dd>
<dt>Référence</dt>
<dd id="ph-reference">-</dd>
<dt>Titre produit</dt>
<dd id="ph-title">-</dd>
<dt>Prix</dt>
<dd id="ph-price">-</dd>
<dt>Frais de port</dt>
<dd id="ph-shipping">-</dd>
<dt>Stock</dt>
<dd id="ph-stock">-</dd>
<dt>Méthode</dt>
<dd id="ph-method">-</dd>
<dt>Statut</dt>
<dd id="ph-status">-</dd>
<dt>Date scraping</dt>
<dd id="ph-fetched-at">-</dd>
</dl>
</div>
</section>
</main>
<script>
document.addEventListener("DOMContentLoaded", () => {
@@ -348,6 +485,177 @@ TEMPLATE = """
renderProduct();
}
});
// Historique des scraps
const loadHistoryBtn = document.getElementById("load-history");
const historyMessage = document.getElementById("history-message");
const historyBody = document.getElementById("history-body");
const setHistoryStatus = (text) => {
historyMessage.textContent = text || "";
};
const formatStock = (status) => {
const stockMap = {
"in_stock": "✓ En stock",
"out_of_stock": "✗ Rupture",
"limited": "⚠ Limité",
"preorder": "⏳ Précommande",
"unknown": "? Inconnu"
};
return stockMap[status] || status || "-";
};
const formatMethod = (method) => {
return method === "playwright" ? "🎭 Playwright" : "📡 HTTP";
};
const formatStatus = (status) => {
const statusMap = {
"success": "✓ Succès",
"partial": "⚠ Partiel",
"failed": "✗ Échec"
};
return statusMap[status] || status || "-";
};
const renderHistory = (history) => {
if (!history.length) {
historyBody.innerHTML = '<tr><td colspan="6" class="muted">Aucun historique disponible pour ce produit.</td></tr>';
return;
}
historyBody.innerHTML = history.map(entry => `
<tr>
<td>${entry.fetched_at || "-"}</td>
<td>${entry.price !== null ? entry.price + "" : "-"}</td>
<td>${entry.shipping_cost !== null ? entry.shipping_cost + "" : "-"}</td>
<td>${formatStock(entry.stock_status)}</td>
<td>${formatMethod(entry.fetch_method)}</td>
<td>${formatStatus(entry.fetch_status)}</td>
</tr>
`).join("");
};
const fetchHistory = async () => {
if (!products.length) {
setHistoryStatus("Chargez d'abord les produits.");
return;
}
const current = products[cursor];
if (!current || !current.id) {
setHistoryStatus("Aucun produit sélectionné.");
return;
}
setHistoryStatus(`Chargement de l'historique pour le produit #${current.id}…`);
try {
const response = await fetch(`/product/${current.id}/history.json`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
if (!Array.isArray(data)) {
throw new Error("Réponse invalide");
}
setHistoryStatus(`${data.length} entrée(s) pour "${(current.title || "Sans titre").slice(0, 30)}…"`);
renderHistory(data);
} catch (err) {
setHistoryStatus(`Erreur: ${err.message}`);
historyBody.innerHTML = '<tr><td colspan="6" class="muted">Erreur lors du chargement.</td></tr>';
}
};
loadHistoryBtn.addEventListener("click", fetchHistory);
// Parcourir price_history
const loadPhBtn = document.getElementById("load-price-history");
const phPrevBtn = document.getElementById("ph-prev");
const phNextBtn = document.getElementById("ph-next");
const phIndicator = document.getElementById("ph-indicator");
const phMessage = document.getElementById("ph-message");
let priceHistoryData = [];
let phCursor = 0;
const setPhStatus = (text) => {
phMessage.textContent = text || "";
};
const renderPriceHistory = () => {
const els = {
id: document.getElementById("ph-id"),
productId: document.getElementById("ph-product-id"),
source: document.getElementById("ph-source"),
reference: document.getElementById("ph-reference"),
title: document.getElementById("ph-title"),
price: document.getElementById("ph-price"),
shipping: document.getElementById("ph-shipping"),
stock: document.getElementById("ph-stock"),
method: document.getElementById("ph-method"),
status: document.getElementById("ph-status"),
fetchedAt: document.getElementById("ph-fetched-at"),
};
if (!priceHistoryData.length) {
phIndicator.textContent = "0 / 0";
Object.values(els).forEach((el) => (el.textContent = "-"));
phPrevBtn.disabled = true;
phNextBtn.disabled = true;
return;
}
const current = priceHistoryData[phCursor];
phIndicator.textContent = `${phCursor + 1} / ${priceHistoryData.length}`;
els.id.textContent = current.id || "-";
els.productId.textContent = current.product_id || "-";
els.source.textContent = current.source || "-";
els.reference.textContent = current.reference || "-";
els.title.textContent = current.title ? (current.title.length > 60 ? current.title.slice(0, 60) + "" : current.title) : "-";
els.price.textContent = current.price !== null ? current.price + "" : "-";
els.shipping.textContent = current.shipping_cost !== null ? current.shipping_cost + "" : "-";
els.stock.textContent = formatStock(current.stock_status);
els.method.textContent = formatMethod(current.fetch_method);
els.status.textContent = formatStatus(current.fetch_status);
els.fetchedAt.textContent = current.fetched_at || "-";
phPrevBtn.disabled = phCursor === 0;
phNextBtn.disabled = phCursor >= priceHistoryData.length - 1;
};
const fetchPriceHistory = async () => {
setPhStatus("Chargement…");
try {
const response = await fetch("/price_history.json");
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
if (!Array.isArray(data)) {
throw new Error("Réponse invalide");
}
priceHistoryData = data;
phCursor = 0;
setPhStatus(`Chargé ${priceHistoryData.length} entrée(s)`);
renderPriceHistory();
} catch (err) {
setPhStatus(`Erreur: ${err.message}`);
priceHistoryData = [];
renderPriceHistory();
}
};
loadPhBtn.addEventListener("click", fetchPriceHistory);
phPrevBtn.addEventListener("click", () => {
if (phCursor > 0) {
phCursor -= 1;
renderPriceHistory();
}
});
phNextBtn.addEventListener("click", () => {
if (phCursor + 1 < priceHistoryData.length) {
phCursor += 1;
renderPriceHistory();
}
});
});
</script>
</body>
@@ -377,5 +685,21 @@ def products_json():
return jsonify(products)
@app.route("/product/<int:product_id>/history.json")
def product_history_json(product_id: int):
history, error = fetch_product_history(product_id)
if error:
return jsonify({"error": error}), 500
return jsonify(history)
@app.route("/price_history.json")
def all_price_history_json():
history, error = fetch_all_price_history()
if error:
return jsonify({"error": error}), 500
return jsonify(history)
if __name__ == "__main__":
app.run(host="0.0.0.0", port=80)