Stock Rupture % N saturated at 100% for high-presence products (degenerate
at the article x network grain), so remove it end to end: config id,
NetworkMetric, hypercube (width 8->7), cache, schema, enrich and grid
column. db-init drops the column on existing DBs.
Also remove the "Sans vente 6 mois" tab from the grid page: the grid now
renders directly without the tab switcher; delete the NoSalesTab component.
(The separate Stock negatif page keeps its own sans-vente tabs.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The qData expr path returned empty. Fetch the real master-measure
definition through GetMeasure -> GetProperties (qMeasure.qDef.qDef) and
log rupture, couverture and CA/mag expressions to understand why
Stock Rupture % saturates at 100% for high-presence products.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3 of 4 new measures now resolve to engine qIds and return varied values
(CA/mag, couverture, marge%). Stock Rupture % N resolves but returns a
constant 1.0 (100%) per article — a measure-semantics issue. Log the
resolved rupture & couverture expressions to understand the definition,
and make the enrich sample pick a non-zero match (the previous sample hit
a no-sales code and looked all-zero).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The new measures returned 0 because their qLibraryId was set to the QRS
object GUID, but the engine references master measures by qInfo.qId
(short codes like JhqJ) — an unknown GUID is silently ignored and yields
null. Resolve CA par Magasin / Couverture / Marge % / Rupture % by title
via the in-page MeasureList (whitespace+case normalized), falling back to
the configured id, and log the resolved ids.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a sample log of the first extracted rows (all 8 hypercube columns)
and a sample of the network metrics matched from cache in getProductRows,
to pinpoint whether the new measures return null from Qlik, fail to
persist, or fail to render.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds CA par Magasin, Couverture stock Qte, Marge % and Stock Rupture %
network measures to the Qlik hypercube extraction (in-page Playwright ws
+ legacy server ws), persists them in qlik_network_metrics, joins them in
getProductRows and renders 4 new grid columns with buy-signal coloring
(low coverage / high margin = green; high rupture = rose).
- qlik-client: config IDs (env-overridable, master GUIDs of app 9872ee6e),
NetworkMetric fields, hypercube width 4->8, page height 2500->1250
- qlik-playwright: same 4 measures in the in-page hypercube + row mapping
- schema + db-init: 4 numeric columns (ALTER ADD COLUMN IF NOT EXISTS for
existing DBs)
- cache + grid types + enrich: plumb the new fields end to end
Note: percent measures stored as raw Qlik ratio; UI renders x100 when |v|<=1.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Validated end-to-end. The recent Qlik client authorizes the engine websocket
via ?reloadUri=...&qlik-csrf-token=<token> (not Xrfkey). Capture that token
from the client's own ws, then open ours in-page with it. Use the real engine
master-item qIds (dim Article Code=yesCP, CA N=JhqJ, Quantité N=41516861...,
Magasin Ventes Nb N=yNBLjc) — the QRS object IDs returned error 7001. Select
the supplier's codes on field "Article Code" (the app holds ~1.2M codes).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The recent Qlik client uses qmfe/single-spa (no js/qlik Capability API). Go
back to a raw engine websocket but opened in-page from the authenticated
browser (injected X-Qlik-Session cookie), which the proxy accepts. Use the
session xrfkey and capture ws close code/reason for diagnostics.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Raw and in-page websockets with a self-generated xrfkey are rejected (403).
Switch to the Qlik Capability API (require(['js/qlik']) → openApp → createCube),
which reuses the authenticated page session and manages the engine ws + xrfkey
internally. Auth via injected X-Qlik-Session cookie (Node ticket flow) +
httpCredentials. Logs rich page diagnostics on failure.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Qlik proxy refuses a raw server-side websocket (403, even internally), so
extraction now runs through headless Chromium (NTLM via httpCredentials, like
the hermes agent) and opens the Engine websocket in-page — same origin, which
the proxy accepts. Efficient: selects the supplier's article codes on the
"Article Code" field so the hypercube returns only those rows.
- qlik-playwright.ts: browser singleton, in-page hypercube extraction (paginated)
- /api/qlik/sync: use the Playwright extractor
- Dockerfile: install chromium + headless deps, PLAYWRIGHT_CHROMIUM_PATH, copy playwright-core
- deps: playwright-core (lockfiles synced)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Qlik proxy validates the WebSocket Origin header (whitelist) — a browser
always sends it. Add Origin + User-Agent to the engine ws. Also surface the
403 response (server/www-authenticate/body) for diagnosis.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a "Qlik Sense — Données réseau" section in Paramètres (host/user/password
+ Save & Test buttons), stored in data/.db-config.json. qlik-client reads this
file first (env fallback), so no env vars needed for the sync.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ajoute un onglet "Cadencier / Alertes" dans la page Commandes auto.
Depuis la liste des fournisseurs, on active un fournisseur en alerte de
commande sur un magasin (292/579) avec une fréquence en X semaines,
gérée indépendamment par magasin.
L'échéance est calculée à partir de la dernière réception réelle
(mvtart, genremvt 1/2, fournisseur principal artfou1.preference=1) +
X semaines. Statut: à commander / bientôt / OK, avec temps restant.
- Table commande_cadences (schema + db-init)
- pgGetDerniereReceptionParFournisseur (mvtart par fournisseur/site)
- Server actions CRUD (list/upsert/toggle/remove)
- UI à onglets + cadencier (ajout, édition intervalle, activer/supprimer)
- Remove unsupported `eslint` key from next.config.ts (Next.js 16)
- Wrap dashboard DB enrichment in try/catch so the dashboard renders
even when PostgreSQL is unavailable, preventing the
"Impossible de charger le tableau de bord" error
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Enrich DashboardTopItem with stock and fournisseur fields via PostgreSQL,
then display them under each article in the dashboard top CA/Qte/Marge tables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Exclude products with a stock entry in the last 6 months (NOT EXISTS subquery)
- Add derniere_entree subquery to SQL (last genremvt 1/2 movement date)
- Display derniere_entree column in table and Excel export
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New SQL query pgGetSansVente6Mois returns products with available
stock (qte > 0) whose last sale was over 6 months ago or never.
Third tab added with filtering by supplier, search, sort, and Excel export.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The previous ::boolean cast caused query failure when suspendu column
is not a native boolean type. Casting to ::text first handles all
column types (char, varchar, int, boolean).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add WHERE NOT COALESCE(af.suspendu::boolean, false) AND NOT COALESCE(a.suspendu::boolean, false)
to pgGetArticlesByFournisseur — products flagged as suspended in artfou1
or articles table are no longer shown in the grid.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Instead of unreliable preference=1, use artfou1.no_id DESC (most recently
added artfou1 entry) to identify the last supplier for each product.
Shows an amber warning icon in the Désignation column when the last
supplier ≠ current supplier view, with tooltip showing who to attribute
the sales to.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>