- npm start / watch:telegram: add --env-file=env so TELEGRAM_* and HTTPS_PROXY from nv reach process.env
- watcher: import CONFIG.candleWindowMinutes from src/config.js so the per-window latch stays in sync with the bot
- watcher: replace prevSide/cooldown latch with lastSentWindowId keyed on floor(ts/WINDOW_MS); one alert per window, NO_TRADE rows no longer reset state
The bot writes recommendation as "side:phase:strength" (e.g. "UP:MID:STRONG")
without an "ENTER:" prefix per src/index.js:721. The watcher checked
startsWith("ENTER"), so all real signals were treated as NO_TRADE.
Only manual test rows (which I had user write with ENTER: prefix for clarity)
ever triggered. 7,460 real signals over 8 hours were silently ignored.
Detection now uses rec.includes(":") as the ENTER marker, parses side from
split[0] and phase/strength from [1]/[2].
Two fs.watch callbacks can fire for the same row within the 300ms debounce
window. Both enter the setTimeout, both read prevSide=null before either
sets it, both call send(). Result: duplicate push within ~20ms.
Fix: track lastSentTs. Same row timestamp = skip. Only a NEW row from
the bot (or a new manual append) can trigger another push.
Also log the slug before each push so we can diagnose the missing link.