744 lines
21 KiB
Python
744 lines
21 KiB
Python
HOME_PAGE_HTML = """
|
|
<!doctype html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<title>SignalScout</title>
|
|
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='14' fill='%230b7f70'/%3E%3Cpath d='M18 42a20 20 0 0 1 28-28' fill='none' stroke='%23bdf8ee' stroke-width='5' stroke-linecap='round' opacity='.72'/%3E%3Cpath d='M32 32 48 16' fill='none' stroke='white' stroke-width='5' stroke-linecap='round'/%3E%3Ccircle cx='48' cy='16' r='5' fill='%23f5d76e'/%3E%3Ctext x='32' y='43' text-anchor='middle' font-family='Arial, sans-serif' font-size='28' font-weight='800' fill='white'%3ES%3C/text%3E%3C/svg%3E" />
|
|
<style>
|
|
:root {
|
|
color-scheme: light;
|
|
--bg: oklch(96.8% 0.018 182);
|
|
--ink: oklch(22% 0.028 210);
|
|
--muted: oklch(50% 0.028 214);
|
|
--line: oklch(87% 0.022 190);
|
|
--panel: oklch(99% 0.006 190);
|
|
--panel-2: oklch(94.5% 0.018 176);
|
|
--accent: oklch(52% 0.12 174);
|
|
--warn: oklch(67% 0.13 70);
|
|
--bad: oklch(58% 0.16 26);
|
|
--good: oklch(53% 0.12 150);
|
|
}
|
|
|
|
* { box-sizing: border-box; }
|
|
|
|
body {
|
|
margin: 0;
|
|
min-height: 100vh;
|
|
background:
|
|
linear-gradient(90deg, oklch(92% 0.018 180 / 0.55) 1px, transparent 1px),
|
|
linear-gradient(0deg, oklch(92% 0.018 180 / 0.55) 1px, transparent 1px),
|
|
var(--bg);
|
|
background-size: 28px 28px;
|
|
color: var(--ink);
|
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
|
|
letter-spacing: 0;
|
|
}
|
|
|
|
a { color: inherit; }
|
|
|
|
.shell {
|
|
display: grid;
|
|
grid-template-columns: 224px minmax(0, 1fr);
|
|
min-height: 100vh;
|
|
}
|
|
|
|
.rail {
|
|
border-right: 1px solid var(--line);
|
|
background: oklch(98% 0.01 180 / 0.82);
|
|
padding: 24px 18px;
|
|
position: sticky;
|
|
top: 0;
|
|
height: 100vh;
|
|
}
|
|
|
|
.brand {
|
|
display: grid;
|
|
gap: 8px;
|
|
padding-bottom: 22px;
|
|
border-bottom: 1px solid var(--line);
|
|
}
|
|
|
|
.brand-mark {
|
|
width: 42px;
|
|
height: 42px;
|
|
display: grid;
|
|
place-items: center;
|
|
border: 1px solid var(--ink);
|
|
background: var(--accent);
|
|
color: white;
|
|
font-weight: 800;
|
|
border-radius: 8px;
|
|
box-shadow: 5px 5px 0 oklch(25% 0.02 210);
|
|
}
|
|
|
|
.brand strong {
|
|
font-size: 18px;
|
|
line-height: 1.1;
|
|
}
|
|
|
|
.brand span {
|
|
color: var(--muted);
|
|
font-size: 12px;
|
|
line-height: 1.55;
|
|
}
|
|
|
|
.nav {
|
|
display: grid;
|
|
gap: 8px;
|
|
margin-top: 24px;
|
|
}
|
|
|
|
.nav button {
|
|
border: 0;
|
|
width: 100%;
|
|
text-align: left;
|
|
background: transparent;
|
|
padding: 10px 12px;
|
|
border-radius: 8px;
|
|
color: var(--muted);
|
|
font: inherit;
|
|
font-size: 14px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.nav button.active {
|
|
background: var(--ink);
|
|
color: white;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.content {
|
|
padding: 28px;
|
|
display: grid;
|
|
gap: 20px;
|
|
align-content: start;
|
|
}
|
|
|
|
.topbar {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
gap: 18px;
|
|
align-items: flex-start;
|
|
border-bottom: 1px solid var(--line);
|
|
padding-bottom: 20px;
|
|
}
|
|
|
|
h1 {
|
|
margin: 0;
|
|
font-size: clamp(28px, 4vw, 48px);
|
|
line-height: 1;
|
|
max-width: 760px;
|
|
}
|
|
|
|
.subtitle {
|
|
margin: 12px 0 0;
|
|
color: var(--muted);
|
|
max-width: 760px;
|
|
line-height: 1.7;
|
|
}
|
|
|
|
.pulse {
|
|
min-width: 220px;
|
|
border: 1px solid var(--line);
|
|
border-radius: 8px;
|
|
padding: 12px 14px;
|
|
background: var(--panel);
|
|
color: var(--muted);
|
|
line-height: 1.55;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.pulse strong {
|
|
display: block;
|
|
color: var(--ink);
|
|
font-size: 18px;
|
|
line-height: 1.2;
|
|
}
|
|
|
|
.pulse span {
|
|
display: block;
|
|
margin-top: 5px;
|
|
}
|
|
|
|
.actions {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
margin-top: 12px;
|
|
}
|
|
|
|
.action-button {
|
|
border: 1px solid oklch(25% 0.02 210);
|
|
border-radius: 8px;
|
|
padding: 9px 13px;
|
|
background: var(--ink);
|
|
color: white;
|
|
font: inherit;
|
|
font-size: 13px;
|
|
font-weight: 700;
|
|
cursor: pointer;
|
|
min-width: 96px;
|
|
}
|
|
|
|
.action-button:disabled {
|
|
cursor: wait;
|
|
opacity: 0.66;
|
|
}
|
|
|
|
.layout {
|
|
display: grid;
|
|
grid-template-columns: minmax(0, 1.2fr) minmax(340px, 0.8fr);
|
|
gap: 18px;
|
|
align-items: start;
|
|
}
|
|
|
|
.view {
|
|
display: none;
|
|
gap: 18px;
|
|
}
|
|
|
|
.view.active {
|
|
display: grid;
|
|
}
|
|
|
|
.view[data-view="signals"].active {
|
|
grid-template-columns: minmax(0, 1.05fr) minmax(360px, 0.95fr);
|
|
}
|
|
|
|
.panel {
|
|
background: var(--panel);
|
|
border: 1px solid var(--line);
|
|
border-radius: 8px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.panel-head {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
gap: 12px;
|
|
padding: 14px 16px;
|
|
border-bottom: 1px solid var(--line);
|
|
background: var(--panel-2);
|
|
}
|
|
|
|
.panel-head h2 {
|
|
margin: 0;
|
|
font-size: 15px;
|
|
line-height: 1.2;
|
|
}
|
|
|
|
.count {
|
|
font-variant-numeric: tabular-nums;
|
|
color: var(--muted);
|
|
font-size: 12px;
|
|
}
|
|
|
|
.stack { display: grid; gap: 18px; }
|
|
|
|
.signals {
|
|
display: grid;
|
|
min-height: 360px;
|
|
}
|
|
|
|
.signal-row {
|
|
display: grid;
|
|
gap: 8px;
|
|
padding: 16px;
|
|
border-bottom: 1px solid var(--line);
|
|
background: white;
|
|
}
|
|
|
|
.signal-row:last-child { border-bottom: 0; }
|
|
|
|
.signal-meta,
|
|
.run-row {
|
|
display: flex;
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
gap: 8px;
|
|
color: var(--muted);
|
|
font-size: 12px;
|
|
}
|
|
|
|
.signal-row h3 {
|
|
margin: 0;
|
|
font-size: 16px;
|
|
line-height: 1.35;
|
|
}
|
|
|
|
.signal-row p {
|
|
margin: 0;
|
|
color: oklch(34% 0.025 210);
|
|
line-height: 1.65;
|
|
}
|
|
|
|
.pill {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
min-height: 24px;
|
|
border: 1px solid var(--line);
|
|
border-radius: 999px;
|
|
padding: 2px 9px;
|
|
background: oklch(98% 0.01 180);
|
|
color: var(--muted);
|
|
max-width: 100%;
|
|
}
|
|
|
|
.importance {
|
|
color: oklch(19% 0.035 190);
|
|
border-color: oklch(72% 0.08 174);
|
|
background: oklch(91% 0.045 174);
|
|
}
|
|
|
|
.report {
|
|
padding: 18px;
|
|
min-height: 520px;
|
|
line-height: 1.75;
|
|
color: oklch(25% 0.028 210);
|
|
}
|
|
|
|
.report-doc {
|
|
display: grid;
|
|
gap: 14px;
|
|
max-width: 980px;
|
|
}
|
|
|
|
.report-doc h2,
|
|
.report-doc h3,
|
|
.report-doc h4,
|
|
.report-doc p,
|
|
.report-doc ul,
|
|
.report-doc ol {
|
|
margin: 0;
|
|
}
|
|
|
|
.report-doc h2 {
|
|
font-size: 24px;
|
|
line-height: 1.25;
|
|
padding-bottom: 12px;
|
|
border-bottom: 1px solid var(--line);
|
|
}
|
|
|
|
.report-doc h3 {
|
|
margin-top: 8px;
|
|
font-size: 18px;
|
|
line-height: 1.35;
|
|
}
|
|
|
|
.report-doc h4 {
|
|
color: oklch(34% 0.035 190);
|
|
font-size: 15px;
|
|
line-height: 1.35;
|
|
}
|
|
|
|
.report-doc p {
|
|
color: oklch(30% 0.025 210);
|
|
}
|
|
|
|
.report-doc ul,
|
|
.report-doc ol {
|
|
display: grid;
|
|
gap: 8px;
|
|
padding-left: 22px;
|
|
}
|
|
|
|
.report-doc li {
|
|
padding-left: 3px;
|
|
}
|
|
|
|
.report-doc a {
|
|
color: oklch(42% 0.12 174);
|
|
text-decoration-thickness: 1px;
|
|
text-underline-offset: 3px;
|
|
}
|
|
|
|
.report-doc code {
|
|
border: 1px solid var(--line);
|
|
border-radius: 6px;
|
|
padding: 1px 5px;
|
|
background: oklch(96% 0.012 180);
|
|
font-size: 0.92em;
|
|
}
|
|
|
|
.runs { display: grid; }
|
|
|
|
.run-row {
|
|
justify-content: space-between;
|
|
padding: 12px 16px;
|
|
border-bottom: 1px solid var(--line);
|
|
background: white;
|
|
}
|
|
|
|
.run-row:last-child { border-bottom: 0; }
|
|
|
|
.state {
|
|
font-weight: 700;
|
|
color: var(--muted);
|
|
}
|
|
|
|
.state.completed { color: var(--good); }
|
|
.state.failed { color: var(--bad); }
|
|
.state.running { color: var(--warn); }
|
|
|
|
.empty {
|
|
padding: 28px 18px;
|
|
color: var(--muted);
|
|
line-height: 1.7;
|
|
}
|
|
|
|
@media (max-width: 980px) {
|
|
.shell { grid-template-columns: 1fr; }
|
|
.rail {
|
|
position: static;
|
|
height: auto;
|
|
border-right: 0;
|
|
border-bottom: 1px solid var(--line);
|
|
}
|
|
.nav { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
|
.view[data-view="signals"].active { grid-template-columns: 1fr; }
|
|
.topbar { display: grid; }
|
|
.pulse { min-width: 0; }
|
|
}
|
|
|
|
@media (max-width: 560px) {
|
|
.content { padding: 18px; }
|
|
.rail { padding: 18px; }
|
|
.nav { grid-template-columns: 1fr; }
|
|
h1 { font-size: 30px; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="shell">
|
|
<aside class="rail">
|
|
<div class="brand">
|
|
<div class="brand-mark">S</div>
|
|
<strong>SignalScout</strong>
|
|
<span>AI 情报采集与研判工作台</span>
|
|
</div>
|
|
<nav class="nav" aria-label="主导航">
|
|
<button class="active" type="button" data-view-target="signals">信号</button>
|
|
<button type="button" data-view-target="report">日报</button>
|
|
<button type="button" data-view-target="runs">记录</button>
|
|
</nav>
|
|
</aside>
|
|
|
|
<main class="content">
|
|
<section class="topbar">
|
|
<div>
|
|
<h1>今日 AI 信号</h1>
|
|
<p class="subtitle">新闻动态与 GitHub 项目分开阅读,日报保留适合快速复盘的重点判断。</p>
|
|
</div>
|
|
<div class="pulse">
|
|
<strong id="todayCountText">读取中</strong>
|
|
<span id="todayCountDetail">正在整理新闻与项目。</span>
|
|
<div class="actions">
|
|
<button class="action-button" id="manualRunButton" type="button">手动搜索</button>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="view active" data-view="signals">
|
|
<div class="panel">
|
|
<div class="panel-head">
|
|
<h2>AI 新闻</h2>
|
|
<span class="count" id="newsCount">0 条</span>
|
|
</div>
|
|
<div class="signals" id="newsList">
|
|
<div class="empty">正在读取新闻。</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="stack">
|
|
<div class="panel">
|
|
<div class="panel-head">
|
|
<h2>GitHub 热门项目</h2>
|
|
<span class="count" id="githubCount">0 条</span>
|
|
</div>
|
|
<div class="signals" id="githubList">
|
|
<div class="empty">正在读取项目。</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="panel">
|
|
<div class="panel-head">
|
|
<h2>最新日报</h2>
|
|
<span class="count" id="reportTime">待生成</span>
|
|
</div>
|
|
<div class="report" id="reportPreview">正在读取日报。</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="view" data-view="report">
|
|
<div class="panel">
|
|
<div class="panel-head">
|
|
<h2>最新日报</h2>
|
|
<span class="count" id="reportTimeFull">待生成</span>
|
|
</div>
|
|
<div class="report" id="reportBody">正在读取日报。</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="view" data-view="runs">
|
|
<div class="panel">
|
|
<div class="panel-head">
|
|
<h2>运行记录</h2>
|
|
<span class="count" id="runCountFull">0 次</span>
|
|
</div>
|
|
<div class="runs" id="runList">
|
|
<div class="empty">正在读取运行记录。</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
</div>
|
|
|
|
<script>
|
|
const navButtons = Array.from(document.querySelectorAll("[data-view-target]"));
|
|
const views = Array.from(document.querySelectorAll("[data-view]"));
|
|
const newsList = document.querySelector("#newsList");
|
|
const newsCount = document.querySelector("#newsCount");
|
|
const githubList = document.querySelector("#githubList");
|
|
const githubCount = document.querySelector("#githubCount");
|
|
const reportPreview = document.querySelector("#reportPreview");
|
|
const reportBody = document.querySelector("#reportBody");
|
|
const reportTime = document.querySelector("#reportTime");
|
|
const reportTimeFull = document.querySelector("#reportTimeFull");
|
|
const runList = document.querySelector("#runList");
|
|
const runCountFull = document.querySelector("#runCountFull");
|
|
const todayCountText = document.querySelector("#todayCountText");
|
|
const todayCountDetail = document.querySelector("#todayCountDetail");
|
|
const manualRunButton = document.querySelector("#manualRunButton");
|
|
|
|
const stateText = {
|
|
completed: "完成",
|
|
failed: "失败",
|
|
running: "运行中"
|
|
};
|
|
|
|
function escapeHtml(value) {
|
|
return String(value ?? "")
|
|
.replaceAll("&", "&")
|
|
.replaceAll("<", "<")
|
|
.replaceAll(">", ">")
|
|
.replaceAll('"', """)
|
|
.replaceAll("'", "'");
|
|
}
|
|
|
|
function renderInlineMarkdown(value) {
|
|
return escapeHtml(value)
|
|
.replace(/`([^`]+)`/g, "<code>$1</code>")
|
|
.replace(/\\*\\*([^*]+)\\*\\*/g, "<strong>$1</strong>")
|
|
.replace(/\\[([^\\]]+)\\]\\((https?:\\/\\/[^\\s)]+)\\)/g, '<a href="$2" target="_blank" rel="noreferrer">$1</a>');
|
|
}
|
|
|
|
function renderMarkdownReport(markdown) {
|
|
const lines = String(markdown || "").split(/\\r?\\n/);
|
|
const blocks = [];
|
|
let paragraph = [];
|
|
let listItems = [];
|
|
let listTag = "ul";
|
|
|
|
const flushParagraph = () => {
|
|
if (!paragraph.length) return;
|
|
blocks.push(`<p>${renderInlineMarkdown(paragraph.join(" "))}</p>`);
|
|
paragraph = [];
|
|
};
|
|
|
|
const flushList = () => {
|
|
if (!listItems.length) return;
|
|
blocks.push(`<${listTag}>${listItems.map((item) => `<li>${renderInlineMarkdown(item)}</li>`).join("")}</${listTag}>`);
|
|
listItems = [];
|
|
};
|
|
|
|
lines.forEach((line) => {
|
|
const trimmed = line.trim();
|
|
const heading = /^(#{1,4})\\s+(.+)$/.exec(trimmed);
|
|
const bullet = /^[-*]\\s+(.+)$/.exec(trimmed);
|
|
const ordered = /^\\d+[.)]\\s+(.+)$/.exec(trimmed);
|
|
|
|
if (!trimmed) {
|
|
flushParagraph();
|
|
flushList();
|
|
return;
|
|
}
|
|
if (heading) {
|
|
flushParagraph();
|
|
flushList();
|
|
const level = Math.min(heading[1].length + 1, 4);
|
|
blocks.push(`<h${level}>${renderInlineMarkdown(heading[2])}</h${level}>`);
|
|
return;
|
|
}
|
|
if (bullet || ordered) {
|
|
flushParagraph();
|
|
const nextTag = ordered ? "ol" : "ul";
|
|
if (listItems.length && listTag !== nextTag) flushList();
|
|
listTag = nextTag;
|
|
listItems.push((bullet || ordered)[1]);
|
|
return;
|
|
}
|
|
flushList();
|
|
paragraph.push(trimmed);
|
|
});
|
|
|
|
flushParagraph();
|
|
flushList();
|
|
return `<div class="report-doc">${blocks.join("")}</div>`;
|
|
}
|
|
|
|
function formatDate(value) {
|
|
if (!value) return "未标注时间";
|
|
const text = String(value);
|
|
const normalized = /(?:Z|[+-]\\d{2}:\\d{2})$/.test(text) ? text : `${text}+08:00`;
|
|
const date = new Date(normalized);
|
|
if (Number.isNaN(date.getTime())) return "未标注时间";
|
|
return date.toLocaleString("zh-CN", {
|
|
timeZone: "Asia/Shanghai",
|
|
month: "2-digit",
|
|
day: "2-digit",
|
|
hour: "2-digit",
|
|
minute: "2-digit"
|
|
});
|
|
}
|
|
|
|
function activateView(name, updateHash = true) {
|
|
navButtons.forEach((button) => button.classList.toggle("active", button.dataset.viewTarget === name));
|
|
views.forEach((view) => view.classList.toggle("active", view.dataset.view === name));
|
|
if (updateHash) history.replaceState(null, "", `#${name}`);
|
|
}
|
|
|
|
function renderSignalGroup(signals, target, counter, emptyText) {
|
|
counter.textContent = `${signals.length} 条`;
|
|
if (!signals.length) {
|
|
target.innerHTML = `<div class="empty">${escapeHtml(emptyText)}</div>`;
|
|
return;
|
|
}
|
|
target.innerHTML = signals.map((item) => `
|
|
<article class="signal-row">
|
|
<div class="signal-meta">
|
|
<span class="pill">${escapeHtml(item.topic)}</span>
|
|
<span class="pill importance">重要度 ${escapeHtml(item.importance)}</span>
|
|
<span>${escapeHtml(item.source_name)}</span>
|
|
<span>${formatDate(item.published_at || item.created_at)}</span>
|
|
</div>
|
|
<h3><a href="${escapeHtml(item.source_url)}" target="_blank" rel="noreferrer">${escapeHtml(item.title)}</a></h3>
|
|
<p>${escapeHtml(item.summary)}</p>
|
|
<div class="signal-meta">
|
|
${(item.entities || []).slice(0, 5).map((entity) => `<span class="pill">${escapeHtml(entity)}</span>`).join("")}
|
|
</div>
|
|
</article>
|
|
`).join("");
|
|
}
|
|
|
|
function renderReport(report) {
|
|
if (!report) {
|
|
reportTime.textContent = "待生成";
|
|
reportTimeFull.textContent = "待生成";
|
|
reportPreview.textContent = "日报生成后会显示在这里。";
|
|
reportBody.textContent = "日报生成后会显示在这里。";
|
|
return;
|
|
}
|
|
const timeText = formatDate(report.created_at);
|
|
const content = report.content_md || "日报内容为空。";
|
|
reportTime.textContent = timeText;
|
|
reportTimeFull.textContent = timeText;
|
|
reportPreview.innerHTML = renderMarkdownReport(content);
|
|
reportBody.innerHTML = renderMarkdownReport(content);
|
|
}
|
|
|
|
function runRows(runs) {
|
|
return runs.map((run) => `
|
|
<div class="run-row">
|
|
<span>#${escapeHtml(run.id)} · ${formatDate(run.started_at)}</span>
|
|
<span class="state ${escapeHtml(run.status)}">${escapeHtml(stateText[run.status] || run.status)}</span>
|
|
</div>
|
|
`).join("");
|
|
}
|
|
|
|
function renderRuns(runs) {
|
|
runCountFull.textContent = `${runs.length} 次`;
|
|
if (!runs.length) {
|
|
runList.innerHTML = '<div class="empty">暂无运行记录。</div>';
|
|
return;
|
|
}
|
|
runList.innerHTML = runRows(runs);
|
|
}
|
|
|
|
function renderSignalSummary(newsSignals, githubSignals) {
|
|
const newsTotal = newsSignals.length;
|
|
const githubTotal = githubSignals.length;
|
|
todayCountText.textContent = `${newsTotal + githubTotal} 条信号`;
|
|
todayCountDetail.textContent = `新闻 ${newsTotal} · GitHub ${githubTotal}`;
|
|
}
|
|
|
|
async function loadDashboard() {
|
|
const response = await fetch("/dashboard", { headers: { Accept: "application/json" } });
|
|
if (!response.ok) throw new Error("读取情报失败");
|
|
const data = await response.json();
|
|
renderSignalGroup(
|
|
data.news_signals || [],
|
|
newsList,
|
|
newsCount,
|
|
"Agent 正在整理 AI 新闻,完成后这里会出现新闻信号。"
|
|
);
|
|
renderSignalGroup(
|
|
data.github_signals || [],
|
|
githubList,
|
|
githubCount,
|
|
"Agent 正在整理 GitHub 热门项目,完成后这里会出现项目线索。"
|
|
);
|
|
renderSignalSummary(data.news_signals || [], data.github_signals || []);
|
|
renderReport(data.latest_report);
|
|
renderRuns(data.runs || []);
|
|
}
|
|
|
|
async function triggerManualRun() {
|
|
manualRunButton.disabled = true;
|
|
manualRunButton.textContent = "搜索中";
|
|
try {
|
|
const response = await fetch("/agent/runs/daily", {
|
|
method: "POST",
|
|
headers: { Accept: "application/json" }
|
|
});
|
|
if (!response.ok) throw new Error("手动搜索失败");
|
|
await loadDashboard();
|
|
} finally {
|
|
window.setTimeout(() => {
|
|
manualRunButton.disabled = false;
|
|
manualRunButton.textContent = "手动搜索";
|
|
loadDashboard().catch(() => {});
|
|
}, 3000);
|
|
}
|
|
}
|
|
|
|
navButtons.forEach((button) => {
|
|
button.addEventListener("click", () => activateView(button.dataset.viewTarget));
|
|
});
|
|
manualRunButton.addEventListener("click", () => {
|
|
triggerManualRun().catch(() => {
|
|
manualRunButton.disabled = false;
|
|
manualRunButton.textContent = "手动搜索";
|
|
});
|
|
});
|
|
|
|
const initialView = window.location.hash.replace("#", "");
|
|
if (["signals", "report", "runs"].includes(initialView)) {
|
|
activateView(initialView, false);
|
|
}
|
|
|
|
loadDashboard().catch(() => {
|
|
newsList.innerHTML = '<div class="empty">新闻读取失败,请稍后再试。</div>';
|
|
githubList.innerHTML = '<div class="empty">项目读取失败,请稍后再试。</div>';
|
|
reportPreview.textContent = "日报读取失败。";
|
|
reportBody.textContent = "日报读取失败。";
|
|
runList.innerHTML = '<div class="empty">运行记录读取失败。</div>';
|
|
});
|
|
window.setInterval(loadDashboard, 60000);
|
|
</script>
|
|
</body>
|
|
</html>
|
|
"""
|