Files
ForexBot2/webapp/api.php
T
Harwinder Sekhon 85a5ba09b8 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
2026-07-17 11:41:03 -04:00

80 lines
2.5 KiB
PHP

<?php
// Public read-only API for the dashboard page.
// GET api.php → latest price + latest report per timeframe
// GET api.php?what=price → latest price only (cheap 2-second poll)
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'] !== 'GET') {
fail(405, 'GET only');
}
$cfg = require __DIR__ . '/config.php';
$symbol = (string)($_GET['symbol'] ?? $cfg['symbols'][0]);
if (!in_array($symbol, $cfg['symbols'], true)) {
fail(400, 'unknown symbol');
}
$what = (string)($_GET['what'] ?? 'all');
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');
}
$out = ['ok' => true, 'server_time' => gmdate('Y-m-d H:i:s')];
try {
$st = $pdo->prepare(
'SELECT price, prev_price, tick_at,
TIMESTAMPDIFF(SECOND, tick_at, UTC_TIMESTAMP()) AS age_sec
FROM live_price WHERE symbol = :s'
);
$st->execute([':s' => $symbol]);
$row = $st->fetch(PDO::FETCH_ASSOC);
$out['price'] = $row === false ? null : [
'price' => (float)$row['price'],
'prev_price' => $row['prev_price'] === null ? null : (float)$row['prev_price'],
'tick_at' => $row['tick_at'],
'age_sec' => (int)$row['age_sec'],
];
if ($what !== 'price') {
$st = $pdo->prepare(
'SELECT r.timeframe, r.payload, r.updated_at,
TIMESTAMPDIFF(SECOND, r.updated_at, UTC_TIMESTAMP()) AS age_sec
FROM signal_reports r
JOIN (SELECT timeframe, MAX(id) AS mid
FROM signal_reports WHERE symbol = :s
GROUP BY timeframe) x ON x.mid = r.id'
);
$st->execute([':s' => $symbol]);
$reports = [];
foreach ($st->fetchAll(PDO::FETCH_ASSOC) as $r) {
$p = json_decode((string)$r['payload'], true);
if (is_array($p)) {
$p['stored_at'] = $r['updated_at'];
$p['age_sec'] = (int)$r['age_sec'];
$reports[$r['timeframe']] = $p;
}
}
$out['reports'] = $reports;
}
} catch (PDOException $e) {
error_log('api.php: ' . $e->getMessage());
fail(500, 'db error');
}
echo json_encode($out, JSON_UNESCAPED_UNICODE);