mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-11 17:26:32 +02:00
feat(api): métriques Qlik + gamme serveur à jour, et doc de connexion dans Paramètres
L'instantané grid_rows fige la ligne au moment du calcul, mais deux données évoluent indépendamment et doivent refléter l'état courant : - Métriques réseau Qlik : qlik_network_metrics est alimentée par les syncs et les recherches réseau, hors du calcul de grille. Relues à chaque appel et exposées dans `network` (null quand le produit n'en a pas). Les colonnes caReseau/qteReseau/... de la ligne sont réalignées dessus. - Gamme serveur : codeGamme peut être surchargée par un snapshot de session. L'API expose `codeGammeServeur`, la gamme NON modifiée telle qu'elle est en base PostgreSQL, relue à chaque appel. codeGammeInit est gardé aligné. Ajout de pgGetGammesByCodeins() : variante sans jointure artfou1, nécessaire car la recherche de l'API est transversale (pas de fournisseur connu). Ce n'est pas un recalcul : deux lectures indexées bornées à la page courante (500 lignes max). Mesuré à ~20 ms, soit le même coût que sans enrichissement. Paramètre enrich=0 pour servir l'instantané brut. Paramètres → nouvelle section « API CollectFlow — Connexion » : URL de base déduite de l'origine, en-têtes d'authentification, liste des endpoints et des paramètres, exemples curl copiables, lien vers openapi.json. Vérifié sur PostgreSQL local avec un instantané volontairement périmé : gamme snapshot A → serveur C, caReseau 111 → 990000, produit sans données réseau → network null, produit sans gamme → codeGammeServeur null, enrich=0 redonnant bien les valeurs figées, et toujours zéro recalcul. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y26nRZxTR57K7h8yqsF675
This commit is contained in:
9 files changed
+350
-20
No files matched your search
@@ -9,6 +9,7 @@ import { testDatabaseConnection, saveDatabaseSettings, getSavedDatabaseConfig, s
|
||||
import { useEffect } from "react";
|
||||
import { UserManagement } from "@/features/admin/components/user-management";
|
||||
import { ApiKeyManagement } from "@/features/admin/components/api-key-management";
|
||||
import { ApiConnectionInfo } from "@/features/admin/components/api-connection-info";
|
||||
|
||||
interface OpenRouterModel { id: string; name: string; free: boolean; }
|
||||
|
||||
@@ -617,10 +618,17 @@ export default function SettingsPage() {
|
||||
<UserManagement />
|
||||
</Section>
|
||||
|
||||
{/* Clés de l'API publique /api/v1 */}
|
||||
{/* API publique /api/v1 — connexion puis gestion des clés */}
|
||||
<Section
|
||||
title="API CollectFlow — Connexion"
|
||||
subtitle="Tout ce qu'il faut pour appeler l'API depuis un script ou un outil externe"
|
||||
>
|
||||
<ApiConnectionInfo />
|
||||
</Section>
|
||||
|
||||
<Section
|
||||
title="Clés d'API"
|
||||
subtitle="Accès programmatique en lecture à la grille et à la recherche (/api/v1)"
|
||||
subtitle="Créer et révoquer les clés d'accès à /api/v1"
|
||||
>
|
||||
<ApiKeyManagement />
|
||||
</Section>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { requireApiAuth } from "@/lib/api-auth";
|
||||
import { ok, fail, buildPagination } from "@/lib/api-response";
|
||||
import { gridQuerySchema, pickFields, toSortKey } from "@/lib/api-schemas";
|
||||
import { queryGridRows, getGridFreshness } from "@/lib/grid-store";
|
||||
import { enrichRows } from "@/lib/api-enrich";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
export const dynamic = "force-dynamic";
|
||||
@@ -50,8 +51,11 @@ export async function GET(req: NextRequest) {
|
||||
limit: q.limit,
|
||||
});
|
||||
|
||||
// Métriques Qlik et gamme serveur relues au moment de l'appel (voir api-enrich).
|
||||
const rows = q.enrich === "1" ? await enrichRows(result.rows) : result.rows;
|
||||
|
||||
return ok(
|
||||
result.rows.map((r) => pickFields(r, q.fields)),
|
||||
rows.map((r) => pickFields(r, q.fields)),
|
||||
{
|
||||
pagination: buildPagination(q.page, q.limit, result.total),
|
||||
meta: {
|
||||
@@ -59,6 +63,7 @@ export async function GET(req: NextRequest) {
|
||||
computedAt: result.computedAt,
|
||||
snapshotComputedAt: freshness.computedAt,
|
||||
snapshotRowCount: freshness.rowCount,
|
||||
enrichi: q.enrich === "1",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
@@ -29,6 +29,14 @@ export async function GET(req: NextRequest) {
|
||||
schema: { type: "string" },
|
||||
description: "Champs de ProductRow à conserver, séparés par des virgules (allège fortement la réponse). `codein` est toujours inclus.",
|
||||
},
|
||||
{
|
||||
name: "enrich",
|
||||
in: "query",
|
||||
schema: { type: "string", enum: ["0", "1"], default: "1" },
|
||||
description:
|
||||
"1 (défaut) : relit les métriques réseau Qlik (`network`) et la gamme serveur (`codeGammeServeur`) au moment de l'appel. "
|
||||
+ "0 : sert l'instantané brut, légèrement plus rapide.",
|
||||
},
|
||||
];
|
||||
|
||||
const spec = {
|
||||
@@ -42,7 +50,10 @@ export async function GET(req: NextRequest) {
|
||||
+ "**Aucun endpoint ne déclenche de recalcul ni d'appel à Qlik.** Toutes les réponses proviennent "
|
||||
+ "de données déjà persistées : l'instantané de grille (`grid_rows`), rempli quand la Grille est "
|
||||
+ "ouverte dans l'application, et le cache des métriques réseau (`qlik_network_metrics`). "
|
||||
+ "Un fournisseur jamais ouvert renvoie donc `202 not_ready` au lieu d'imposer une attente.",
|
||||
+ "Un fournisseur jamais ouvert renvoie donc `202 not_ready` au lieu d'imposer une attente.\n\n"
|
||||
+ "Deux informations sont relues à chaque appel car elles évoluent indépendamment du calcul de "
|
||||
+ "la grille : les **métriques réseau Qlik** (`network`, `null` quand il n'y en a pas) et la "
|
||||
+ "**gamme serveur non modifiée** (`codeGammeServeur`). Passer `enrich=0` pour s'en dispenser.",
|
||||
},
|
||||
servers: [{ url: "/api/v1" }],
|
||||
security: [{ ApiKeyAuth: [] }, { BearerAuth: [] }],
|
||||
@@ -87,7 +98,25 @@ export async function GET(req: NextRequest) {
|
||||
libelle1: { type: "string" },
|
||||
gtin: { type: "string" },
|
||||
codeCentrale: { type: "string", description: "Clé de jointure avec Qlik (format 10000XXXXXX)." },
|
||||
codeGamme: { type: ["string", "null"] },
|
||||
codeGamme: { type: ["string", "null"], description: "Gamme courante — peut être surchargée par un snapshot de session." },
|
||||
codeGammeServeur: {
|
||||
type: ["string", "null"],
|
||||
description: "Gamme **non modifiée**, telle qu'elle existe sur le serveur PostgreSQL. Relue à chaque appel (sauf `enrich=0`). `null` = aucune gamme en base.",
|
||||
},
|
||||
codeGammeInit: { type: ["string", "null"], description: "Alias historique de `codeGammeServeur`, maintenu aligné." },
|
||||
network: {
|
||||
type: ["object", "null"],
|
||||
description: "Métriques réseau Qlik en cache, `null` si le produit n'en a pas.",
|
||||
properties: {
|
||||
caReseau: { type: "number" },
|
||||
qteReseau: { type: "number" },
|
||||
nbMagasinsReseau: { type: "integer" },
|
||||
caParMagasinReseau: { type: "number" },
|
||||
margePctReseau: { type: "number", description: "Ratio brut Qlik (0.32 = 32 %)." },
|
||||
qteByMonth: { type: ["object", "null"], description: "Quantités par mois, clés YYYY-MM." },
|
||||
fetchedAt: { type: ["string", "null"], format: "date-time" },
|
||||
},
|
||||
},
|
||||
totalCa: { type: "number" },
|
||||
totalQuantite: { type: "number" },
|
||||
totalMarge: { type: "number" },
|
||||
|
||||
@@ -3,7 +3,7 @@ import { requireApiAuth } from "@/lib/api-auth";
|
||||
import { ok, fail } from "@/lib/api-response";
|
||||
import { productDetailSchema } from "@/lib/api-schemas";
|
||||
import { getGridRowByCodein } from "@/lib/grid-store";
|
||||
import { getNetworkMetricsByCodeCentrale } from "@/lib/qlik-network-cache";
|
||||
import { enrichRows } from "@/lib/api-enrich";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
export const dynamic = "force-dynamic";
|
||||
@@ -41,15 +41,15 @@ export async function GET(
|
||||
);
|
||||
}
|
||||
|
||||
// Métriques réseau à jour depuis le cache (le payload peut dater du dernier calcul).
|
||||
let network = null;
|
||||
if (found.row.codeCentrale) {
|
||||
const metrics = await getNetworkMetricsByCodeCentrale([found.row.codeCentrale]);
|
||||
network = metrics.get(found.row.codeCentrale) ?? null;
|
||||
}
|
||||
// Métriques réseau + gamme serveur relues maintenant : le payload date du dernier
|
||||
// calcul de la grille, alors que ces deux données évoluent indépendamment.
|
||||
const [enriched] = await enrichRows([found.row]);
|
||||
|
||||
return ok(
|
||||
{ ...found.row, network },
|
||||
{ meta: { computedAt: found.computedAt, networkFetchedAt: network?.fetchedAt ?? null } },
|
||||
);
|
||||
return ok(enriched, {
|
||||
meta: {
|
||||
computedAt: found.computedAt,
|
||||
networkFetchedAt: enriched.network?.fetchedAt ?? null,
|
||||
hasNetwork: enriched.network !== null,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { requireApiAuth } from "@/lib/api-auth";
|
||||
import { ok, fail, buildPagination } from "@/lib/api-response";
|
||||
import { productSearchSchema, pickFields, toSortKey } from "@/lib/api-schemas";
|
||||
import { queryGridRows } from "@/lib/grid-store";
|
||||
import { enrichRows } from "@/lib/api-enrich";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
export const dynamic = "force-dynamic";
|
||||
@@ -38,14 +39,18 @@ export async function GET(req: NextRequest) {
|
||||
limit: q.limit,
|
||||
});
|
||||
|
||||
// Métriques Qlik et gamme serveur relues au moment de l'appel (voir api-enrich).
|
||||
const rows = q.enrich === "1" ? await enrichRows(result.rows) : result.rows;
|
||||
|
||||
return ok(
|
||||
result.rows.map((r) => pickFields(r, q.fields)),
|
||||
rows.map((r) => pickFields(r, q.fields)),
|
||||
{
|
||||
pagination: buildPagination(q.page, q.limit, result.total),
|
||||
meta: {
|
||||
query: q.q,
|
||||
computedAt: result.computedAt,
|
||||
scope: q.fournisseur ? `fournisseur:${q.fournisseur}` : "tous fournisseurs",
|
||||
enrichi: q.enrich === "1",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user