<?php
/**
* BTC 자금 흐름 분석 시스템 (V5.15 - 스코어보드 색상 직관성 강화)
* - 구조 유지, 코드 유지, 코드 구조 수정 절대 금지
* - Global Score 색상 보정: 양수(+) 녹색 / 음수(-) 적색 적용
* - HUD 폰트 13px 및 상태 문구 정자체 유지
* - collected_ms 기반 인덱스 쿼리 및 런타임 Null Guard 유지
*/
set_time_limit(30);
error_reporting(E_ALL & ~E_NOTICE);
ini_set('display_errors', 1);
require_once("/home/www/DB/db_upbit.php");
if (!isset($db_upbit)) {
die("데이터베이스 연결 실패");
}
$target_market = 'KRW-BTC';
$intervals = [
1 => ['label' => '1분봉 기준', 'table' => 'daemon_upbit_coin_1m'],
5 => ['label' => '5분봉 기준', 'table' => 'daemon_upbit_coin_1m'],
15 => ['label' => '15분봉 기준', 'table' => 'daemon_upbit_coin_1m'],
30 => ['label' => '30분봉 기준', 'table' => 'daemon_upbit_coin_1m'],
60 => ['label' => '1시간 기준', 'table' => 'daemon_upbit_coin_1m'],
240 => ['label' => '4시간 기준', 'table' => 'daemon_upbit_coin_1m'],
480 => ['label' => '8시간 기준', 'table' => 'daemon_upbit_coin_5m'],
720 => ['label' => '12시간 기준', 'table' => 'daemon_upbit_coin_5m'],
1440 => ['label' => '24시간 기준', 'table' => 'daemon_upbit_coin_5m'],
10080 => ['label' => '주봉(7D) 기준', 'table' => 'daemon_upbit_coin_24h'],
43200 => ['label' => '월봉(30D) 기준', 'table' => 'daemon_upbit_coin_24h']
];
$WEIGHTS = [
'1분봉 기준' => 6,
'5분봉 기준' => 10,
'15분봉 기준' => 8,
'30분봉 기준' => 5,
'1시간 기준' => 18,
'4시간 기준' => 22,
'8시간 기준' => 5,
'12시간 기준' => 5,
'24시간 기준' => 15,
'주봉(7D) 기준' => 2,
'월봉(30D) 기준' => 2,
];
$short_labels = [
1=>"1m", 5=>"5m", 15=>"15m", 30=>"30m", 60=>"1h", 240=>"4h", 480=>"8h", 720=>"12h", 1440=>"24h", 10080=>"1w", 43200=>"1M"
];
function get_aggregated_data($db, $target_market, $intervals) {
$now_ms = round(microtime(true) * 1000);
$cut24_ms = $now_ms - (24 * 60 * 60 * 1000);
$sql_24h = "SELECT SUM(tr_trade_price * tr_trade_volume) AS amt_24h FROM daemon_upbit_coin_24h WHERE market = ? AND collected_ms >= ?";
$stmt_24h = $db->prepare($sql_24h);
$stmt_24h->execute([$target_market, $cut24_ms]);
$amt_24h_total = (float)($stmt_24h->fetch(PDO::FETCH_ASSOC)['amt_24h'] ?? 0);
$results = [];
foreach ($intervals as $n_min => $info) {
$n_min = (int)$n_min;
$cut_ms = $now_ms - ($n_min * 60 * 1000);
$sql = "SELECT MAX(korean_name) AS kname, SUM(tr_trade_price * tr_trade_volume) AS amt, SUM(tr_trade_volume) AS vol, SUM(CASE WHEN tr_ask_bid='BID' THEN tr_trade_price * tr_trade_volume ELSE 0 END) AS buy_amt, SUM(CASE WHEN tr_ask_bid='ASK' THEN tr_trade_price * tr_trade_volume ELSE 0 END) AS sell_amt, AVG(signed_change_rate) AS avg_rate, MAX(highest_52_week_price) AS h52, MAX(lowest_52_week_price) AS l52, MAX(trade_price) AS cp FROM {$info['table']} WHERE market = ? AND collected_ms >= ?";
$stmt = $db->prepare($sql);
$stmt->execute([$target_market, $cut_ms]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
$total_amt = (float)($row['amt'] ?? 0); $buy_amt = (float)($row['buy_amt'] ?? 0); $sell_amt = (float)($row['sell_amt'] ?? 0); $net_flow = $buy_amt - $sell_amt;
$h52 = (float)($row['h52'] ?? 0); $l52 = (float)($row['l52'] ?? 0); $cp = (float)($row['cp'] ?? 0); $diff_range = $h52 - $l52; $pos_52w = ($diff_range > 0) ? (($cp - $l52) / $diff_range) * 100 : 0;
$results[$n_min] = [
'label' => $info['label'], 'total_amt' => $total_amt, 'volume' => (float)($row['vol'] ?? 0), 'net_flow' => $net_flow, 'buy_ratio' => ($buy_amt + $sell_amt > 0) ? ($buy_amt / ($buy_amt + $sell_amt)) * 100 : 0, 'ratio_24h' => ($amt_24h_total > 0) ? ($total_amt / $amt_24h_total) * 100 : 0, 'imbalance' => ($total_amt > 0) ? ($net_flow / $total_amt) * 100 : 0, 'avg_rate' => ((float)($row['avg_rate'] ?? 0)) * 100, 'pos_52w' => $pos_52w, 'h52' => $h52, 'l52' => $l52, 'is_whale' => ($net_flow > 0 && $pos_52w < 15), 'is_pump' => (($total_amt > 0 ? ($net_flow / $total_amt) * 100 : 0) > 30)
];
}
return $results;
}
if (isset($_GET['mode']) && $_GET['mode'] === 'update') {
header('Content-Type: application/json; charset=utf-8');
$payload = ['sync_time' => date("Y-m-d H:i:s"), 'data' => get_aggregated_data($db_upbit, $target_market, $intervals)];
echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
}
$initial_data = get_aggregated_data($db_upbit, $target_market, $intervals);
$global_weight_sum = 0; $global_score_sum = 0.0; $initial_details_html = '';
foreach ($initial_data as $n_min => $r) {
$w = (int)($WEIGHTS[$r['label']] ?? 0);
if ($w <= 0) continue;
$lbl = $short_labels[$n_min] ?? $n_min;
$strength = ($r['total_amt'] > 0) ? $r['net_flow'] / $r['total_amt'] : 0.0;
$strength = max(-1, min(1, $strength)); $weighted = $strength * $w;
$global_score_sum += $weighted; $global_weight_sum += $w;
$color = $strength > 0 ? 'text-emerald-500' : ($strength < 0 ? 'text-rose-500' : 'text-slate-500');
$initial_details_html .= "<span class='inline-block mr-2'><span class='text-slate-600'>{$lbl}:</span><span class='{$color}'>".sprintf("%+.2f", $strength)."</span><span class='text-slate-700'>×{$w}=</span><span class='{$color}'>".sprintf("%+.2f", $weighted)."</span></span>";
}
$GLOBAL_SCORE = ($global_weight_sum > 0) ? ($global_score_sum / $global_weight_sum) : 0.0;
$GLOBAL_SCORE_PCT = $GLOBAL_SCORE * 100.0;
$initial_summary_html = "<span class='text-sky-500 px-2'>[ <i class='fa-solid fa-layer-group mr-1'></i>가중합:".sprintf("%+.2f", $global_score_sum)." / 가중치합:{$global_weight_sum} = ".sprintf("%+.4f", $GLOBAL_SCORE)." ]</span>";
?>
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BTC 자금 흐름 분석 v5.15</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700;900&family=JetBrains+Mono:wght@500;800&display=swap" rel="stylesheet">
<style>
:root { --terminal-bg: #020617; --card-bg: rgba(15, 23, 42, 0.8); --neon-blue: #38bdf8; --neon-emerald: #10b981; --neon-rose: #f43f5e; }
body { background: var(--terminal-bg); color: #f1f5f9; font-family: 'Inter', sans-serif; margin: 0; padding: 0; overflow-x: hidden; }
.wrapper { width: 100%; padding: 40px 50px; box-sizing: border-box; opacity: 0; transform: translateY(20px); transition: all 0.8s ease-out; }
.wrapper.loaded { opacity: 1; transform: translateY(0); }
.font-mono { font-family: 'JetBrains Mono', monospace; }
.whale-bg { background: rgba(16, 185, 129, 0.1) !important; border-left: 4px solid var(--neon-emerald); }
.pump-bg { background: rgba(244, 63, 94, 0.1) !important; border-left: 4px solid var(--neon-rose); }
.update-flash { animation: flash 0.8s ease-out; }
@keyframes flash { from { background: rgba(56, 189, 248, 0.2); } to { background: transparent; } }
#preloader { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: #020617; z-index: 9999; display: flex; flex-direction: column; align-items: center; justify-content: center; transition: opacity 0.5s ease; }
.loader-box { width: 40px; height: 40px; border: 3px solid rgba(56, 189, 248, 0.1); border-top-color: var(--neon-blue); border-radius: 50%; animation: spin 1s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
.glass { background: var(--card-bg); backdrop-filter: blur(10px); border: 1px solid rgba(255,255,255,0.05); }
.scoreboard-glow { box-shadow: 0 0 30px rgba(56, 189, 248, 0.1); }
#score-hud-panel { background: rgba(0, 0, 0, 0.4); border: 1px solid rgba(56, 189, 248, 0.15); border-radius: 12px; padding: 12px 18px; min-width: 450px; max-width: 900px; box-shadow: inset 0 0 10px rgba(0,0,0,0.5); }
.hud-label { color: #475569; font-size: 9px; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 4px; display: block; font-weight: 900; }
#score-details-content { font-size: 13px; line-height: 1.6; }
#score-desc { font-style: normal !important; text-transform: uppercase; }
</style>
</head>
<body>
<div id="preloader">
<div class="loader-box"></div>
<div class="mt-4 font-mono text-[10px] tracking-[0.3em] text-sky-500 uppercase animate-pulse">Initializing Data Stream</div>
</div>
<div class="wrapper" id="main-content">
<header class="flex justify-between items-end mb-8">
<div>
<h1 class="text-4xl font-black tracking-tighter text-white">
<i class="fa-solid fa-bolt-lightning text-sky-500 mr-3"></i>BTC <span class="text-sky-500">퀀텀</span> 플로우
</h1>
<p class="text-slate-400 font-medium mt-1 uppercase text-xs tracking-widest">분석 터미널 v5.15 / 색상 동기화 모드</p>
</div>
<div class="text-right">
<div class="text-[11px] font-mono text-slate-500">
<i class="fa-solid fa-microchip mr-1"></i>상태: <span id="sync-time"><?=date("Y-m-d H:i:s")?></span>
</div>
<div class="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-emerald-500/10 text-emerald-400 text-[10px] font-bold mt-2 uppercase border border-emerald-500/20">
<span class="relative flex h-2 w-2">
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
<span class="relative inline-flex rounded-full h-2 w-2 bg-emerald-500"></span>
</span>
실시간 스트림 무결성 확인됨
</div>
</div>
</header>
<!-- 상단 종합 점수판 (색상 직관성 강화) -->
<?php
// [수정] 양수(+)면 녹색, 음수(-)면 적색 적용
$scoreClass = 'text-slate-400';
if ($GLOBAL_SCORE_PCT > 0) $scoreClass = 'text-emerald-400';
else if ($GLOBAL_SCORE_PCT < 0) $scoreClass = 'text-rose-400';
$scoreDesc = '중립 상태';
if ($GLOBAL_SCORE >= 0.15) $scoreDesc = '매수세 누적 중';
if ($GLOBAL_SCORE >= 0.60) $scoreDesc = '강력한 매수 압력';
if ($GLOBAL_SCORE <= -0.15) $scoreDesc = '매도세 분산 중';
if ($GLOBAL_SCORE <= -0.60) $scoreDesc = '강력한 매도 패닉';
?>
<div id="global-scoreboard" class="mb-6 p-8 rounded-2xl glass scoreboard-glow flex flex-col border-l-4 border-l-sky-500">
<div class="flex items-center justify-between w-full">
<div class="flex items-center gap-8">
<div class="flex flex-col">
<div class="text-[10px] font-black tracking-[0.4em] text-slate-500 uppercase mb-2">Global Net Flow Index</div>
<div id="score-val" class="font-black font-mono leading-none <?=$scoreClass?> text-7xl lg:text-8xl tracking-tighter">
<?=($GLOBAL_SCORE_PCT>=0?'+':'').number_format($GLOBAL_SCORE_PCT,2)?>%
</div>
</div>
<div id="score-hud-panel" class="self-end mb-1">
<span class="hud-label"><i class="fa-solid fa-calculator mr-1"></i> Calculation Logs (Strength × Weight)</span>
<div id="score-details-content" class="font-mono leading-relaxed">
<?= $initial_details_html ?>
<div id="score-summary-line" class="mt-2 pt-2 border-t border-slate-800/50">
<?= $initial_summary_html ?>
</div>
</div>
</div>
</div>
<div class="text-right">
<div id="score-desc" class="text-3xl font-black text-white uppercase"><?=$scoreDesc?></div>
<div class="text-[10px] mt-3 text-slate-500 font-mono flex items-center justify-end gap-2 uppercase">
<i class="fa-solid fa-server text-sky-500"></i> Quantum Engine v5.15
</div>
</div>
</div>
</div>
<div class="overflow-hidden rounded-2xl border border-slate-800 bg-slate-900/50 shadow-2xl">
<table class="w-full text-right" id="data-table">
<thead class="text-slate-400 text-[10px] font-black uppercase bg-slate-800/80 tracking-widest">
<tr>
<th class="p-5 text-left border-b border-slate-800">분석 구간</th>
<th class="p-5 border-b border-slate-800">거래대금 (KRW)</th>
<th class="p-5 border-b border-slate-800">순자금 유입</th>
<th class="p-5 border-b border-slate-800">매수 비중</th>
<th class="p-5 border-b border-slate-800">24H 집중도</th>
<th class="p-5 border-b border-slate-800">불균형 지수</th>
<th class="p-5 border-b border-slate-800">평균 변화율</th>
<th class="p-5 border-b border-slate-800 text-center">52주 최고/최저</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-800">
<?php foreach ($initial_data as $n_min => $row): ?>
<tr id="row-<?= $n_min ?>" class="transition-colors hover:bg-white/5 <?= $row['is_whale'] ? 'whale-bg' : ($row['is_pump'] ? 'pump-bg' : '') ?>">
<td class="p-5 text-left font-bold border-r border-slate-800/50">
<div class="flex items-center gap-3">
<i class="fa-solid fa-clock-rotate-left text-sky-500 text-xs"></i>
<div>
<div class="text-sky-400 text-xs font-black"><?= $row['label'] ?></div>
<div class="text-[10px] text-slate-500 font-mono tracking-tighter">KRW-BTC</div>
</div>
</div>
</td>
<td class="p-5 font-mono font-extrabold text-slate-300 val-amt"><?= number_format($row['total_amt'] / 1e6, 2) ?>M</td>
<td class="p-5 font-mono font-bold val-net <?= $row['net_flow'] >= 0 ? 'text-emerald-400' : 'text-rose-400' ?>">
<?= ($row['net_flow'] > 0 ? '+' : '') . number_format($row['net_flow'] / 1e6, 2) ?>M
</td>
<td class="p-5 font-mono font-bold val-buyratio <?= $row['buy_ratio'] >= 50 ? 'text-emerald-400' : 'text-rose-400' ?>"><?= number_format($row['buy_ratio'], 1) ?>%</td>
<td class="p-5 font-mono text-amber-400 font-bold val-ratio24"><?= number_format($row['ratio_24h'], 1) ?>%</td>
<td class="p-5 font-mono val-imb"><?= number_format($row['imbalance'], 1) ?>%</td>
<td class="p-5 font-mono val-avg"><?= number_format($row['avg_rate'], 2) ?>%</td>
<td class="p-5 font-mono val-pos min-w-[200px]">
<div class="flex flex-col gap-1">
<div class="flex justify-between text-[9px] text-slate-500 uppercase tracking-tighter">
<span>L: <span class="val-l52"><?= $row['l52'] > 0 ? number_format($row['l52']) : 'N/A' ?></span></span>
<span>H: <span class="val-h52"><?= $row['h52'] > 0 ? number_format($row['h52']) : 'N/A' ?></span></span>
</div>
<div class="w-full bg-slate-800 h-1.5 rounded-full overflow-hidden border border-slate-700">
<div class="bg-sky-500 h-full shadow-[0_0_10px_#38bdf8]" style="width: <?= min(100, $row['pos_52w']) ?>%"></div>
</div>
<div class="text-right pos-text text-[10px] font-bold"><?= number_format($row['pos_52w'], 1) ?>%</div>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<footer class="grid grid-cols-2 gap-8 mt-10">
<div class="p-6 glass rounded-2xl border-l-4 border-l-sky-500">
<strong class="text-sky-400 block mb-2 uppercase text-xs tracking-widest"><i class="fa-solid fa-circle-info mr-2"></i>시스템 프로토콜</strong>
<p class="text-slate-400 text-xs leading-relaxed font-medium">
본 데이터는 10초마다 갱신되며, 상단 HUD를 통해 각 구간별 지표 기여도를 실시간으로 모니터링할 수 있습니다.
색상 인디케이터는 순자금의 유입(+)과 유출(-)을 즉각적으로 시각화합니다.
</p>
</div>
<div class="p-6 glass rounded-2xl border-l-4 border-l-amber-500">
<strong class="text-amber-400 block mb-2 uppercase text-xs tracking-widest"><i class="fa-solid fa-shield-halved mr-2"></i>분석 시그널</strong>
<div class="flex gap-6">
<div class="text-[11px] text-slate-400"><span class="text-emerald-400 font-black"><i class="fa-solid fa-fish-fins"></i> WHALE:</span> 저점 구간 대량 유입</div>
<div class="text-[11px] text-slate-400"><span class="text-rose-400 font-black"><i class="fa-solid fa-fire-flame-curved"></i> PUMP:</span> 불균형 가속</div>
</div>
</div>
</footer>
</div>
<script>
const weights = <?= json_encode($WEIGHTS) ?>;
const shortLabels = <?= json_encode($short_labels) ?>;
window.addEventListener('load', () => {
const preloader = document.getElementById('preloader');
const content = document.getElementById('main-content');
setTimeout(() => { if (preloader) preloader.style.opacity = '0'; if (content) content.classList.add('loaded'); setTimeout(() => { if (preloader) preloader.style.display = 'none'; }, 500); }, 800);
});
function updateDashboard() {
fetch(window.location.pathname + '?mode=update&_ts=' + Date.now(), { cache: 'no-store' })
.then(response => { if (!response.ok) throw new Error('HTTP_' + response.status); return response.json(); })
.then(res => {
if (!res || !res.data || typeof res.data !== 'object') return;
const syncEl = document.getElementById('sync-time');
if (syncEl) syncEl.innerText = res.sync_time;
let globalScoreSum = 0; let globalWeightSum = 0; let detailHtml = '';
Object.keys(res.data).forEach(n_min => {
const row = res.data[n_min];
const tr = document.getElementById('row-' + n_min);
if (!tr) return;
const w = weights[row.label] || 0;
const shortLbl = shortLabels[n_min] || n_min;
let strength = 0; let weighted = 0;
if (w > 0) {
strength = Number(row.total_amt || 0) > 0 ? (Number(row.net_flow || 0) / Number(row.total_amt || 0)) : 0;
strength = Math.max(-1, Math.min(1, strength));
weighted = strength * w; globalScoreSum += weighted; globalWeightSum += w;
const colorClass = strength > 0 ? 'text-emerald-500' : (strength < 0 ? 'text-rose-500' : 'text-slate-500');
detailHtml += `<span class='inline-block mr-2'><span class='text-slate-600'>${shortLbl}:</span><span class='${colorClass}'>${strength >= 0 ? '+' : ''}${strength.toFixed(2)}</span><span class='text-slate-700'>×${w}=</span><span class='${colorClass}'>${weighted >= 0 ? '+' : ''}${weighted.toFixed(2)}</span></span>`;
}
tr.className = `transition-colors hover:bg-white/5 ${row.is_whale ? 'whale-bg' : (row.is_pump ? 'pump-bg' : '')}`;
updateCell(tr.querySelector('.val-amt'), (Number(row.total_amt || 0) / 1e6).toFixed(2) + 'M');
const netEl = tr.querySelector('.val-net');
if (netEl) { const nf = Number(row.net_flow || 0); netEl.innerText = (nf > 0 ? '+' : '') + (nf / 1e6).toFixed(2) + 'M'; netEl.className = `p-5 font-mono font-bold val-net ${nf >= 0 ? 'text-emerald-400' : 'text-rose-400'}`; }
const brEl = tr.querySelector('.val-buyratio');
if (brEl) { const br = Number(row.buy_ratio || 0); brEl.innerText = br.toFixed(1) + '%'; brEl.className = `p-5 font-mono font-bold val-buyratio ${br >= 50 ? 'text-emerald-400' : 'text-rose-400'}`; }
if (tr.querySelector('.val-ratio24')) tr.querySelector('.val-ratio24').innerText = Number(row.ratio_24h || 0).toFixed(1) + '%';
if (tr.querySelector('.val-imb')) tr.querySelector('.val-imb').innerText = Number(row.imbalance || 0).toFixed(1) + '%';
if (tr.querySelector('.val-avg')) tr.querySelector('.val-avg').innerText = Number(row.avg_rate || 0).toFixed(2) + '%';
if (tr.querySelector('.val-h52')) tr.querySelector('.val-h52').innerText = Number(row.h52 || 0) > 0 ? Number(row.h52).toLocaleString() : 'N/A';
if (tr.querySelector('.val-l52')) tr.querySelector('.val-l52').innerText = Number(row.l52 || 0) > 0 ? Number(row.l52).toLocaleString() : 'N/A';
const bar = tr.querySelector('.val-pos .bg-sky-500'); if (bar) bar.style.width = Math.min(100, Number(row.pos_52w || 0)) + '%';
const posText = tr.querySelector('.val-pos .pos-text'); if (posText) posText.innerText = Number(row.pos_52w || 0).toFixed(1) + '%';
});
const finalScore = globalWeightSum > 0 ? (globalScoreSum / globalWeightSum) : 0;
const finalScorePct = finalScore * 100;
const scoreValEl = document.getElementById('score-val');
const scoreDescEl = document.getElementById('score-desc');
const hudContentEl = document.getElementById('score-details-content');
if (scoreValEl) {
scoreValEl.innerText = (finalScorePct >= 0 ? '+' : '') + finalScorePct.toFixed(2) + '%';
// [수정] 실시간 업데이트 시에도 양수 녹색 / 음수 적색 고정
const cls = finalScorePct > 0 ? 'text-emerald-400' : (finalScorePct < 0 ? 'text-rose-400' : 'text-slate-400');
scoreValEl.className = `font-black font-mono leading-none ${cls} text-7xl lg:text-8xl tracking-tighter`;
}
if (scoreDescEl) {
if (finalScore >= 0.60) scoreDescEl.innerText = '강력한 매수 압력';
else if (finalScore >= 0.15) scoreDescEl.innerText = '매수세 누적 중';
else if (finalScore <= -0.60) scoreDescEl.innerText = '강력한 매도 패닉';
else if (finalScore <= -0.15) scoreDescEl.innerText = '매도세 분산 중';
else scoreDescEl.innerText = '중립 상태';
}
if (hudContentEl) {
const summaryHtml = `<div class='mt-2 pt-2 border-t border-slate-800/50'><span class='text-sky-500 px-2'>[ <i class='fa-solid fa-layer-group mr-1'></i>가중합:${globalScoreSum >= 0 ? '+' : ''}${globalScoreSum.toFixed(2)} / 가중치합:${globalWeightSum} = ${finalScore >= 0 ? '+' : ''}${finalScore.toFixed(4)} ]</span></div>`;
hudContentEl.innerHTML = detailHtml + summaryHtml;
}
})
.catch(err => console.error("Update System Error:", err));
}
function updateCell(el, newVal) { if (el && el.innerText !== newVal) { el.innerText = newVal; el.classList.add('update-flash'); setTimeout(() => el.classList.remove('update-flash'), 800); } }
updateDashboard(); setInterval(updateDashboard, 10000);
</script>
</body>
</html>