Files

216 lines
6.9 KiB
Python
Raw Permalink Normal View History

2026-03-13 17:18:19 +08:00
from datetime import date
from fastapi import APIRouter
from fastapi.responses import HTMLResponse
from ..services.store import counts, get_store, list_events, list_raw_items, sources_today
router = APIRouter()
@router.get("/health")
def health():
return {"ok": True}
@router.get("/status")
def status(day: str | None = None):
st = get_store()
conn = st.connect()
item_date = day or date.today().isoformat()
return {
"date": item_date,
"counts": counts(conn),
"sources": sources_today(conn, item_date),
}
@router.get("/raw_items")
def raw_items(limit: int = 20):
st = get_store()
conn = st.connect()
return {"items": list_raw_items(conn, limit=limit)}
@router.get("/events")
def events(limit: int = 20):
st = get_store()
conn = st.connect()
return {"items": list_events(conn, limit=limit)}
@router.get("/ui", response_class=HTMLResponse)
def ui():
# Tiny no-build UI for early validation.
html = """<!doctype html>
<html>
<head>
<meta charset=\"utf-8\" />
<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />
<title>EventFlow V1</title>
<style>
body { font-family: ui-sans-serif, system-ui, Arial; margin: 18px; }
h1 { margin: 0 0 8px 0; }
.row { display: flex; gap: 16px; flex-wrap: wrap; }
.card { border: 1px solid #ddd; border-radius: 8px; padding: 12px; min-width: 320px; }
table { border-collapse: collapse; width: 100%; }
th, td { border-bottom: 1px solid #eee; padding: 6px 8px; font-size: 13px; vertical-align: top; }
th { text-align: left; color: #444; }
code { background: #f6f6f6; padding: 2px 4px; border-radius: 4px; }
.muted { color: #666; }
.ok { color: #0a7; font-weight: 600; }
.bad { color: #b00; font-weight: 600; }
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
button { padding: 6px 10px; }
input, textarea { width: 100%; box-sizing: border-box; }
textarea { height: 90px; }
</style>
</head>
<body>
<h1>EventFlow V1</h1>
<div class=\"muted\">Status page: sources, dates, counts, and parsed events.</div>
<div style=\"height:12px\"></div>
<div class=\"row\">
<div class=\"card\" style=\"flex: 1\">
<div style=\"display:flex; justify-content:space-between; align-items:center; gap:12px;\">
<div>
<div><b>Today</b>: <span id=\"today\"></span></div>
<div class=\"muted\">Auto-refresh every 5s</div>
</div>
<button onclick=\"refreshAll()\">Refresh</button>
</div>
<div style=\"height:10px\"></div>
<div id=\"counts\" class=\"mono\"></div>
<div style=\"height:10px\"></div>
<div><b>Sources (today)</b></div>
<table id=\"sources\"></table>
</div>
<div class=\"card\" style=\"flex: 1\">
<div><b>Quick Analyze (manual)</b></div>
<div class=\"muted\">This will save a raw item + parsed event into SQLite.</div>
<div style=\"height:8px\"></div>
<label>Title</label>
<input id=\"m_title\" placeholder=\"e.g. Fed signals higher-for-longer rates\" />
<div style=\"height:6px\"></div>
<label>Content</label>
<textarea id=\"m_content\" placeholder=\"Paste a paragraph...\"></textarea>
<div style=\"height:6px\"></div>
<button onclick=\"runAnalyze()\">Analyze</button>
<div id=\"m_out\" class=\"mono\" style=\"white-space:pre-wrap; margin-top:10px;\"></div>
</div>
</div>
<div style=\"height:16px\"></div>
<div class=\"row\">
<div class=\"card\" style=\"flex: 1\">
<div><b>Latest Raw Items</b> <span class=\"muted\">(limit 20)</span></div>
<table id=\"raw\"></table>
</div>
<div class=\"card\" style=\"flex: 1\">
<div><b>Latest Parsed Events</b> <span class=\"muted\">(limit 20)</span></div>
<table id=\"events\"></table>
</div>
</div>
<script>
function esc(s) {
return (s ?? '').toString().replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;');
}
async function jget(url) {
const r = await fetch(url);
if (!r.ok) throw new Error('HTTP ' + r.status);
return await r.json();
}
function setTable(el, headers, rows) {
let h = '<tr>' + headers.map(x => `<th>${esc(x)}</th>`).join('') + '</tr>';
let b = rows.map(r => '<tr>' + r.map(x => `<td>${x}</td>`).join('') + '</tr>').join('');
el.innerHTML = h + b;
}
async function refreshStatus() {
const st = await jget('/query/status');
document.getElementById('today').textContent = st.date;
const c = st.counts;
document.getElementById('counts').textContent = `raw_items=${c.raw_items} events=${c.events} ok=${c.events_ok} err=${c.events_err}`;
const srows = (st.sources || []).map(x => [esc(x.source), esc(x.count)]);
setTable(document.getElementById('sources'), ['source', 'count'], srows);
}
async function refreshRaw() {
const data = await jget('/query/raw_items?limit=20');
const rows = (data.items || []).map(it => {
const u = it.url ? `<a href="${esc(it.url)}" target="_blank">link</a>` : '';
return [
`<span class="mono">${esc(it.id)}</span>`,
esc(it.source),
esc(it.item_date),
esc(it.lang || ''),
esc((it.title || '').slice(0, 120)),
u,
];
});
setTable(document.getElementById('raw'), ['id', 'source', 'date', 'lang', 'title', 'url'], rows);
}
async function refreshEvents() {
const data = await jget('/query/events?limit=20');
const rows = (data.items || []).map(it => {
const cls = it.ok ? 'ok' : 'bad';
let ev = '';
try {
if (it.event_json) {
const obj = JSON.parse(it.event_json);
ev = `${esc(obj.event_type)} dir=${esc(obj.impact_direction)} conf=${esc(obj.confidence)}<br/><span class="muted">${esc((obj.summary_en || obj.summary_zh || '').slice(0, 140))}</span>`;
}
} catch (e) {
ev = '<span class="bad">bad json</span>';
}
return [
`<span class="mono">${esc(it.id)}</span>`,
esc(it.source),
`<span class="${cls}">${it.ok ? 'ok' : 'err'}</span>`,
esc((it.title || '').slice(0, 90)),
ev,
];
});
setTable(document.getElementById('events'), ['id', 'source', 'ok', 'raw_title', 'event'], rows);
}
async function runAnalyze() {
const title = document.getElementById('m_title').value;
const content = document.getElementById('m_content').value;
const out = document.getElementById('m_out');
out.textContent = 'running...';
const r = await fetch('/analyze/event', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({source: 'manual', date: new Date().toISOString().slice(0,10), title, content, lang: ''}),
});
const j = await r.json();
out.textContent = JSON.stringify(j, null, 2);
await refreshAll();
}
async function refreshAll() {
try {
await refreshStatus();
await refreshRaw();
await refreshEvents();
} catch (e) {
console.error(e);
}
}
refreshAll();
setInterval(refreshAll, 5000);
</script>
</body>
</html>"""
return HTMLResponse(content=html)