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:
+20
-4
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user