Dashboard d'analyse du marché d'un serveur Dofus Retro privé. Stocke dans une
base SQLite toutes les données de prix scrapées (cartes, items d'équipement,
ressources HDV) et offre une interface web sécurisée pour explorer les prix,
l'historique, les vendeurs et les variations. Remplace l'ancien export Google
Sheets.
dofus-market.service (ExecStart uvicorn, port 8355)systemctl restart dofus-market pour appliquer un changement de code./root/workspace/apps/dofus-market/backend/ : FastAPI (app.py), accès DB (db.py), requêtes (analytics.py), import (import_existing.py), schema.sql, .env (secrets, chmod 600).
backend/data/dofus-market.db : base SQLite (WAL).frontend/ : index.html (dashboard) + login.html.Chart.js (CDN) côté front. Pas de venv.
Table observations : une ligne = un prix observé.
`obs_date, category(cards|items|resources), item_name, price, qty(1/10/100),
seller, map_x, map_y, stats, res_category, source_file, ingested_at, dedup_key`.
dedup_key (hash) rend les imports/ingestions idempotents (pas de doublon).
Table ingestions : journal des imports (audit).
Lecture (cookie de session requis):
GET /api/summary — KPIs + last_ingest (derniere ingestion source=agent : ts, rows_seen, rows_added, note) + today_ingest (rows_added, batches du jour) : badge
"Direct" du dashboard, rafraichi toutes les 30 s cote front.
GET /api/items?category=&search=&sort=&qty=&res_cat= — liste agrégée (dernier/min/moy/max). res_cat (ressources uniquement) filtre par
sous-catégorie HDV (Aile, Cuir, Poudre...). La ligne renvoyée porte res_category.
GET /api/res-categories — sous-catégories de ressources HDV avec le nombre d'objets distincts ([{category, n_items}], trié décroissant). Alimente les
chips de catégories de l'onglet Ressources.
GET /api/craft?only_priced= — recettes de craft croisées avec les prix marché(coût de craft, prix de vente, marge, ROI, couverture ingrédients). Alimente
l'onglet Craft. Voir backend/recipes.py + recipes_schema.sql.
GET /api/item?name=&category= — détail + séries temporelles + vendeurs.GET /api/dofus — marche des Dofus regroupe par type puis par JET. Chaque type (Cawotte, Kaliptus...) : stat (caracteristique dominante lue dans
stats), n_obs, jet_min/jet_max, et jets[] (par palier de jet :
min/avg/med/max, n offres, last_date, offers[] = 10 moins chers).
Le jet est extrait de stats via _JET_RE ("+30 Sagesse", "+18 en
prospection", "+19 aux coups critiques"). Alimente l'onglet Dofus.
GET /api/movers?category=&qty= — plus fortes variations.GET /api/last-run — résumé de la DERNIÈRE run + produits intéressants. La run est reconstituée par rupture temporelle sur ingested_at (le direct envoie un
source_file par MINUTE, donc inexploitable comme identifiant ; on regroupe les
lots séparés de moins de RUN_GAP_MINUTES=30 min). Renvoie found, la période
(start/end/duration_min), les compteurs (n_obs, n_items, n_sellers,
n_maps, n_dofus, by_category, maps) et deals[] = bonnes affaires (offre
de la run ≥35% sous son prix marché historique élagué, robust_mean calculée
HORS run, min 5 relevés d'historique, remise plafonnée à 90% pour écarter les
erreurs OCR de prix ; champs price/market/discount/saving/seller/map/
stats/history). Constantes dans analytics.py (DEAL_*). Alimente le
panneau permanent en HAUT du dashboard (rafraîchi toutes les 30 s).
Synchro des fichiers du bot vers le PC (auto-updater, token DOFUS_SYNC_TOKEN, header X-Sync-Token ou Bearer):
GET /sync/manifest — liste des fichiers synchronisables de Dofus-Bot-Nouveau/scraping-manuel/ (racine only) : {version, count, files:[{name,size,sha256,mode}]}.
mode = overwrite (code/routes/templates, à écraser sur le PC) ou seed
(maps-retro.json / liens-maps.json / sorties-apprises.json / dashboard.json /
faux_marchands.json : écrits seulement s'ils manquent, jamais un état appris).
Exclus du manifest (écrits par le bot en run) : route-observee.txt, .live_sent.json,
*.log, sous-dossiers Data/IMG/debug. Logique dans backend/sync.py.
GET /sync/file?name=<nom> — contenu brut d'un fichier (anti path-traversal : racine de scraping-manuel/ uniquement). Client PC : Dofus-Bot-Nouveau/bot-sync/.
Icones d'items (vrai art du jeu, token X-Sync-Token):
GET /sync/item-names — noms distincts en base + slug + categorie (pour le scraper PC).GET /sync/icons — slugs des icones deja stockees (frontend/item-icons/*.png).POST /sync/icon?name=<nom> — corps = image brute (PNG/GIF/JPG/WEBP) ; normalisee en PNG <=80px par PIL et stockee frontend/item-icons/<slug>.png (servie via /static).
Le slug (accents retires, " - No.X" des cartes retire) est IDENTIQUE cote serveur
(sync.slug), front (slug() dans index.html) et le peupleur, sinon les images ne se
retrouvent pas.
SOURCE DES ICONES (depuis 2026-09-12) : backend/populate_icons.py, cote SERVEUR,
depuis l'API DofusDude (https://api.dofusdu.de, art Ankama Dofus 3). Construit une
table slug->icone a partir des listes completes (equipment/resources/consumables,
~10 700 slugs), puis telecharge ce qui manque. Rejouable (--limit N, --force).
Matching EXACT sur slug uniquement (le fallback /search renvoie de FAUX items -> ne
jamais l'utiliser). Couverture actuelle : items 69%, resources 46%, cards 4% (les
"Paquet de cartes" Retro n'existent pas chez Ankama), total ~567/1613. Les non
couverts (items Retro supprimes/renommes dans le Dofus moderne) tombent sur le repli
onerror du front (pas d'image, aucune casse).
ANCIENNE source ABANDONNEE : Dofus-Bot-Nouveau/recup-images/ (recup_images.py via
moon-bot). moon-bot n'heberge AUCUN sprite (juste des avatars-lettres) et son schema
d'URL /items/<slug>/ renvoie 404 partout : cul-de-sac. Le VPS est de toute facon
bloque en IP sur moon-bot.
Agent PC piloté par l'AIOS (canal de commandes, token X-Sync-Token):
GET /agent/next — l'agent PC récupère le prochain ordre (poll ~2 s) ; marque l'agent en ligne.POST /agent/report — l'agent renvoie log / status / result (stockés en mémoire, agent_hub.py).POST /agent/command body {action, args} — l'AIOS dépose un ordre. Actions : sync, run + args.name (lance une action de la LISTE BLANCHE de l'agent = un .bat précis)
+ args.cli optionnel (options sûres passées au .bat, filtrées par _safe_cli), stop, ping.
Le serveur ne relaie qu'un NOM (+ options allowlistées), jamais du shell (allowlist dans agent.json côté PC).
GET /agent/status?events=N — en ligne ? action en cours ? derniers logs/résultats. Helper AIOS : python3 lib/dofus_bot.py status|logs|sync|run <nom>|stop.
Agent PC : Dofus-Bot-Nouveau/agent/ (aios_agent.py + Agent-AIOS.bat + agent.json).
Pilotage agent depuis le dashboard (authentifié par SESSION, pas par token) :
GET /api/agent/status et POST /api/agent/command = mêmes actions que /agent/*mais protégées par le cookie de session (le navigateur ne voit jamais le token de
synchro). Alimentent le panneau "Agent du bot" en haut du dashboard (statut en ligne,
boutons Synchroniser / Foire / Place marchande / Foire rapide / Lecteur / Stop, logs
en direct, refresh 3 s).
Ingestion (token Bearer séparé, pour l'agent local):
POST /api/ingest body {category, data, source_file?} header Authorization: Bearer <DOFUS_INGEST_TOKEN>.
data = même forme que les JSON scrapés (liste de relevés).
Public: GET /health, GET /login.
.env (DOFUS_ADMIN_PASSWORD_HASH), jamais en clair. Session = cookie signé HMAC (DOFUS_SESSION_SECRET), Secure + HttpOnly.
DOFUS_INGEST_TOKEN (distinct du login).DOFUS_SYNC_TOKEN (distinct aussi ; lecture seule des fichiers du bot)..env en chmod 600, hors Git. DOFUS_ADMIN_PASSWORD_HASH, puis systemctl restart dofus-market.
/root/workspace/Data/*.json via python3 backend/import_existing.py (idempotent).
Data/*.json et POST vers /api/ingest avec le token Bearer. Voir integrations/local-agent/.
(/root/workspace/Dofus-Bot-Nouveau/scraping-manuel/dashboard_live.py, fil
LiveSender) POST chaque vendeur lu vers /api/ingest dans les 5 s, lots de 50,
source_file = live <date heure>. Config cote PC : dashboard.json (URL + token).
integrations/local-agent/ : agent Python (stdlib only) qui tourne sur le PC Windows du bot. Il détecte les fichiers dofus_cards_* / dofus_items_* /
hdv_ressources_* nouveaux ou modifiés et les envoie à /api/ingest
(catégories cards / items / resources). Envoi incrémental via empreinte sha1
(.agent_sync_state.json), idempotent côté serveur. Modes: envoi unique,
--watch (surveillance continue), --full (renvoi complet). Le token réel
n'est PAS stocké ici (voir agent_config.example.json), il vit dans
backend/.env (DOFUS_INGEST_TOKEN) et dans la copie de travail
/root/workspace/Scripts/agent_config.json. Ne modifie PAS le bot de scraping.
Secure → ne marche qu'en HTTPS (donc via l'URLpublique, pas en http://IP:8355 dans un navigateur; curl fonctionne).
3 séries distinctes par objet.
debug_pnj/watchdog_20260924-142723.txt remonte : etape reelle = « Scraping map (-10, -38)... » (le bot AVAIT continue), et 3 fils bloques dans scraping_manuel.py:192 flush (ManuelWorker via _consume du sous-scraper, NetReader, NetInterp) = le tuyau stdout est plein, plus personne ne le vide. Cause : aios_agent.py lance le .bat en text=True SANS encodage (cp1252 strict sous Windows, d'ou les « â†' » dans les logs) ; la ligne [ESC-CHECK] OCR: '...' de -9,-38 (texte OCR garbage, deterministe sur cette map) contient un octet invalide en cp1252 -> UnicodeDecodeError dans readline -> fil _stream mort -> tuyau plein -> gel de tout fil qui ecrit (les 12h09, 15/09 « Raoulita » et 13/09 « capture reseau morte » sont le meme mecanisme). Correctifs : aios_agent.py 2.14 (Popen encoding="utf-8", errors="replace" ; _stream en try/except qui continue de vider le tuyau quoi qu'il arrive) ; scraping_manuel._Tee : ecriture/flush console au mieux (un tuyau casse par un redemarrage d'agent ne fait plus planter le bot, le fichier log garde tout). Le timeout OCR et le chien de garde restent (utiles). Veilleur : --attach avait un t_launch faux (started_at efface par les rapports d'etat periodiques) -> heure du « Lancement : foire-reprise » retrouvee dans les evenements. Run stoppee + tuer-bot, systemctl restart dofus-market (fix agent_hub actif), synchro agent 2.14, 3e lancement de la reprise (bg_task 1d26241b, 14h42) : reconnexion ecran=ingame immediate (repli coordonnees OK), map de reprise auto = -12,-38 (-10,-38 et -11,-38 avaient ete faites par la 2e run avant le gel), « deja dans l'enclos : pas de potion ni de zaap », scraping immediat. 1re tentative sur -12,-38 : 0/8 vendeurs de la liste MEMOIRE lus -> passage de main -> le veilleur a relance (tuer-bot + reconnexion + reprise -12,-38) ; 2e tentative : 2/8, tour complet, liste venue de la memoire -> map COMPLETE (1 vendeur present, Bresaola), puis -12,-39 (zaap, transit), -11,-39... La ligne [ESC-CHECK] OCR: 'vue 4 << -' est relayee sans tuer le lecteur : correctif agent 2.14 valide en conditions reelles.[ESC-CHECK] OCR: '...' (Move_between_maps.pressEscape -> _is_escape_menu_open : capture BitBlt + pytesseract). Le fil de travail s'est donc fige dans cette capture/OCR (tesseract = sous-process lance SANS timeout). Les 2 relevés « -10,-38 » de 12h09 en base sont un rattachement de map errone, pas un scrape de cette map ; 13 maps restaient a faire (-10,-38 .. -12,-40). Correctifs bot (scraping-manuel/, synchro OK, agent 2.13) : (1) watchdog_bot.py, chien de garde interne : pouls = ecritures stdout (_Tee.write -> tick) ; bot muet SM_WATCHDOG_STALL s (defaut 300) -> piles de TOUS les fils via faulthandler dans debug_pnj/watchdog_<horodatage>.txt (table nom->id des fils incluse), STOP_EVT, puis arret force code 3 apres 20 s de grace (minuteur programme AVANT tout, un print peut bloquer) ; filet C faulthandler.dump_traceback_later(exit=True) re-arme toutes les 5 s (tue a 480 s meme si le GIL est tenu, code 1) ; desarme + fichier supprime sur fin normale ; auto-test python watchdog_bot.py --selftest (valide sur le VPS : dump + code 3). (2) OCR borne Move_between_maps._ocr_text : timeout SM_OCR_TIMEOUT (8 s) si le pytesseract installe le supporte, exception -> lecture vide, jamais de blocage ; applique aux 3 appels (menu Echap, coordonnees x2). (3) Mode reprise --resume-from x,y / SM_RESUME_FROM (scraping_manuel.py) : les maps de la route AVANT cette map sont traversees sans scraping ([REPRISE] ... transit), [REPRISE] Map de reprise atteinte puis scraping normal ; route lue en tete de worker ; perso deja dans l'enclos de la Foire sur une map de la route (2 lectures votees identiques) -> ni potion ni zaap (bloques dans l'enclos) ; HUD masque au demarrage -> ensure_booth_closed (Echap puis croix). (4) Scraping-Foire-Reprise.bat (route-foire-auto.txt, SM_NET_DEBUG=1, defaut SM_RESUME_FROM=-10,-38 sans argument), action whitelistee foire-reprise, option CLI --resume-from (regex -?\d{1,3},-?\d{1,3}) dans aios_agent.py. Cote AIOS : lib/dofus_foire_reprise.py : reconnexion sure ([RC] ... EN JEU ; mot de passe demande -> arret propre), map de reprise = 1re map de la route avec < 3 vendeurs releves aujourd'hui (ou --from x,y), run foire-reprise --resume-from x,y, veille (agent hors ligne 3 min, silence 8 min, jeu deconnecte 4 min, [PASSE-LA-MAIN], plafond 1 h par lancement) ; run morte -> pull des watchdog_*.txt (diagnostic dans le bilan), tuer-bot + reconnexion, relance depuis la meme map puis en SAUTANT la map fautive au 2e echec (--no-skip l'interdit), --max-launches 3 ; bilan maps faites/manquantes + releves + extrait des piles. Silencieux (--notify opt-in). Lancement : python3 lib/bg_task.py launch --title "Reprise Foire Dofus" --timeout 7200 -- 'python3 /root/aios/lib/dofus_foire_reprise.py' 1er lancement (bg_task 14f541cd, 14h20) : run morte en code 2 = argparse prenait la valeur -10,-38 pour une option (« expected one argument ») ; corrige dans main() (recollage --resume-from=x,y avant parse_args), synchro, 2e lancement bg_task d3635faf 14h26 : reconnexion OK, potion -> Astrub -> zaap Plaines Rocheuses -> colonne -14 en transit [REPRISE] ... transit, chien de garde actif. Bug annexe corrige : reconnexion._screen rendait unknown x12 avec le perso EN JEU (mot « Coordonnees » rate par l'OCR, captures rc_auto_unknown_*.png) -> repli nav.read_map_once() (lecture des coordonnees) quand aucun autre ecran ne correspond ; lib/dofus_foire_reprise.py ne lit plus que les lignes de log POSTERIEURES au lancement (fresh_lines, champ ts/action des evenements) pour ne jamais confondre un vieux [RC] ... EN JEU ou [PASSE-LA-MAIN] avec la run courante. BUG BACKEND trouve dans la foulee (14h29) : agent_hub.report remettait running a vide sur N'IMPORTE QUEL resultat (sync, pull, refus « occupé par ... ») jusqu'au rapport d'etat suivant de l'agent (~35 s) ; une synchro de reconnexion.py pendant la run a fait croire au veilleur que la run etait finie (ses ordres tuer-bot/reconnexion/relance ont ete refuses « occupé », la run du bot n'a pas ete touchee). Corrige : running n'est efface que par le resultat de l'action en cours (hors refus « occup... ») ; systemctl restart dofus-market a faire apres la run en cours (file d'ordres en memoire, DB intacte). lib/dofus_foire_reprise.py : fin de run = resultat pour foire-reprise posterieur au lancement (jamais un refus), sinon running vide pendant 7 sondages (70 s) ; nouveau --attach (se raccrocher a une run foire-reprise deja en cours, puis enchainer relance/saut). Sauvegardes : .tmp/{scraping_manuel.py,Move_between_maps.py,aios_agent.py,reconnexion.py,agent_hub.py}.bak-20260924b. PIEGE : Move_between_maps.py est en CRLF ; une edition via io.open (newlines universels) le convertit en LF, a restaurer avant synchro.tuer-bot (agent 2.12). Run scrap-general (bg_task 3bd67dcd, 11h47) : Place Marchande Bonta OK (29 vendeurs, 2 min, lignes SANS coordonnees en base : map_x/map_y NULL, map_id 4259), Village Amakna OK (81 vendeurs, 6 maps, 6 min), Foire 17/30 maps (94 vendeurs) puis le bot s'est FIGE sur -10,-38 apres « Boutique encore ouverte (reseau+ecran) : Echap (essai 1/3) » (plus aucune ligne de log, meme pas [NET] ; -9/-10,-38 = maps deja connues pour la fenetre d'achat a 2 panneaux, cf. ensure_booth_closed 16/09). Le veilleur lib/dofus_scrap_marchand.py a stoppe la run apres 10 min de silence (12h19), rc 1. Le jeu s'est ensuite deconnecte pour inactivite. CONSTAT : stop de l'agent tue le cmd.exe mais le python scraping_manuel.py (PID 14752) a survecu 40 min, fenetre « Bot - controle » encore a l'ecran (incident shell=True du 13/09, encore d'actualite). AJOUT : action whitelistee tuer-bot -> scraping-manuel/Tuer-Bot.bat (PowerShell : Stop-Process des processus dont la ligne de commande contient scraping_manuel.py ou combat_run.py, hors $PID/powershell ; jamais l'agent ni le jeu), aios_agent.py VERSION agent-2.12 (sync -> relance auto OK). Sequence de recuperation validee : run tuer-bot -> run reconnexion (Ok, login, serveur, perso Lama : OK) -> run sortir-foire. FIX veilleur : game_silent() acceptait seulement des lignes « aucun paquet » consecutives alors que le lecteur entrelace « capture armee » : corrige (toutes lignes [NET] + au moins une alerte). A CORRIGER cote bot : le gel apres l'Echap d'ensure_booth_closed (candidats : _close_options_menu/_options_menu_open, _booth_open_on_screen grab, _net_up_to_date) ; ajouter un chien de garde interne (thread) qui log l'etape en cours toutes les 60 s.lib/dofus_scrap_marchand.py (remplace les scripts jetables .tmp/scrap_marchand_watch.py). Lance l'action scrap-general (Scrap-General.bat : Place Marchande Bonta -> Places Marchandes Village Amakna -> Foire, 3 appels scraping_manuel.py separes) et veille jusqu'a la fin reelle avec 4 garde-fous (regle automation-liveness-guard) : agent hors ligne > 3 min, aucune nouvelle ligne de log > 10 min (stop), jeu deconnecte = que des lignes [NET] aucun paquet Dofus depuis pendant > 4 min (stop), plafond global 2h30. Bilan imprime avec les releves marchands ajoutes en base par zone (Bonta / Village / Foire, via map_y), code retour 1 si echec ou 0 releve. Silencieux (--notify opt-in). Options : --no-launch (veiller une run deja lancee), --action <nom> (une seule route), --timeout. Usage normal : python3 lib/bg_task.py launch --title "Scrap marchand Dofus" --timeout 10800 -- 'python3 /root/aios/lib/dofus_scrap_marchand.py'. Contexte : le run du 22/09 (bg_task c67b2ce7) a tourne 68 min A VIDE (0 releve marchand en base, logs = uniquement des alertes de silence reseau) avant que l'agent passe hors ligne : le jeu etait deconnecte et rien ne coupait la run ; d'ou le garde-fou « jeu deconnecte ». Premier run avec ce lanceur : 24/09 11h47 (bg_task 3bd67dcd). Prerequis rappeles par le .bat : potion Bonta (slot 1833,1032) ET potion de rappel Astrub (template) dans la barre d'action ; les routes Village/Foire partent de rappel 4,-19.scraping-manuel/Scraping-HDV-Alch.bat repasse sur route-hdv-alch.txt avec SM_ZAAPI_ANCHOR=1160,301 + SM_POTION_XY=1833,1032 (meme structure que les 7 autres HDV, regle depart Milice du 13/09 ; l'economie de potion du 15/09 s'applique via zaapi-anchors.json). Cause : la variante « zaapi au sol » du 15/09 (route-hdv-alch-onmap.txt, ancre fixe 835,370) ne marchait que depuis la map -32,-50 ; dans l'enchainement de tous les HDV (lib/dofus_hdv_chain.py, alch en 8e apres forgemage) elle tombait en timeout 3 fois sur 3 (17, 18 et 22/09), soit ~40 min perdues par run. Ancienne version sauvegardee cote AIOS (.tmp/Scraping-HDV-Alch.bat.bak-20260924). Synchronisee sur le PC (sync : 1 fichier). VALIDE en live le 24/09 (bg_task edfbc329, alch en 8e position apres forgemage) : HDV ouvert au 1er essai, 118 objets lus, 270 releves, 9 min 30 au lieu de 40 min de timeout. Enchainement complet 8/8 OK en 1 h 06 (2816 releves, 1115 objets, 50 categories). LIMITE connue (pre-existante, deja 0 le 15/09) : a l'HDV alch, les categories « Potion d'oubli de metier / de sort / de maitrise » sont lues 0 : le menu du jeu classe l'apostrophe AVANT l'espace (Potion d'oubli... avant Potion de forgemagie) alors que pnj_talk._norm supprime l'apostrophe (« potiondoubli » apres « potiondeforgemagie »), donc apres « Potion de forgemagie » le bot doit REMONTER dans le menu, ce que la regle « ordre monotone » ne fait pas -> selection ratee, 0 clic. Piste : trier avec une cle qui garde l'apostrophe comme caractere < espace, ou scraper dans l'ordre exact renvoye par l'enumeration. Rappel : enchainement « tous les HDV » = python3 lib/bg_task.py launch --title "Scrap tous HDV Dofus (8)" --timeout 18000 -- 'python3 /root/aios/lib/dofus_hdv_chain.py hdv-ressources hdv-boucher hdv-bucheron hdv-mineur hdv-paysan hdv-pecheur hdv-forgemage hdv-alch' (les scripts .tmp/hdv_scrape_all.py du 22/09 plafonnaient a 25 min/HDV, trop court pour Ressources qui prend ~27 min). A surveiller : le 22/09 Boucher etait « termine » mais 0 releve Viande en base (echec silencieux, cause inconnue).#q) appelait loadTable() à CHAQUE frappe (un appel API /api/items + une réinjection innerHTML de la table entière par lettre), et les onglets volumineux rendaient toutes leurs lignes d'un coup (Ressources 3087, Items 1902 objets distincts) -> DOM énorme, re-render à répétition -> lag puis crash de l'onglet. Correctifs (frontend/index.html, statique, pas de restart, un simple refresh navigateur suffit) : (1) helper debounce(fn,ms) (après api()) ; recherche items debouncée 250 ms, recherches Âmes/Banque/Craft/Ventes debouncées 200 ms (.oninput). (2) Plafond RENDER_CAP=400 : loadTable ne rend que les 400 premières lignes + bandeau ".showall" « N sur M affichés » avec bouton Tout afficher (state.showAll) ; state.showAll remis à false sur changement de recherche/tri/catégorie/quantité/rareté/sous-catégorie ressources. CSS .showall. Aucun changement backend/API. Syntaxe JS validée (node --check).scraping-manuel/scraping_manuel.py worker() (bloc recall_first) : au lancement d'un run HDV (route avec rappel + zaapi), CAS 1 = si le perso est DÉJÀ à Bonta sur une map dont on connaît le zaapi (zaapi-anchors.json : {"x,y":"sx,sy"}, seed -33,-57=Milice 1160,301 et -33,-55=HDV alch 1468,368), on force SM_ZAAPI_ANCHOR sur cette map et on prend son zaapi SANS potion ; CAS 2 = map inconnue/hors Bonta -> ensure_start_at_zaap (potion Bonta -> Milice -33,-57) puis ancre Milice. Helpers route_has_zaapi(), _load_zaapi_anchors(), _bonta_zaapi_anchor(coords). La table grandit au fil des maps taguées via Clic-Guide ; s'applique à TOUS les HDV Bonta (le cas 1 ne se déclenche que sur les maps listées). Vendeur tailleur SM_HDV_NPC_XY calibré 978,463 -> 980,441 (Clic-Guide). Fichiers synchronisés (overwrite) : scraping_manuel.py, route-hdv-tailleur.txt, Scraping-HDV-Tailleur.bat, zaapi-anchors.json. Testé en logique (3 cas) ; validation live = sync + run HDV.analytics.py : capes() liste les capes deja capturees (distinct listings_v items LIKE '%cape%' avec prix) enrichies de leurs jets de base (ref_jets_for -> base_jets.json xixou), nb d'offres, dernier prix, min, et l'etat capture. Config persistee data/capture_config.json {"ignore":[noms d'origine...]} (dedup par forme normalisee) via set_capture(name, capture) / capture_ignore_list(). (2) Endpoints (app.py) : GET /api/capes (session), POST /api/capture-toggle body {name, capture} (session), GET /sync/capture-config (token sync) = {"ignore":[...]} lue par le bot. (3) Front (index.html) : onglet capes (groupe Equipement) renderCapes/drawCapes : table icone-less avec toggle-switch CSS .sw, jets de base en puces .jchip, recherche, filtre "seulement ignorees", boutons Tout capturer / Tout ignorer. (4) Bot : bot-sync/aios_sync.py _sync_capture_config() ecrit capture-ignore.json dans scraping-manuel a chaque sync ; scraping-manuel/pnj_talk.py _load_capture_ignore() (fichier ou SM_CAPTURE_IGNORE_FILE) -> set normalise passe a scrape_hdv_category_net(..., ignore_names=) qui SAUTE toute ligne dont _norm(nom) est dans la liste (match nom EXACT). Une liste d'ignore non vide force le scrape net (name-aware) au lieu du replay par position. Teste cote serveur (toggle -> /sync/capture-config reflete). ACTIVATION cote bot : necessite un sync puis un run HDV pour valider en live. Backend redemarre.sortir-foire : sortir le perso de l'enclos de la Foire du Troll + rappel. Dans l'enclos de la Foire la potion de rappel est BLOQUEE par le jeu ; il faut d'abord ressortir. Procedure (dictee par Matt) depuis -11,-37 a l'interieur : (1) se placer/verifier -11,-37 ; (2) 1er clic de sortie vers le BAS (sud) = le perso marche vers la porte ; (3) attente ~5 s ; (4) 2e clic de sortie (bas) = franchit -> changement de map ; (5) map changee -> potion de rappel (Astrub 4,-19). GARDE-FOU principal (etape la plus dangereuse) : si apres le 4 la map n'a PAS change, le 1er clic n'a pas franchi l'enclos -> on REPREND au 1er clic (boucle bornee SM_FOIRE_EXIT_TRIES, defaut 4). Confirmation finale de la sortie = la potion de rappel FONCTIONNE (refusee tant qu'on est dans l'enclos). Garde-fou serveur (_server_guard) a chaque clic, clics humanises, aucun scraping. Implementation : scraping_manuel.py exit_foire() (+ helpers _foire_locate_exit/_foire_walk_to, branche SM_SORTIR_FOIRE=1 en tete de worker()) ; lanceur Sortir-Foire.bat ; action whitelistee sortir-foire -> Sortir-Foire.bat (agent/aios_agent.py). Reglages : SM_FOIRE_EXIT_DIR (bas), SM_FOIRE_EXIT_TRIES (4), SM_FOIRE_WALK_WAIT (5 s), SM_FOIRE_CROSS_TIMEOUT (12 s). Testee en simulation (28/28 verifs : nominal, retry-depuis-etape-2, abandon borne, coupure serveur, rappel non confirme, marche depuis une autre map, coords illisibles) PUIS en live : perso parti de -9,-37 -> marche jusqu'a -11,-37 -> sortie -> arrive au zaap d'Astrub (4,-19). Lancer : python3 lib/dofus_bot.py run sortir-foire.analytics.ref_jets_for(name) lit data/base_jets.json (max de base par stat, source xixou) et rend une liste ordonnee {stat, label FR, max} ; ajoute au retour de item_detail sous ref_jets. Nouveau _JET_LABELS/_JET_ORDER (libelles FR + ordre d'affichage). (2) Front (renderItemModal) : bloc "Jets de référence (normaux)" (roll de base max, puces via formatJets) affiche AU-DESSUS de la liste ; la table des annonces s'appelle desormais "Annonces constatées (prix + jets)" et affiche les jets EN LIGNE (formatJets(s.stats) au lieu du tag repliable jetsCell), limite portee a 120 annonces. CSS .refjets/.jline/table.sellt. Backend redemarre.analytics.last_run() melangeait des ingested_at avec fuseau (offset-aware) et sans (naive) -> TypeError a la soustraction de dates -> GET /api/last-run renvoyait une 500 -> le panneau "Derniere run + produits interessants" ne s'affichait JAMAIS (front : if(!d) return). Corrige dans _parse_ts (normalisation naive/UTC systematique). Verifie : found=True, 71 bonnes affaires sur la run 21:56-22:09. (2) UI (demande Matt) : l'onglet Accueil est retire de CATS ; on ouvre l'accueil en cliquant le logo "DofusMarket" en haut a gauche (onclick=setCat('accueil')). L'etat cat='accueil' reste la vue par defaut au chargement et la cible du clic logo ; render()/#home inchanges. Backend redemarre.run_20260913-154141.pcap) contenait les messages EHl = le detail de CHAQUE annonce avec ses JETS (l ancien ingest ne gardait que EHP = prix MOYEN sans jets). Nouveau captures-combat/ingest_hdv_jets.py : parse les EHl (decode via lecteur_reseau.decode_effects + EFFECT_NAMES etendue : 174=Initiative, 240-244=Res fixes ; effet 623=ame exclu), ecrit une observation par annonce avec jets. 18710 annonces ingerees, ancienne source EHP scrap-hdv-items-20260913 supprimee. recipes.price_index : prix vente VERIFIE (jets basiques, non-exo, non-premium) prioritaire ; repli prix de reference sans jets plafonne DM_REF_PRICE_CAP (defaut 1.5M) via _accept_listing. Champ sell_source (marche/ref/lot) ajoute au rapport + badge ref. au front. Resultat : 1186 crafts price-es dont 1092 VERIFIES (vs 273), fausses marges multi-M eliminees (L Epee Nice masquee, Az tech 478k, Ceinture Bitoufale 666k).a 2 niveaux (groupes -> sous-onglets) est remplacee par UNE rangee d'onglets de categorie a
plat en haut de page (renderTabs liste tout CATS dans #tabs ; #groups, setGroup,
GROUPS/GROUP_OF ne sont plus utilises). (2) Nouvel onglet Accueil (cat='accueil',
defaut au chargement) : le bloc "derniere run + produits interessants", les KPIs et la
collection sont deplaces dans #home et ne s'affichent QUE sur l'accueil (render() masque
#home sur les pages de categorie). (3) Onglet Banque : champ prix unitaire min qui masque
les objets sous ce seuil ; les totaux (valeur globale, quantite, chips par categorie) sont
RECALCULES cote client sur l'ensemble retenu, donc filtrer change la valeur globale affichee
(la valeur totale complete reste rappelee "sur X au total"). Tout est cote frontend/index.html
(servi depuis le disque : un simple refresh suffit, pas de restart).
banque_stock (item_name PK, qty, fiche, updated_at) creee dans db.init_db() (migration
idempotente) ; script backend/banque_ingest.py qui lit data/agent-debug/banque-inventaire.jsonl
(rapatrie du bot) et remplit la table en dedoublonnant par nom (garde la DERNIERE ligne a qty
valide, car le jsonl est en mode append entre deux runs) ; analytics.banque() croise le stock
avec le prix HDV le moins cher a l'unite (offre la moins chere du dernier jour, lot x1/x10/x100 le
plus avantageux) ; endpoint GET /api/banque (auth) renvoie items valorises + total_value +
by_category ; onglet front "Ma banque" (groupe Coffre) avec cartes de synthese, chips par
sous-categorie, tableau triable. Premiere ingestion : 723 objets, qty 270 501, valeur ~27,16 M
kamas. Reingerer apres un nouveau scan : python3 backend/banque_ingest.py puis
systemctl restart dofus-market. Le scan cote bot : action banque-inventaire (voir bot).
Eteindre.bat avait une parenthese dans un echo a l'interieur d'un bloc if (...) : cmd ferme le bloc trop tot
(". etait inattendu.", code 255) MAIS la ligne shutdown precedente s'execute quand meme : le
PC s'est eteint alors que l'agent renvoyait "ok: False". Corrige (plus de parenthese dans les
blocs cmd ; regle : jamais de (/) dans un echo sous if (...)). Le .bat corrige part au
prochain demarrage de l'agent (do_sync initial). (2) La file d'ordres (agent_hub.py) est EN
MEMOIRE, sans endpoint d'annulation : des ordres deposes pendant que le PC est eteint
s'executent au reboot (ici un 2e eteindre aurait re-eteint le PC). Purge = `systemctl restart
dofus-market` (perd seulement l'historique d'evenements, pas la DB). A retenir avant tout
run eteindre : verifier pending = 0 ensuite, et ne jamais renvoyer un ordre a un agent
hors ligne sans purger.
aios_agent.py 2.9 : deux actions dans DEFAULT_ACTIONS, eteindre (Eteindre.bat = shutdown /s /t 60,
delai en secondes surchargeable en argument CLI, message affiche a l'ecran) et
annuler-extinction (Annuler-Extinction.bat = shutdown /a, code 0 meme si rien n'est
en attente, erreur 1116 = normal). Usage : python3 lib/dofus_bot.py run eteindre (toujours
avec la confirmation de Matt) puis run annuler-extinction dans la minute pour annuler.
Le delai de 60 s laisse l'agent renvoyer son log avant la coupure. Limites : l'agent doit
tourner sur le PC (pas lance au demarrage de Windows) ; aucun rallumage a distance (il
faudrait du Wake-on-LAN). Pourquoi : Matt voulait eteindre son ordi depuis l'AIOS ; le
pont agent existant evite tout tunnel/SSH supplementaire. Test OK le 12/09 (annulation).
lecteur avait 45 s de retard, faux BLOCAGE F0, 5 Echap). Cote bot (scraping-manuel/) :
lecteur_reseau.py reecrit en 3 fils (capture brute -> interpretation -> ecriture), filtre
noyau BPF limite a la connexion Dofus (re-arme apres 45 s sans paquet), temoins backlog /
lag / drained(), copie brute Data/captures/run_*.pcap relue en fin de run
(finalize() -> reconcile_from_pcap). scraping_manuel.py : _net_up_to_date() avant F0
et A3, l'ecran tranche si le lecteur est en retard ou contredit 2 fois un ecran propre.
Agent aios_agent.py 2.7 : option CLI --maps N autorisee (run foire --maps 2 = run
courte de test) ; stdin=DEVNULL (le pause final des .bat rendait l'action "en cours"
pour toujours) ; journal envoye en LOTS ({"type":"log_batch","lines":[...]}, un POST
toutes les 0,25 s au lieu d'un par ligne : le tuyau stdout du bot ne bloque plus, c'etait
la 2e source de retard du lecteur, 7 s mesures sur la run test). Serveur : agent_hub.report
eclate un log_batch en evenements log, /agent/report repond {"ok":true,"batch":true}.
sync.py : maps-provenance.json et maps-a-verifier.json passent en seed-only (ecrits
par le bot). Moteur : suppression de l'Echap aveugle apres le scraping (ouvrait le menu
options, 1er deplacement rate), menu options ferme par template avant tout deplacement.
Regles : section N de Dofus-Bot-Nouveau/REGLES-BOT-PROPOSITION.md.
(-11,-37 -> haut au lieu de droite) : _dirs_to classait un chemin appris en transit avant la
voisine directe ; corrige (voisine d'abord). (b) A -10,-38 la capture reseau est morte en
silence (45 s sans paquet, boutique jamais lue) : _capture_loop rouvre le handle apres 8 s
de silence. (c) Donnees de la run recuperees : pcap run_20260912-212903.pcap relu sur le VPS
(reconcile_from_pcap, +18 releves) puis ingestion directe db.ingest_payload : +252 lignes
cartes, +22 lignes items. Pull : --dir "ressources-IMG-Data (1)/Data" (relatif au dossier
bot DofusRetroBot, pas a scraping-manuel).
recipes.py : resolve_price() valorise un ingrédient par son prix marché (x1), sinon par le prix
unitaire d'un lot x10/x100 (source: lot), sinon par le COÛT DE CRAFT de sa propre
recette (récursif, anti-cycle, profondeur <= 6, source: craft). /api/craft renvoie en
plus derived / n_derived. Couverture : 1 775 / 2 197 recettes à coût complet (81 %,
contre ~51 % le matin), dont 315 grâce à un intermédiaire déduit ; 278 avec marge (prix de
vente connu). Onglet Craft : coût préfixé « ≈ » + badge quand un intermédiaire est déduit,
info-bulles listant les ingrédients déduits / manquants. Service redémarré.
lecteur_reseau.py : provenance par id (maps-provenance.json : manuel / ocr / reseau-verifie / route / estime,
confirm), ancre = dernière position vérifiée + hops_since_anchor (changements de map
comptés au réseau), set_anchor, confirm_map, is_verified, entrée manuelle jamais
écrasée. scraping_manuel.py : _ocr_plausible (OCR accepté seulement si distance <=
maps traversées depuis l'ancre), verify_position appelée avant scraping et avant chaque
mouvement (confirme, corrige, ou verification manuelle), _request_manual_verification
(capture debug_sorties/verif_map_<id>_est_<x>_<y>.png + maps-a-verifier.json + arrêt
propre). Côté AIOS : lib/dofus_map_verif.py check (remonte captures + attente) et
set <id> <x> <y> (écrit maps-corrections.json + sync). Règles complètes proposées :
workspace/Dofus-Bot-Nouveau/REGLES-BOT-PROPOSITION.md.
Chronologie (journal PC scraping-manuel-log.txt, capture echec_-14_-40_bas_memoire.png
montrant « Coordonnées -14,-37 » quand le bot se croit sur -14,-40) : à 18:23:04 la map
change PENDANT le scraping de -14,-43 (id 3255, autres marchands) sans que la route
le voie ; puis chaque nouvelle map est « apprise » par estimation (position supposée
+ 1) -> ids 3256..3259 enregistrés faux (décalés d'1 puis 2), et read_coords_voted
préfère le réseau (donc la table fausse) à l'OCR écran qui affichait la vérité ;
enfin les clics de sortie (marche > 12 s sur les grandes maps) déclenchaient des
changements de map « que plus personne n'attendait » -> re-clics -> descente jusqu'à
-35. Correctifs : (1) scraping_manuel.py worker : si l'id de map change pendant
run_scraper, alerte + resynchronisation (réseau connu, sinon OCR x2 identiques) ou
arrêt propre ; (2) _wait_coords_change : fenêtre de stabilisation 1,5 s comptant les
doubles changements, et contre-vérification OCR (2 lectures identiques, <= 3 maps)
avant tout apprentissage par estimation ; (3) clic de sortie touche A : timeout 12 -> 18 s ;
(4) lecteur_reseau.load_tables applique maps-corrections.json (poussé par sync) :
ids 3250..3262 = -14,-47..-35 (contigus, vérifiés). Données : les relevés Foire de
18:23 à 18:33 sont rattachés à une map décalée (-43 = réellement -42, etc.) ; prix
corrects, seule l'attribution de map est fausse.
hdv-ressources-ici, ONLY_CATS) :Os 97/104 objets (263 relevés), Etoffe 46/48 (111 relevés). Couverture recettes :
équipements 65 % -> 87 % (1 351/1 546), toutes recettes 51 % -> 81 % (1 774/2 197).
Le menu OCR liste maintenant 22 catégories (Os inclus) ; Etoffe reste absente de
l'énumération mais est sélectionnée par nom via EXTRA_CATS (matching tolérant).
(a) lecteur_reseau.py _HDV_PRICE_RE : le champ flags accepte tout sauf ;
(les consommables portent leurs EFFETS dans la ligne de prix, ex. 6e#4b##0d0+75 :
rejetés avant -> "Viande/Poisson comestible" lus 0, 100 % de parasites). (b)
pnj_talk.py : la catégorie Os (2 lettres) était jetée par le filtre OCR du
menu (len >= 3) et Etoffe mal matchée ; à l'ouverture le serveur annonce 23
types (paquet ECK), le bot n'en visitait que 21. Ajout _HDV_SHORT_CATS,
_cat_match tolérant (ratio 0.8, noms >= 5), et modes SM_HDV_ONLY_CATS (ne
scraper que ces catégories) / SM_HDV_EXTRA_CATS (ajouter aux énumérées). (c)
Nouveaux bats/actions : hdv-ressources (one-click potion -> zaapi "ressources" ->
HDV -32,-57, vendeur 783,512, EXTRA_CATS=Os,Etoffe, NET_DEBUG) et
hdv-ressources-ici (rattrapage ONLY_CATS=Os,Etoffe). Le dump réseau est lu via
pull --dir debug_pnj --glob net-debug.log. Analyse complète des trous côté
équipements : workspace/Dofus-Bot-Nouveau/ANALYSE-DATA-EQUIPEMENTS-2026-09-12.md
(65 % des recettes d'équipement ont un coût calculable, 3 % une marge : aucun HDV
d'équipement jamais scrapé ; prochaine étape = HDV équipements/bijoux/armes, avec
lecteur à faire évoluer pour garder toutes les annonces (uid) d'un même gid).
pech depuis Milice -33,-57 → HDV -35,-55, vendeur PNJ SM_HDV_NPC_XY=815,642. Boucher : zaapi
bouc → HDV -30,-50, vendeur SM_HDV_NPC_XY=1298,650 (position trouvée par
enregistrement du clic réel via Enregistrer-HDV.bat puis pull de
../Data/records/<session>/events.jsonl : le signal net.hdv_count passant de 0 à
positif isole les 2 clics d'ouverture). Bats/actions : hdv-pecheur[-recon|-ici],
hdv-boucher[-recon|-ici]. Relevés en base : pêcheur (Poisson, Poisson vidé),
boucher (Viande 21 objets, Viande conservée 16). BUG connu : les sous-catégories
« comestible » (Poisson comestible 42, Viande comestible 39) sont annoncées mais lues
0 (100% des clics = PARASITES) : la lecture réseau des lignes de ces catégories
échoue, à corriger dans scraping_manuel.py (les autres catégories passent avec ~1
parasite). ~81 objets comestibles encore manquants.
Avec le correctif (3), re-scrape complet un par un (relevés live sur le dashboard) :
alchimiste 124, mineur 160, bûcheron 82, runes forgemagie 51, paysan 44, « 0
parasite » sur quasiment toutes les catégories. PIÈGE de navigation identifié et
corrigé : le scrape laissait le HDV OUVERT, dont la fenêtre masque l'affichage des
coordonnées (coin haut-droit) → au run suivant read_coords_voted échoue → potion
de rappel abandonnée → navigation en échec. FIX : scraping_manuel.py ferme
désormais le HDV automatiquement en fin de scrape (appelle reconnexion.do_close_hdv,
clic souris « Fermer » 1741,798), désactivable via SM_HDV_NO_CLOSE=1. Permet
d'enchaîner les HDV (et un futur cron balayant tous les métiers) sans close-hdv
manuel entre chaque. Rappel navigation : chaque route métier part de rappel -33,-57
(potion Bonta) → zaapi (onglet Hôtels de vente ; Ateliers pour le forgemage) → HDV.
réseau" du (2) n'était PAS un problème de capture : le dump net-debug.log
(mode SM_NET_DEBUG=1, désormais écrit dans debug_pnj/ = seul dossier pullable,
et purgé à chaque run) a prouvé que le serveur répond à CHAQUE clic avec la ligne
de prix, mais fragmentée (préfixe EHl<gid>| amputé au réassemblage → le
fragment commence par les derniers chiffres du gid, ex
4|26138;77#1##;7,0;85,0;993,0;1524). Surtout, le champ <flags> (ex 77#1##,
9e#a##) est presque toujours REMPLI, donc il n'y a pas de ;; : l'ancienne
regex _HDV_PRICE_RE = ";;..." ratait 51/54 runes (0 en prod), et comme la ligne
de prix n'était jamais parsée, la moyenne (EHP fragmentée) ne l'était pas non plus.
CORRECTIF dans lecteur_reseau.py : nouvelle regex
;[0-9a-fA-F#]*;(x1)?;(x10)?;(x100)?;(gid)$ (queue fiable, gid ancré en fin,
flags rempli OU vide → rétro-compatible ancien format ; lots vides gérés) +
nouvelle branche EHm+<uid>|<gid>|<flags>|x1|x10|x100 (variante poussée quand un
objet est affiché/rafraîchi). VALIDÉ EN PROD : hdv-forgemage-ici passe de
0 lu / PARASITES 52 à 51 lus / PARASITES 1, **51 runes + moyennes envoyées au
dashboard**. Note : le [BILAN] 0 carte est l'ancien compteur fichier ; le chemin
réseau-HDV pousse en direct (voir la ligne [LIVE] ... relevés). Bats/actions de
debug ajoutés : hdv-forgemage-netdebug, hdv-forgemage-netdebug-ici (route/ici
+ SM_NET_DEBUG=1), réutilisables pour reverse-engineer un futur format HDV.
onglet zaapi. Le forgemage n'est PAS dans l'onglet « Hôtels de vente » du zaapi
mais dans l'onglet « Ateliers » (zaapi > Ateliers > forgemage). Nouveau
support SM_ZAAPI_TAB dans use_zaapi (pnj_talk.py) : force l'onglet du zaapi
(mots-clés OCR, défaut hotels,vente) + repli de clic d'onglet positionné selon
la cible (Ateliers 872,151 / Divers 955,151) si l'OCR de l'onglet rate. Route
rappel -33,-57 → zaapi forgemage (onglet Ateliers) → map atelier forgemagie
-38,-55 ; vendeur PNJ au comptoir sous l'enseigne bleue à rune rouge,
SM_HDV_NPC_XY=700,668 (clic bon du 1er coup, HDV ouvert, catégorie unique
« Rune de forgemagie » = 54 objets). Nouveaux bats/actions :
hdv-forgemage (one-click), hdv-forgemage-recon, hdv-forgemage-ici.
RÉSULTAT : navigation + vendeur OK, mais 0/54 runes capturées : mur de
capture réseau des prix (cf. LIMITE CONNUE ci-dessous), ici en échec total
(les 52 clics partent, aucune ligne de prix EHl ne remonte). « PARASITES » dans
le log = simplement clics − résultats (pas un filtre). À débloquer avec un dump
paquets dédié (net-debug non pullable en l'état).
alch depuis Milice-33,-57 → HDV alchimiste -33,-55 ; vendeur PNJ au comptoir sous l'enseigne à la
fiole verte, SM_HDV_NPC_XY=1650,457). Nouveaux bats/actions côté bot
(Dofus-Bot-Nouveau/scraping-manuel/) : hdv-alch (one-click potion→Milice→
zaapi→scrape), hdv-alch-recon, hdv-alch-ici. 49 objets poussés au dashboard.
Trois correctifs bot : (1) reconnexion.py étape close_hdv + Close-HDV.bat +
action close-hdv : ferme une fenêtre HDV restée ouverte par un CLIC souris sur
« Fermer » (1741,798) au lieu d'Échap (Échap = keybd_event, part dans la fenêtre
qui a le focus clavier ; au démarrage auto le jeu n'a pas le focus, donc l'Échap
ne fermait pas le HDV et bloquait la lecture du HUD). (2) use_zaapi (pnj_talk.py)
double-clique désormais la 1re ligne filtrée en la contraignant à la BANDE de la
1re ligne (~Y233) : l'OCR hallucinait un faux mot à Y~650 (décor de la Milice
transparaissant sous la fenêtre semi-opaque) et cliquait dans le vide. (3) Cadence
de clic réglable (SM_HDV_CLICK_MIN/MAX) dans scrape_hdv_category_net.
LIMITE CONNUE : sur ce HDV les grosses catégories de PRODUITS FINIS
(Potion 47→6, Objet de dons 18→3, potions d'oubli 30→0) sous-capturent, alors que
les catégories ressources passent en entier (Metaria 13/13, Teinture 5/5). Clic +
scroll + OCR des lignes sont pourtant PARFAITS (46 clics, 47 noms distincts) et le
client AFFICHE bien les prix : le manque est au niveau de la CAPTURE RÉSEAU des
lignes de prix EHl pour ces items (cause non tranchée : ni la cadence — 6/47 même
à 17s/obj — ni le scroll ni le format regex ; nécessite un dump paquets propre de
Data/net-debug.log côté PC). Les plans de clic appris (Data/hdv_click_plans.json)
mémorisent la capture partielle : relancer avec SM_HDV_RELEARN=1 après un futur
correctif réseau.
bot (qui poll /api/agent/status toutes les 3 s) était PERMANENT en haut de page
et tournait en fond sur TOUTES les vues -> lag (signalé Matt). Désormais : (1) il
a son propre onglet « Agent du bot » (groupe Bot) ; startAgentPolling/
stopAgentPolling : le poll 3 s ne tourne QUE quand cet onglet est ouvert, stoppé
partout ailleurs. (2) Les onglets sont regroupés en sections (rangée de groupes +
sous-onglets) : Cartes (cartes/paquets), Équipement (items/classiques/exos/
montures/archi/dofus), Ressources & Craft (ressources/craft), Analyse (variations/
vendus), Bot (agent). GROUPS + renderTabs/setGroup dans index.html ; #groups
au-dessus de #tabs ; #agentpanel retiré du haut de page (monté dans la vue par
renderAgent). Front pur (FileResponse par requête, aucun redémarrage).
source scrapstuff équipement-seul). backend/import_recipes_ancestra.py parse
le dump LOCAL `/root/workspace/perso/dofus-retro-129/data/AncestraRemake/BDD/
AncestraR_Game.sql : table crafts` (id_résultat, "ingId*qty;...") = SET COMPLET
des recettes du jeu (tous types, secrètes incluses, le serveur en a besoin). Les
ids -> noms FR PROPRES + type via moon-bot-api/items.json LOCAL (même espace
d'ids ; le dump Ancestra a des noms accentués corrompus/tronqués, ex. "Bois de
Ch" pour Chêne -> moon-bot donne le nom propre). Résultat : 2197 recettes, 10453
ingrédients, source ancestra-1.29. Moon-bot n'est PAS interrogé en ligne (bloqué
IP) : on lit le JSON déjà aspiré en local par le projet 1.29. La source
scrapstuff-retro (équipement seul) est retirée de la base (doublon) ;
import_recipes.py reste dispo pour recoupement. COUVERTURE PRIX actuelle : 204
recettes avec coût 100% calculé sur nos prix scrapés (35% des ingrédients
tarifés). Le manque = surtout Minerai + Bois (le bot ne scrape pas encore ces
HDV : _HDV_RESOURCE_CATEGORIES est incomplet) : les scraper ferait passer à ~465
recettes chiffrées. Autres manquants = intermédiaires craftables (Planche, Etoffe,
familiers, équipement) -> à chiffrer par coût de craft récursif plus tard. Rejouer :
python3 backend/import_recipes_ancestra.py.
But long terme : croiser 100% des recettes du jeu (dont secrètes) avec les prix
scrapés pour repérer les crafts rentables (et plus tard le brisage en runes des
bas niveaux). (1) SCHÉMA recipes_schema.sql : tables recipes
(result_name, result_level, result_type, job, is_secret, source) +
recipe_ingredients (ingredient_name, qty), dans la même base que les prix pour
joindre recette<->marché. backend/recipes.py : init_recipes() (appelé au
démarrage), price_index() (nom normalisé -> prix marché x1 = 25e percentile de
listings_v), craft_report() (coût de craft = Σ prix ingrédient × qty, prix de
vente = prix marché du résultat, marge, ROI, couverture des ingrédients). (2)
IMPORT backend/import_recipes.py : source scrapstuff-retro = dump GitHub
raw retro-craft/scrapstuff/fetched_data/items.json (noms FR, format
{item,count}), ÉQUIPEMENT/armes uniquement (~1285 recettes, 6765 ingrédients).
Idempotent par source. (3) API GET /api/craft?only_priced= . (4) FRONT :
onglet « Craft (rentabilité) » (objet, niveau, type, coût de craft, couverture
ingrédients, prix vente, marge, ROI ; tri + recherche + filtre 100% tarifé).
ÉTAT : côté COÛT solide (161 recettes avec coût 100% calculé sur prix réels) ;
côté VENTE encore pauvre (peu de prix d'équipement scrapés + aberrations OCR) ->
fiabilisé quand on scrapera l'HDV équipement. Couverture ingrédients ~42% (montera
avec les consommables, qui utilisent surtout des ressources déjà tarifées). SOURCE
COMPLÈTE (consommables + ressources craftables + recettes SECRÈTES) en cours de
sourcing : moon-bot ÉCARTÉ (VPS bloqué IP). Rejouer l'import :
python3 backend/import_recipes.py.
libellés OCR pollués (« | Ressource », « cr », « Etoffe »...). Base ressources
repartie propre sur les runs du 11-12 septembre (accord Matt : repartir sain).
catégories. (1) PIPELINE : scraping-manuel/dashboard_live.py (LiveSender)
envoie désormais AUSSI Data/dofus_hdv_<jour>.json (records plats du scraper
HDV {category=res_category, name, x1, x10, x100, avg, ts}) vers /api/ingest
en catégorie resources, en les emballant au format attendu
{date, items:[{name, price_x1, price_x10, price_x100, category}]} (date au
jour = 1 point d'historique/jour/lot, idempotent). Avant, seuls dofus_cards et
dofus_items partaient : les ressources HDV n'arrivaient jamais. (2) SCHÉMA :
listings_v expose res_category (MAX). (3) API : GET /api/res-categories
(sous-catégories HDV : Aile, Cuir, Poudre... + nb d'objets distincts) ;
GET /api/items accepte res_cat=<Aile|Cuir|...> (filtre). (4) FRONT : onglet
Ressources affiche une barre de chips de catégories (« Toutes » + une par
sous-catégorie avec compteur), qui filtre le tableau ; l'historique par lot
x1/x10/x100 existait déjà dans la fiche objet. (5) BACKFILL : les libellés de
res_category des données de mars 2026 (OCR pollué : « | Ressource », « cr »,
« poil »...) sont relabellisés vers les catégories propres du run du jour par
jointure sur le nom d'objet (.tmp/hdv_full_run.py), pour unifier tout
l'historique sous des catégories propres. Aucun prix touché. Run complet HDV
validé (628+ objets sur 21 catégories propres).
pnj_talk.open_hdv_buy(nav) : clic sur le PNJ "Vendeur de Ressources de Bonta"
(position fixe HDV_NPC_XY=(783,512) sur la map d'arrivée -32,-57), puis clic
"Acheter" dans le menu contextuel → la fenêtre "Hôtel de vente" s'ouvre.
Contrairement au menu orange du zaapi, ce menu PNJ (gris sur beige) est LISIBLE
à l'OCR : "Acheter" lu score 0.97 au run réel (repli offset fixe
_HDV_ACHETER_OFFSET=(77,168) depuis le clic PNJ conservé en filet). Nouveau
type d'étape de route hdv (route-hdv-open-test.txt, .bat
Scraping-HDV-Open-Test.bat, action agent hdv-open, agent-1.7). DEUX bugs
latents corrigés au passage : (1) route_coords[0] plantait (IndexError) sur
une route SANS coordonnée (pure action hdv) → garde if route_coords and ...
dans worker() ; (2) un .bat piloté par l'agent NE DOIT PAS finir par
pause (l'agent bloque sur _proc.wait() jusqu'à la sortie du .bat → flag
running bloqué, runs suivants refusés "occupé") : le bat de test finit par
exit /b sans pause. Le raccourci pull exige le nom AVEC extension .png.
pnj_talk._search_prefix tape ~65% des lettres du 1er mot (min 4) au lieu du
mot entier ("ressources" → "ressour"), suffisant pour filtrer la liste ; le
matching de la ligne reste sur le nom complet.
des ressources -32,-57, run réel). Correctif clé : le menu contextuel du zaapi
("Zaapi" + "Se faire transporter") est ILLISIBLE à l'OCR (texte sombre sur fond
orange, police stylisée → tesseract ne rend que du bruit, vérifié sur capture
réelle). Solution : use_zaapi (pnj_talk.py) clique désormais "Se faire
transporter" en POSITION FIXE _ZAAPI_TRANSPORT_XY=(1320,331) (machine à
position déterministe à Bonta) quand l'OCR échoue, au lieu d'abandonner l'essai.
Le reste marche par OCR : onglet "Hôtels de vente" (lu via "vente"), champ
"Rechercher", frappe "ressources", double-clic sur la ligne filtrée. NOTE : le
HDV des ressources est à -32,-57 (adjacent à la Milice) ; le log "map hors
route" après téléport est normal (v1 = valider le téléport, pas de scraping).
Prochaine étape : mapper les maps marchandes autour du HDV pour les scraper, et
ajouter zaapi equipement.
pull (agent v1.6) = boucle de debug AUTONOME. python3 lib/dofus_bot.py pull [noms...] : l'agent PC lit des fichiers de
debug_pnj/ (défaut : z*.png récentes) et les POSTe sur /agent/debug du
serveur ; ils atterrissent dans backend/data/agent-debug/ que l'AIOS peut lire
directement. Plus besoin que Matt envoie les captures à la main. Serveur :
endpoint POST /agent/debug?name= (token synchro, ≤12 Mo), pull ajouté à
_AGENT_ACTIONS. Nginx : client_max_body_size 20M sur le vhost dofus-market
(les captures 1920x1080 font ~2,7 Mo, sinon 413). Action hdv-bonta ajoutée aux
DEFAULT_ACTIONS de l'agent (mappe Scraping-HDV-Bonta.bat).
(1) load_route() (scraping_manuel.py) : le mode maps ne se déclenchait que si
la route contenait une COORDONNÉE tuple. Une route de téléports enchaînés sans
coordonnée (ex. zaapi ressources puis zaapi equipement) voyait ses étapes
zaapi: silencieusement supprimées (retombait en mode directions). Corrigé :
la présence d'un zaapi: déclenche aussi le mode maps. La route v1 réelle
(-33,-57 + zaapi ressources) marchait déjà (elle a un tuple) ; le bug était
latent pour l'étape 3 prévue. (2) _norm() (pnj_talk.py) retirait PAS les
accents alors que _type_text() les retire : un onglet lu "Hôtels"/"Équipement"
ne matchait le mot-clé sans accent que par un ratio limite (0.833 < seuil 0.85).
Corrigé : _norm normalise NFKD comme la frappe → inclusion forte. Tests :
6 cas de parsing + 7 cas de matching OCR, tous verts. Non testable en autonome :
le flux end-to-end réel (clic machine + OCR sur le jeu live) exige le jeu ouvert
sur le PC à Bonta -33,-57. Les 2 fichiers corrigés synchronisés au PC.
run accepte désormais args.cli (ex. --retry, --limit N, --only cards|items|resources),
transmis au .bat via %*. Sanitizer _safe_cli() côté agent : allowlist STRICTE
(flag inconnu ou valeur invalide => refus TOTAL), aucune injection possible malgré
shell=True. Helper AIOS : python3 lib/dofus_bot.py run images --retry --limit 40.
DÉCOUVERTE liée : la source d'images moon-bot (https://wiki.moon-bot.io/items/<slug>/)
renvoie 404 sur TOUS les items (équipements compris), depuis le PC qui la joint
bien (DNS réparé côté Matt). Le schéma d'URL / moon_urls.json est périmé : recup-images
ne peut rien récupérer tant qu'on n'a pas la vraie structure d'URL de moon-bot (VPS
bloqué en IP sur moon-bot, impossible à sonder côté serveur). Correctif source posé
dans recup_images.py : une panne réseau (URLError non-HTTP) ne pollue plus le cache
.images-tried.json (seuls les vrais 404/parse le font). NB : recup-images/ n'est
pas encore dans le manifest de sync => correctif pas encore livrable au PC sans zip.
RÉSOLUTION (même jour) : moon-bot abandonné comme source (aucun sprite, juste des
avatars-lettres). Bascule sur backend/populate_icons.py qui peuple les icônes CÔTÉ
SERVEUR depuis l'API DofusDude (art Ankama Dofus 3, api.dofusdu.de joignable du VPS).
567 icônes posées en une passe, 0 échec (items 69% / resources 46% / cards 4%). Plus
aucune dépendance au PC ni à moon-bot pour les images. Front inchangé (déjà branché sur
/static/item-icons/<slug>.png avec repli onerror). Aucun redémarrage nécessaire.
GET /agent/code (token) sert le code de l'agent ; l'agent self_update() (compile-check + backup + relance via .bat
boucle exit 10) se met a jour seul au demarrage et sur sync/update. Actions standard
integrees au code (foire, place-marchande, images) + surcharge agent.json. Action
serveur update ajoutee. But : plus jamais de re-telechargement manuel pour une
nouvelle action/correction (agent.json local token/bot_dir jamais ecrase).
tout de suite en "⏳ Lancement…" (pulsé, jaune) et TOUS les boutons run se bloquent
jusqu'à ce que le PC démarre vraiment l'action (le script met qq s à se lancer) ->
fini les double-clics. agentPending (state client) + renderAgentPanel() séparé du
fetch ; résolu quand running==action / résultat plus récent / timeout 30 s. Bouton en
cours = "● En cours…" (vert). Front pur.
intéressants (inutile pour l'instant, demande Matt). renderDeals/fillDealRows
simplifiés (plus de dealMin/input), toutes les affaires affichées. Front pur.
l'AIOS). En haut du dashboard : pastille en ligne/hors ligne, boutons lancer une
action (foire, place-marchande, foire-rapide, lecteur), Synchroniser, Stop, et un
flux de logs live (refresh 3 s). Endpoints session-authed GET /api/agent/status
+ POST /api/agent/command (le navigateur n'a jamais le token de synchro ; allowlist
d'actions identique). refreshAgent() + #agentpanel dans index.html. Complémentaire
du pilotage via l'AIOS (lib/dofus_bot.py) : dashboard = usage quotidien, AIOS =
orchestration/planifié. Restart appliqué.
pilotage à distance du bot). backend/agent_hub.py (état mémoire thread-safe :
file d'ordres + événements + statut) + routes /agent/next (poll ~2 s), /agent/report,
/agent/command (actions sync|run|stop|ping), /agent/status. L'agent PC
(Dofus-Bot-Nouveau/agent/, zip agent.zip) garde le contact en SORTIE (rien à ouvrir
sur la box), fait la synchro de fichiers ET lance à la demande une action de sa LISTE
BLANCHE (nom -> .bat dans agent.json ; le serveur n'envoie jamais de shell). Logs
remontés à l'AIOS. Helper lib/dofus_bot.py. Testé bout-en-bout sur le VPS.
du jeu a cote de chaque item (tableaux, affaires, fiche) via
itemIcon(name) -> /static/item-icons/<slug>.png (se retire seul si absente,
onerror). Slug partage serveur/front/scraper (sync.slug, retire accents + suffixe
" - No.X" des cartes). Nouveaux endpoints token : /sync/item-names, /sync/icons,
POST /sync/icon (normalise en PNG <=80px via PIL, dossier frontend/item-icons/).
Source = moon-bot (art Retro authentique + cartes), INJOIGNABLE depuis le VPS ->
scraper cote PC Dofus-Bot-Nouveau/recup-images/ (recup_images.py + Recup-Images.bat
+ moon_urls.json 11273 slugs->url + images.json ; zip recup-images.zip). Reprend ou il
en est (only-missing), extrait og:image, dump les pages en echec pour calibrage.
Plomberie testee de bout en bout depuis le VPS (item-names 1613, upload+normalisation
PNG, service /static 200) ; le fetch moon-bot ne marche que sur le PC de Matt. Restart applique.
synchro : backend/sync.py + routes GET /sync/manifest et GET /sync/file
(token DOFUS_SYNC_TOKEN, header X-Sync-Token). Sert les fichiers de
Dofus-Bot-Nouveau/scraping-manuel/ (lecture seule) avec un mode par fichier :
overwrite (code/routes/templates) vs seed (état appris : maps-retro.json etc,
écrit seulement si absent) ; fichiers écrits en run (route-observee.txt,
.live_sent.json) exclus. Client Windows dans Dofus-Bot-Nouveau/bot-sync/
(aios_sync.py stdlib + Sync-Bot.bat + sync.json, zip bot-sync.zip) : poll 10 s,
télécharge les fichiers changés, backup dans .sync-backup\, protège l'état appris.
Testé bout-en-bout (public HTTPS 200, seed non écrasé, code restauré, backup, 401
sans token, path-traversal 404). Restart dofus-market appliqué.
n'a qu'un seul jet (typiquement un Dofus), jetsCell appelle jetDirect(part) qui
ecrit la valeur a cote du nom : "Dofus Pourpre [icone]+26" (icone du stat si connue,
sinon libelle complet). Plus de pastille + bulle inutiles sur une seule ligne. Le
cas multi-jets garde la pastille exo PA/PM + bulle. Front pur, pas de restart.
13 icônes (screenshots), découpées et stockées dans frontend/stat-icons/*.png
(servies via /static/stat-icons/). formatJets affiche l'icône + la valeur
("❤ +273" au lieu de "+273 en vitalité") quand une icône existe, sinon repli texte
(PA bleu / PM vert / gris). Mapping ORDONNÉ STAT_RULES + STAT_ICON_SET (clés
ayant un fichier). Icônes en place : pa, portee, initiative, vitalite, force,
dommages_pct, sagesse, prospection, dommages, coups_critiques, res_terre,
res_neutre, res_eau. MANQUENT (stats présentes en base, pas d'icône) : agilite,
chance, pm, invocations, res_air, intelligence, res_feu, soins. NB : "initiative"
a son icône mais s'affiche encore comme "effet NNN" en base (à décoder côté bot,
EFFECT_NAMES). Front pur (frontend/index.html + PNG), pas de restart.
de la table Vendeurs n'étale plus tout le roll (débordait horizontalement) :
jetsCell(s.stats) = "exo PA"/"exo PM" (ou pastille "jets") + bulle info avec le
roll complet au survol/clic. Badge EXO PA/PM du haut du modal (exoBadge) agrandi
(14px, gras) pour l'avoir en 1 clin d'œil dès le début. Bulles rendues SURVOLABLES :
pont transparent .jtag .jtip::before (8px) qui comble l'écart badge→bulle, donc la
souris peut aller sur la fenêtre de stats sans la fermer (elle reste tant qu'on laisse
la souris dessus). #msellers table{overflow:visible} pour ne pas rogner la bulle.
Front pur, pas de restart.
la colonne Objet n'étale plus tout le roll (ça prenait trop de place) : on affiche
juste le nom + une étiquette compacte "exo PA" (bleu) / "exo PM" (vert) déduite des
jets, ou une pastille "jets" pour les autres équipements. Le roll complet est dans
une BULLE INFO révélée au survol OU au clic (formatJets, PA bleu / PM vert). Front
pur (frontend/index.html) : helper jetsCell(stats), CSS .jtag/.jbadge/.jtip
(hover + focus-within + classe .open au clic, event.stopPropagation pour ne pas
ouvrir la fiche). .deals table{overflow:visible} ajouté car table{overflow:hidden}
(arrondi des coins) rognait la bulle sur les dernières lignes. Pas de restart (front à chaud).
annonce (vendeur + objet + jets + prix) rescannée plusieurs fois (3x/jour, ou N
jours d'affilée) ne compte plus qu'UNE fois dans le prix marché : nouvelle vue
SQL listings_v (GROUP BY category,item_name,qty,seller,stats,price + days_seen
= nb de jours distincts vus, first_seen/last_seen). list_items, dofus,
collection et le baseline de last_run lisent désormais listings_v (sur la
base actuelle : 41936 observations -> 33182 annonces distinctes). dedup_key
inclut maintenant les JETS (sinon 2 exos identiques au même prix/jour se
fusionnaient à tort). (2) list_items expose stale_max (jours max qu'une même
annonce est restée) ; le détail item affiche une colonne "Vu (j)" par annonce
(ambre + ⚠ si >=4 j = ne part pas = prix au-dessus du marché). (3) Nouvel
analytics.disappeared() + /api/sold + onglet "Vendus (est.)" : compare les
deux dernières visites d'un vendeur ; un objet présent avant, absent la dernière
fois = a quitté l'étal = probablement vendu (non garanti : capture réseau/OCR
peut rater un objet). Colonnes : Objet / Vendeur / Vendu~ (dernier prix) /
Marché / vs marché / Présent (jours) / Vu en vente le / Disparu le / Map.
groupé par vendeur, jets perdus). item_detail sellers : GROUP BY (seller,
price, stats, map) + colonne stats, LIMIT 120. Front sellersTable : colonne
"Jets" (roll complet) affichée dès qu'un item porte des stats. Essentiel pour
analyser exos/équipements (chaque exemplaire a un roll différent).
min_clean <= prix actuel <= 110% du min all-time NETTOYÉ (avant : ≥35% sous le
marché). raw_clean_min(prices) -> (min brut, min nettoyé = plus bas prix >=
0.35x médiane, exclut aberrations OCR/fat-finger). Exposé partout : list_items
ajoute min_raw+min_clean ; deals ajoutent min_raw/min_clean/vs_min.
Front : table deals (Prix actuel / Min brut / Min nettoyé / vs min / Marché) +
colonnes "Min brut" & "Min nettoyé" dans la liste d'items. Constantes
FLOOR_CLEAN_K=0.35, DEAL_NEAR_FLOOR=0.10, DEAL_MIN_HISTORY=5.
item_type. Le lecteur réseau envoie le type de chaque item ; colonne item_type TEXT (schema + migration
init_db) stockée par l'ingest. analytics.item_group(type, stats) -> 'archi'
(Pierre d'âme d'Archi-monstre), 'monture' (Familier/Monture/élevage), 'equip'
(armes+armures sans PA/PM), 'exo' (équipement avec +PA ou +PM, regex \bP[AM]\b).
list_items(..., group=) + api_items(group=). Front : 4 onglets = sous-vues
de la catégorie items. Backfill item_type de l'historique via items-retro.json
(41325 lignes). NB exo = proxy "donne PA/PM" (affinable avec une base de jets).
confidence_tier(n) : solide (>=20), moyen (8-19), faible (<8). list_items
renvoie n_offers (offres distinctes = set (vendeur,prix)) + confidence ;
item_detail idem (via nb de vendeurs). Front : cellule "Marché" ambre + ~ +
⚠ quand faible (prix indicatif, peu de relevés, souvent biaisé haut car les
moins chères se vendent vite), tooltip avec le nb d'offres. Ne maquille pas le
prix (reste P25), signale juste sa fiabilité. Réglable CONF_SOLID_N/CONF_MED_N.
analytics.market_price(prices) = 25e percentile après nettoyage des
aberrations OCR basses (< 0.2x médiane). Motif : les annonces boutique sont des
prix DEMANDÉS ; beaucoup restent surpricées sans partir et gonflaient la
moyenne (386 : 7,46M -> 5,0M réel ; Meulou : 527k -> 400k). Percentile =
relatif, marche pour grosses et petites cartes. Réglable :
MARKET_PCTILE (0.25) et MARKET_OCR_LOW_K (0.20). robust_mean conservée
mais plus appelée pour l'affichage marché (list_items, item_detail, collection,
last_run baseline utilisent tous market_price). Copie front mise à jour.
map_id (id de map serveur). But : afficher la map de la dernière run même quand ses coordonnées ne sont pas résolues. schema.sql +
migration idempotente dans db.init_db() (ALTER TABLE ADD COLUMN si absente).
_rows_from_payload/ingest_payload lisent et stockent rec["map_id"] (le
lecteur réseau l'envoie déjà) ; _dedup inclut map_id. analytics._fmt_map
renvoie "id X" quand les coords sont nulles ; last_run() sélectionne
map_id, la liste maps et la colonne Map des deals utilisent ce repli.
Front : panneau dernière run affiche la liste des maps (chips ; les id X
non résolus sont surlignés ambre). NB : les runs déjà ingérées avant ce
changement n'ont pas de map_id (perdu à l'ingestion) : effet à partir de la
run suivante avec le bot à jour.
carte -> rareté récupéré depuis l'encyclopédie TTG (endpoint
https://dofusretrotools.com/api/ttg-data, 464 cartes), écrit dans
frontend/ttg_rarity.json ({"386":"ultime",...}, servi via /static/).
4 paliers officiels : Commune (166) < Rare (155) < Épique (94) < Ultime (49).
Front : rarity(name) extrait le "No.XXX" du nom et lit la rareté réelle
(avant : seuils de prix). loadRarityMap() chargé au boot. Classes CSS
tr.t0..t3 (Commune=rien, Rare=bleu, Épique=violet+pulse, Ultime=holo doré
animé + ★). Légende/filtre ramenés à 4 paliers. Join vérifié : 465/465 cartes
réelles matchées ; paquets + bruit OCR restent neutres. Pas de restart (front).
6 paliers nommés par prix marché : Commun <75k, Peu commun <200k, Rare <600k,
Épique <1,5M, Légendaire <4M, Mythique >=4M (seuils calés sur ~553 cartes).
Effets CROISSANTS façon Yugioh (CSS tr.r0..r5) : commun = nom blanc sans
rien -> peu commun/rare/épique = nom coloré + liseré + halo -> légendaire =
dégradé animé or + ✦ -> mythique = holographique arc-en-ciel animé + ★ +
liseré qui cycle. Nouvelle fonction JS rarity() = paliers (avant : rampe log
continue). Légende + filtre par rareté : barre de puces cliquables #rarbar
(chaque puce = swatch + nom + compte, filtre la liste ; state.rar,
window.setRar). prefers-reduced-motion géré. Front pur, pas de restart.
rarity() renvoie commun:t<0.25 ; dans loadTable/rowHTML, une carte commune est traitée
comme sans rareté (nom en blanc simple, ni bord ni fond gradué, comme avant).
Les effets (bord, tint, nom coloré, animations légendaire) restent sur les
paliers peu commun -> légendaire. Front pur, pas de restart.
affaires : "Min all-time" (prix minimum jamais constaté pour l'objet, hors
aberrations OCR < 10% du marché) et "vs min" (écart de l'offre par rapport à ce
minimum ; = min en doré si l'offre EST le meilleur prix jamais vu, sinon
+X%). Back : last_run() calcule floor (min sur historique + run) et
vs_min par deal. Front : colonnes dans renderLastRun, style .dl-atmin.
Restart service pour le back.
Front-only, pas de champ rareté en base : proxy = prix marché (robust_mean)
sur une échelle LOG continue. Helper JS rarity(price) (rampe commun gris →
peu commun vert → rare bleu → épique violet → légendaire or ; RAR_PMIN=5k,
RAR_PMAX=2M). Chaque ligne : bord gauche + fond dégradés (--rc/--rct) +
nom coloré. Palier LÉGENDAIRE (≥ RAR_LEGENDARY=2M, ~top 6%) : dégradé animé
(rarflow), halo doré pulsé (rarpulse), préfixe ✦, respecte
prefers-reduced-motion. Gaté sur state.cat in (cards,packs) (pas items/
ressources : prix ≠ rareté). Rendu dans loadTable (rowHTML), CSS tr.rar/
tr.legendary. Ajuster les seuils = constantes en tête de <script>.
analytics.last_run() + GET /api/last-run + div#lastrun (avant les KPIs),
rendu par renderLastRun() (appelé au boot ET dans refreshLive, refresh 30 s).
Deux parties : (1) résumé statistique de la dernière run (période + durée,
relevés, objets distincts, vendeurs, maps, Dofus, répartition par catégorie) ;
(2) "Produits intéressants" = bonnes affaires détectées automatiquement : offres
de la run nettement sous leur prix marché historique (robust_mean calculée hors
run, remise 35–90%, min 5 relevés d'historique, gain ≥ 2000 k), triées par gain,
cliquables (ouvre la fiche objet). Run reconstituée par rupture temporelle sur
ingested_at (le source_file du direct change chaque minute donc inutilisable).
Helper front DTFR (date+heure locale). Restart service pour le back (front à chaud).
d'inventaire parchemin), GÉNÉRÉS via image-gen (Nano Banana, gemini-3-pro-image,
hors quota Claude), recadrés + réduits 128px, servis depuis
frontend/icons/*.png via /static/icons/. ICONS = <img class="tabic">.
CSS .tab .tabic 26px arrondi + drop-shadow. Front-only. Sprites : carte
(blop+étoile), paquet ficelé, épée gemme, ŒUF de Dofus orangé, émeraude, kamas+
flèche. Régénérables via le même script si besoin d'ajuster le style.
analytics.collection() + /api/collection : ne compte que les vraies cartes numérotées ("Nom" - No.X,
regex _REAL_CARD, exclut paquets + charabia OCR sans No). Deux totaux :
total_floor (le moins cher de chaque carte en ignorant les prix < 10% du
marché = anti-OCR bas) et total_market (somme des prix marché élagués) ; plus
complétude (num_min/max, missing) et top 15 cartes coûteuses. Front : bannière
cliquable sous les KPIs (helper KBIG compact M/Mrd) + modale détail (clic sur
une carte -> fiche objet). Données : 464 cartes No.1–464 (2 manquantes : 323,
337), plancher ~259 M, marché ~366 M kamas.
DATEFRcôté front). Couvre : barre "Dernière donnée", colonne "Vu le" (objets), liste
vendeurs, axe X du graphe, offres Dofus, repli date du direct. Stockage DB
inchangé (ISO AAAA-MM-JJ), conversion à l'affichage seulement. Front-only, pas
de restart.
(graphe + stats min/max + liste vendeurs), plus seulement les vendeurs.
item_detail(..., days) applique le cutoff date('now','-N days') aussi à la
série du graphe ; /api/item?period=N. Front : sélecteur période (7j/30j/90j/
Tout) remonté au-dessus du graphe, renderItemModal(d) reconstruit tout au
changement. Tooltip Chart.js : interaction mode:'index', intersect:false +
callbacks formatés en kamas, pointHoverRadius sur les deux courbes -> le prix
marché (courbe médiane) se lit au survol n'importe où sur l'axe.
bruités : 68% vus 1 seul jour, 42% de noms douteux). Route /api/sellers et
analytics.sellers() supprimées. L'info vendeur actionnable reste dans la
fiche objet (liste "Vendeurs (meilleurs prix)" filtrable par période).
fiche objet. item_detail(..., seller_days) + /api/item?period=N : ne garde
que les relevés des N derniers jours (obs_date >= date('now','-N days')) et
RECALCULE le meilleur prix sur la période (pas un simple masquage). Front :
chips 7j / 30j / 90j / Tout au-dessus du tableau, défaut 30j (le vieux stock
de mars est masqué par défaut), re-fetch qui remplace juste le tableau.
list_items() prend kind(monsters|packs|all) filtrant sur le préfixe "Paquet de cartes" ; route
/api/items expose kind. Front : onglet "Paquets" (category=cards&kind=packs)
et l'onglet "Cartes" ne montre plus que les monstres (kind=monsters), zéro
doublon. 49 paquets / 503 cartes monstres.
analytics.robust_mean() : ancre sur la médiane, ne garde que la bande
[0,35x ; 2,5x] la médiane, moyenne le reste (constantes ROBUST_LOW_K/
ROBUST_HIGH_K). Vire les cartes complètement surpricées (un vendeur à 20-50x
fausse une moyenne classique) et les aberrations OCR basses, sans se laisser
tirer. Appliqué à : table objets (market_price, colonne "Marché"), séries du
détail (market, ligne "Prix marché" du graphe), paliers de jet Dofus
(market). Effet mesuré : Bouftou No.81 34 416 -> 8 382 (médiane 8 999) ; un
objet déjà propre (Le Chouque) reste inchangé. Restart service applique le back
(le front est re-servi à chaud).
analytics.dofus() + route GET /api/dofus : regroupe les Dofus (item_name LIKE 'Dofus %') par type puis
par valeur de jet (lue dans stats), agrege meilleur prix / moyen / max /
nb d'offres par palier. Front index.html : onglet "Dofus" avec selecteur de
type (chips) et echelle VERTICALE par jet (une barre = meilleur prix du palier,
largeur proportionnelle au prix max du type), tri par jet ou par prix, clic sur
un palier = detail des offres (vendeur/prix/position/date). But : comparer d'un
coup d'oeil quel jet vaut combien. Aucun em-dash ajoute. Restart service applique.
integrations/local-agent/: pont Python entre le bot Windows et /api/ingest. Lit les JSON produits par
le scraper (aucune modif du bot), envoi incrémental idempotent, mode watch.
Testé de bout en bout contre l'URL publique (22 fichiers, 401 sur mauvais
token, 0 doublon sur renvoi). Creds Google legacy (credentials.json,
token.json) sortis de /root/workspace/Data/ vers un stockage interne
verrouillé (plus de secret Google dans le workspace).
(31 113 observations: 514 cartes, 803 ressources, 11 items, 1808 vendeurs, du
2026-03-01 au 2026-03-18). Auth par mot de passe haché + token d'ingestion.
Publiée sur https://dofus-market.panelbay.com.
analytics.summary() expose last_ingest / today_ingest (table ingestions, source agent) ; index.html affiche "Direct :
dernier envoi il y a X min (+N)" (vert < 10 min) avec refresh 30 s. Cote bot,
dashboard_live.py envoie chaque vendeur pendant le run (bloc 1 du projet
"app Dofus" : live data, puis app tray, puis deblocage IA). Pourquoi : voir les
prix arriver pendant le scraping sans copier de fichiers.
Deux marchands (Exodur, Satelaidy) vendaient exactement 1 x Cape Dragarmure a 319999 ; le
lecteur reconnaissait le tour complet par l'empreinte de contenu et coupait la passe a la
28e boutique sur 30 (Pantougle, Xx-x-x-Xx-[B12] jamais lus, 3 passes identiques, handover,
5 maps de la route non scrapees). lecteur_reseau.py (lecteur-2026-09-13b) : tour complet =
meme id marchand rouvert ; empreinte de contenu seulement si l'id est inconnu ; journal
"meme contenu mais marchand different, je continue". Verifie au rejeu de la copie brute
run_20260912-222331.pcap (avant : wrap sur Satelaidy ; apres : wrap au vrai retour sur Exodur).
Regle N8 dans REGLES-BOT-PROPOSITION.md.
lecteur_reseau.py (lecteur-2026-09-13c) : pass_seen/pass_opens/wrap_reason ; une passe
s'arrete quand 'Suivant' rouvre une boutique deja ouverte dans la passe (meme id), quand tous
les marchands annonces sont lus, ou quand le cycle est epuise (ouvertures > annonces + 1).
L'empreinte de contenu ne sert que si l'id est inconnu. scraping_manuel.py
(moteur-2026-09-13b) : boutiques lues remises a zero par MAP (plus par passe), reset
pass_seen a chaque passe, passe sans progres (memes manquants) = passe-la-main direct,
message [MANUEL] Fin de passe (reseau) : <raison>. Rejeu run_20260912-222331.pcap : maps
6/6 closes au 6e marchand, (0,1) sans faux tour complet. Couche 2 (clic cible sur la case des
manquants) non faite.
227 lignes / 26 vendeurs "marchand -N" du 12/09 supprimees (doublons verifies par contenu exact
des releves nommes de 22:25 ; sauvegarde dofus-market.db.bak-20260912-marchandN). (b)
lecteur_reseau.py (lecteur-2026-09-13d) : memoire id->nom par map Data/marchands-ids.json
(_remember_merchants sur liste serveur, preload_merchants(coords) quand la liste n'est jamais
arrivee, nom retrouve a l'ECK4, merchants_from_cache, map_id_for) ; idle_reopen 8 -> 15 s.
(c) scraping_manuel.py (moteur-2026-09-13c) : preload avant scraping, liste memorisee + tour
complet = COMPLETE meme avec absents, plafond scraper 60 sans liste / annonces+3 avec. (d)
route-place-marchande.txt : (1,-1) n'a pas de sortie gauche -> 1,-2 et 0,-2 ajoutees (6
marchands chacune), 10 maps. Agent 2.8 : action place-marchande-ici
(Scraping-Place-Marchande-Ici.bat, scrape la map de depart). Tests hors jeu OK (cache +
rejeu pcap).
analytics.disappeared faisait ~7000 requetes par vendeur sans index et regroupait toute la table (vue listings_v)
pour chacun des 4652 objets disparus. Ajout des index idx_obs_seller_date
(seller, obs_date) et idx_obs_cat_item_qty (category, item_name, qty) dans schema.sql
(crees sur la base live), prix marche memorise par objet dans l'appel, resultat cache tant
que (COUNT(*), MAX(ingested_at)) ne change pas (_SOLD_CACHE, champ cached). Service
redemarre. Question de Matt (lag PC) : la page ne fait qu'un rafraichissement de 0,4 Ko / 30 s ;
la charge PC vient du bot en run, pas du dashboard.
zaap. Matt a change le zaap de rappel (Astrub au lieu de la Foire). scraping_manuel.py (moteur-2026-09-13e) :
ZAAP_RAPPEL=(4,-19) par defaut pour rappel, nouvelle etape zaap <recherche> @ x,y (parsing
zaap:<texte>|x,y, branche de boucle, arrivee verifiee au reseau puis marche vers l'etape
suivante). pnj_talk.use_zaap (clic zaap, Utiliser par OCR/offset, recherche, ligne par OCR ou 1re
ligne, DOUBLE-clic, confirmation reseau), constantes mesurees sur 2 enregistrements (action
record, sessions zaap-astrub-foire_090943 et zaap-astrub-2_091234, PC Desktop\\Data\\records).
Foire = "Plaines Rocheuses" [-14,-47]. route-foire-auto.txt : rappel 4,-19 puis `zaap plaines ro
@ -14,-47. Agent : action borderless` (Borderless-Auto.bat) pour forcer le 1920x1080 a distance.
Pas encore testee en run reelle.
Symptome (Matt) : dans Craft (rentabilite), cliquer un objet ouvrait une fiche vide
(pas d'historique/vendeurs) malgre un scrap complet des HDV. Cause : le clic appelait
openItem(result_name) SANS categorie, donc le front envoyait /api/item?category=craft ;
or item_detail filtre STRICTEMENT sur category et il n'existe AUCUNE ligne
category='craft' en base (les objets sont en items / resources) -> 0 resultat pour
toutes les recettes. Correctif : recipes.craft_report expose sell_category (categorie
obs reelle de l'objet resultat, tiree de price_index().cats, priorite items>resources>cards) ;
frontend/index.html (drawCraft) passe r.sell_category a openItem (repli 'items').
Verifie : 1186 recettes, 954 items + 232 resources, historique + vendeurs remontent
(avant : 0 pt / 0 vendeur). Service redemarre. Cote navigateur : rafraichir la page (Ctrl+Maj+R).