85a5ba09b8
- 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
121 lines
3.9 KiB
PHP
121 lines
3.9 KiB
PHP
<?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');
|
|
}
|