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
+120
View File
@@ -0,0 +1,120 @@
<?php
// Ingest endpoint — the bot POSTs ticks and signal reports here.
// Auth: shared token in the JSON body, compared with hash_equals().
declare(strict_types=1);
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-store');
function fail(int $code, string $msg): never {
http_response_code($code);
echo json_encode(['ok' => false, 'error' => $msg]);
exit;
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
fail(405, 'POST only');
}
$cfg = require __DIR__ . '/config.php';
$raw = file_get_contents('php://input');
if ($raw === false || strlen($raw) > 512 * 1024) {
fail(400, 'bad body');
}
$msg = json_decode($raw, true);
if (!is_array($msg)) {
fail(400, 'invalid JSON');
}
$token = (string)($msg['token'] ?? '');
if ($token === '' || !hash_equals((string)$cfg['ingest_token'], $token)) {
fail(403, 'bad token');
}
$symbol = (string)($msg['symbol'] ?? '');
if ($symbol === '' || !in_array($symbol, $cfg['symbols'], true)) {
fail(400, 'unknown symbol');
}
try {
$pdo = new PDO($cfg['db_dsn'], $cfg['db_user'], $cfg['db_pass'], [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);
} catch (PDOException $e) {
fail(500, 'db unavailable');
}
$type = (string)($msg['type'] ?? '');
try {
if ($type === 'tick') {
$price = filter_var($msg['price'] ?? null, FILTER_VALIDATE_FLOAT);
if ($price === false || $price === null) {
fail(400, 'bad price');
}
$prev = filter_var($msg['prev'] ?? null, FILTER_VALIDATE_FLOAT);
$tickAt = (string)($msg['tick_at'] ?? gmdate('Y-m-d H:i:s'));
$st = $pdo->prepare(
'INSERT INTO live_price (symbol, price, prev_price, tick_at)
VALUES (:s, :p, :pp, :t)
ON DUPLICATE KEY UPDATE
prev_price = VALUES(prev_price),
price = VALUES(price),
tick_at = VALUES(tick_at)'
);
$st->execute([
':s' => $symbol,
':p' => $price,
':pp' => ($prev === false || $prev === null) ? null : $prev,
':t' => substr($tickAt, 0, 19),
]);
echo json_encode(['ok' => true]);
exit;
}
if ($type === 'report') {
$tf = (string)($msg['timeframe'] ?? '');
$barTs = substr((string)($msg['bar_ts'] ?? ''), 0, 19);
$signal = (string)($msg['signal'] ?? '');
$quality = (string)($msg['quality'] ?? 'F');
$score = filter_var($msg['score'] ?? null, FILTER_VALIDATE_FLOAT);
$conf = filter_var($msg['confidence'] ?? null, FILTER_VALIDATE_FLOAT);
$payload = $msg['payload'] ?? null;
if ($tf === '' || strlen($tf) > 8 || $barTs === '' || $signal === ''
|| $score === false || $score === null
|| $conf === false || $conf === null
|| !is_array($payload)) {
fail(400, 'bad report fields');
}
$st = $pdo->prepare(
'INSERT INTO signal_reports
(symbol, timeframe, bar_ts, signal_name, quality,
score, confidence, payload)
VALUES (:s, :tf, :bt, :sig, :q, :sc, :c, :pl)
ON DUPLICATE KEY UPDATE
signal_name = VALUES(signal_name),
quality = VALUES(quality),
score = VALUES(score),
confidence = VALUES(confidence),
payload = VALUES(payload)'
);
$st->execute([
':s' => $symbol,
':tf' => $tf,
':bt' => $barTs,
':sig' => substr($signal, 0, 12),
':q' => substr($quality, 0, 2),
':sc' => $score,
':c' => $conf,
':pl' => json_encode($payload, JSON_UNESCAPED_UNICODE),
]);
echo json_encode(['ok' => true]);
exit;
}
fail(400, 'unknown type');
} catch (PDOException $e) {
error_log('ingest.php: ' . $e->getMessage());
fail(500, 'db error');
}