Add LAMP web dashboard: bot→MySQL publisher, PHP API, numbers-only page

- webapp/: deployable LAMP app for /var/www/ — ingest.php (token-gated
  writes), api.php (public read-only), schema.sql (live_price +
  signal_reports), vanilla-JS dashboard with plain-English indicator
  readings, .htaccess, deploy README
- web_publisher.py: fire-and-forget publisher (bounded queue + daemon
  thread) with plain-English payload builder; public API contractually
  never raises; disabled unless WEBAPP_INGEST_URL/TOKEN are set
- telegram_bot.py: publish hooks (tick in WS loop, report in
  _update_level_cache); _candle_loop per-iteration guard so a failing
  scan can never kill an auto-scan loop; quiet heartbeat teardown
- test_all.py: Module 13 — 6 resilience tests (never-raise contract,
  worker survival, candle-loop survival); suite 189/189
- .gitignore: webapp/config.php (server-side secrets)

Verified end-to-end on a scratch MariaDB 10.11 + PHP 8.3 built-in
server incl. headless-Firefox render of the live page.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KYNEHBuwwy6sgRM9f9sPx5
This commit is contained in:
Harwinder Sekhon
2026-07-17 11:41:03 -04:00
parent 85b7567677
commit 85a5ba09b8
13 changed files with 1437 additions and 4 deletions
+20 -4
View File
@@ -68,6 +68,7 @@ from config import (
from data_provider import ForexDataProvider
from pair_signals import PAIR_PRODUCERS
from signal_aggregator import SignalAggregator, SignalReport
import web_publisher
# ── Force UTF-8 on Windows ────────────────────────────────────────────
if hasattr(sys.stdout, "reconfigure"):
@@ -619,6 +620,9 @@ _breach_tasks: Set[asyncio.Task] = set()
def _update_level_cache(reports: List[SignalReport]) -> None:
now = time.time()
for r in reports:
# Every report-producing path (boot, auto-scan, manual scan,
# breach rescan) flows through here — piggyback the web publish.
web_publisher.publish_report(r)
if r.primary_analysis and r.primary_analysis.key_levels:
levels = {
k: v for k, v in r.primary_analysis.key_levels.items()
@@ -701,7 +705,12 @@ async def _ws_heartbeat(ws) -> None:
alive through quiet periods (weekends, low-liquidity hours)."""
while True:
await asyncio.sleep(10)
await ws.send(json.dumps({"action": "heartbeat"}))
try:
await ws.send(json.dumps({"action": "heartbeat"}))
except Exception:
# Connection is going down — the monitor loop reconnects;
# exiting quietly avoids an unretrieved-task-exception warning.
return
async def _ws_monitor(bot) -> None:
@@ -750,6 +759,8 @@ async def _ws_monitor(bot) -> None:
continue
breached = _check_level_breach(pair_symbol, price)
web_publisher.publish_tick(
pair_symbol, price, _last_prices.get(pair_symbol))
_last_prices[pair_symbol] = price
_last_price_ts[pair_symbol] = time.time()
@@ -785,9 +796,14 @@ async def _candle_loop(bot, tf: Timeframe, interval_minutes: int) -> None:
if not _feed_active():
break
logger.info(f"{label} auto-scan: firing")
await _scan_timeframe(
bot, tf, AUTOSCAN_CONFIRMATIONS[tf]
)
try:
await _scan_timeframe(
bot, tf, AUTOSCAN_CONFIRMATIONS[tf]
)
except Exception:
# One bad scan (data outage, web publish, Telegram error)
# must never kill the loop — retry at the next candle.
logger.exception(f"{label} auto-scan iteration failed — will retry at next boundary")
except asyncio.CancelledError:
pass
finally: