diff --git a/README.md b/README.md
index accfed2..fc083bf 100644
--- a/README.md
+++ b/README.md
@@ -26,8 +26,9 @@
## 部署到 Cloudflare
1. Fork 本仓库并打开 [Workers & Pages](https://dash.cloudflare.com/?to=/:account/workers-and-pages/create),选择 **Import a repository** 后连接仓库。
-2. 部署命令修改为 `bun run deploy`。
-3. 在 **高级设置** 中填写下表变量后部署。
+2. 在构建设置中将 **Build command(构建命令)清空**。Cloudflare 可能自动识别为 `bun run build`,需要手动删除;如果控制台不允许留空,可填写 `true`。
+3. 将 **Deploy command(部署命令)** 修改为 `bun run deploy`。该命令会按需构建前端和 Worker、应用 D1 迁移并完成部署,无需单独执行构建命令;同时配置 `bun run build` 会导致前端重复构建。
+4. 在 **高级设置** 中填写下表变量后部署。
| 变量 | 必填/可选 | 说明 |
| --- | ---: | --- |
diff --git a/frontend/src/AppProvider.tsx b/frontend/src/AppProvider.tsx
index 0a75ad6..94bdb39 100644
--- a/frontend/src/AppProvider.tsx
+++ b/frontend/src/AppProvider.tsx
@@ -188,11 +188,12 @@ export function AppProvider({ children }: { children: ReactNode }) {
servers: serversRef.current,
})),
onConnectedChange: (connected) => { liveConnectedRef.current = connected; },
- wakeTargets: () => {
+ onOverviewConnected: () => {
const now = Date.now() / 1000;
- return serversRef.current
+ const serverIds = serversRef.current
.filter((server) => server.timestamp && now - server.timestamp <= config.offline_threshold_seconds)
.map((server) => server.id);
+ return serverIds.length ? api.wakeServers(serverIds) : Promise.resolve();
},
});
}, [access, config.offline_threshold_seconds, loading, selectedId]);
@@ -211,8 +212,9 @@ export function AppProvider({ children }: { children: ReactNode }) {
link.rel = "icon";
document.head.append(link);
}
- link.href = config.favicon_url || config.logo_url || "/logo.svg";
- }, [config.favicon_url, config.logo_url]);
+ link.removeAttribute("type");
+ link.href = config.logo_url || "/logo.svg";
+ }, [config.logo_url]);
useEffect(() => {
const media = matchMedia("(prefers-color-scheme: dark)");
diff --git a/frontend/src/admin-main.tsx b/frontend/src/admin-main.tsx
index 67338e6..5885c7a 100644
--- a/frontend/src/admin-main.tsx
+++ b/frontend/src/admin-main.tsx
@@ -50,8 +50,9 @@ function AdminApp() {
link.rel = "icon";
document.head.append(link);
}
- link.href = config?.favicon_url || config?.logo_url || "/logo.svg";
- }, [config?.favicon_url, config?.logo_url]);
+ link.removeAttribute("type");
+ link.href = config?.logo_url || "/logo.svg";
+ }, [config?.logo_url]);
if (error) return
{error}
;
if (!config) return 正在加载管理面板
;
diff --git a/frontend/src/api.ts b/frontend/src/api.ts
index 07db199..2e3e1ce 100644
--- a/frontend/src/api.ts
+++ b/frontend/src/api.ts
@@ -41,6 +41,10 @@ export const api = {
bootstrap: () => request("/api/bootstrap"),
config: () => request("/api/config"),
exchangeRates: () => request("/api/exchange-rates"),
+ wakeServers: (serverIds: string[]) => request("/api/live/wake", {
+ method: "POST",
+ body: JSON.stringify({ server_ids: serverIds }),
+ }),
refreshExchangeRates: () => request("/api/admin/exchange-rates/refresh", { method: "POST" }, true),
servers: () => request<{ servers: Server[] }>("/api/servers"),
adminServers: () => request<{ servers: AdminServer[] }>("/api/admin/servers", {}, true),
diff --git a/frontend/src/components/AdminPanel.tsx b/frontend/src/components/AdminPanel.tsx
index d452b31..a81ebd8 100644
--- a/frontend/src/components/AdminPanel.tsx
+++ b/frontend/src/components/AdminPanel.tsx
@@ -620,7 +620,7 @@ export function AdminPanel({
外观与展示
-
+
> : null}
diff --git a/frontend/src/demo.ts b/frontend/src/demo.ts
index 6e645c5..0105cfd 100644
--- a/frontend/src/demo.ts
+++ b/frontend/src/demo.ts
@@ -5,7 +5,6 @@ export const demoConfig: Config = {
site_description: "边缘节点与核心服务运行状态",
site_announcement: "",
logo_url: "",
- favicon_url: "",
locale: "zh-CN",
public_dashboard: true,
offline_threshold_seconds: 180,
diff --git a/frontend/src/transport.ts b/frontend/src/transport.ts
index 75deddf..02834be 100644
--- a/frontend/src/transport.ts
+++ b/frontend/src/transport.ts
@@ -3,16 +3,13 @@ import type { Server } from "./types";
const RECONNECT_DELAY = 3_000;
const HEARTBEAT_INTERVAL = 30_000;
-const WAKE_CONCURRENCY = 6;
-const WAKE_TIMEOUT = 5_000;
-const WAKE_SETTLE = 100;
export interface LiveTransportHandlers {
onServer: (server: Server) => void;
onBatch: (updates: BatchUpdate[], cached: boolean) => void;
onConnectedChange: (connected: boolean) => void;
- /** Online servers that should be woken when the overview connects. */
- wakeTargets: () => string[];
+ /** Wakes online Agents through one batched HTTP request. */
+ onOverviewConnected: () => Promise;
}
export interface LiveTransportOptions {
@@ -31,8 +28,8 @@ function endpoint(serverId: string | null): URL {
/**
* WSS-only live feed. Returns a disposer; the caller owns when the connection
- * starts and stops. Reconnects on close, and on the overview it opens a short
- * socket per online server so hibernating Durable Objects resume reporting.
+ * starts and stops. Reconnects on close, and asks the Worker to wake online
+ * Agents in one batch when the overview connection opens.
*/
export function connectLive(
{ serverId }: LiveTransportOptions,
@@ -40,48 +37,20 @@ export function connectLive(
): () => void {
const sockets: WebSocket[] = [];
const reconnects: number[] = [];
- const wakeTimers: number[] = [];
- const wakeSockets = new Set();
let cancelled = false;
+ let wakeInFlight = false;
const reportConnected = () => {
handlers.onConnectedChange(sockets.some((socket) => socket.readyState === WebSocket.OPEN));
};
const wakeOverviewAgents = () => {
- if (serverId || cancelled) return;
- const queue = Array.from(new Set(handlers.wakeTargets()));
- let cursor = 0;
- let active = 0;
- const launch = () => {
- while (!cancelled && active < WAKE_CONCURRENCY && cursor < queue.length) {
- const socket = new WebSocket(endpoint(queue[cursor++]));
- wakeSockets.add(socket);
- active += 1;
- let finished = false;
- let timeout = 0;
- const finish = () => {
- if (finished) return;
- finished = true;
- clearTimeout(timeout);
- wakeSockets.delete(socket);
- active -= 1;
- socket.onopen = null;
- socket.onclose = null;
- socket.onerror = null;
- if (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING) {
- socket.close();
- }
- launch();
- };
- timeout = window.setTimeout(finish, WAKE_TIMEOUT);
- wakeTimers.push(timeout);
- socket.onopen = () => wakeTimers.push(window.setTimeout(finish, WAKE_SETTLE));
- socket.onclose = finish;
- socket.onerror = finish;
- }
- };
- launch();
+ if (serverId || cancelled || wakeInFlight) return;
+ wakeInFlight = true;
+ void Promise.resolve()
+ .then(handlers.onOverviewConnected)
+ .catch(() => { /* Best effort; the live socket remains usable. */ })
+ .finally(() => { wakeInFlight = false; });
};
const connect = () => {
@@ -126,9 +95,6 @@ export function connectLive(
handlers.onConnectedChange(false);
clearInterval(heartbeat);
reconnects.forEach(clearTimeout);
- wakeTimers.forEach(clearTimeout);
- wakeSockets.forEach((socket) => socket.close());
- wakeSockets.clear();
sockets.forEach((socket) => socket.close());
sockets.length = 0;
};
diff --git a/frontend/src/types.ts b/frontend/src/types.ts
index 70f9c69..43439b8 100644
--- a/frontend/src/types.ts
+++ b/frontend/src/types.ts
@@ -2,8 +2,8 @@ export interface Config {
site_name: string;
site_description: string;
site_announcement: string;
+ /** 站点 Logo,同时用作浏览器标签图标。空则回落到 /logo.svg。 */
logo_url: string;
- favicon_url: string;
locale: "zh-CN" | "en";
public_dashboard: boolean;
offline_threshold_seconds: number;
diff --git a/migrations/initial_01.sql b/migrations/initial_01.sql
index 22d16de..f59e732 100644
--- a/migrations/initial_01.sql
+++ b/migrations/initial_01.sql
@@ -272,7 +272,6 @@ INSERT INTO settings(id, value, updated_at) VALUES (
"site_description": "轻量、实时的服务器运行状态",
"site_announcement": "",
"logo_url": "",
- "favicon_url": "",
"locale": "zh-CN",
"public_dashboard": "true",
"history_cache_version": "0",
diff --git a/scripts/websocket-smoke.mjs b/scripts/websocket-smoke.mjs
index ab8fac4..1c3d618 100644
--- a/scripts/websocket-smoke.mjs
+++ b/scripts/websocket-smoke.mjs
@@ -188,6 +188,27 @@ try {
throw new Error(`Invalid Agent config: ${JSON.stringify(config)}`);
}
if (!configOnly) {
+ const wakeHintPromise = waitForJsonMessage(
+ agent,
+ "batched overview wake hint",
+ (message) => message.type === "ack" && message.realtimeHint === true,
+ );
+ const wakeResponse = await fetch(new URL("/api/live/wake", baseUrl), {
+ method: "POST",
+ headers: {
+ Authorization: `Bearer ${adminToken}`,
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({ server_ids: [serverId] }),
+ });
+ if (wakeResponse.status !== 204) {
+ throw new Error(`Batch wake returned HTTP ${wakeResponse.status}: ${await wakeResponse.text()}`);
+ }
+ const wakeHint = await wakeHintPromise;
+ if (wakeHint.nextWssReportAfterMs !== 5_000) {
+ throw new Error(`Invalid batch wake hint: ${JSON.stringify(wakeHint)}`);
+ }
+
const timestamp = Math.floor(Date.now() / 1_000);
const baseSample = {
timestamp,
diff --git a/src/db.rs b/src/db.rs
index b8046ed..ea011bf 100644
--- a/src/db.rs
+++ b/src/db.rs
@@ -159,6 +159,11 @@ struct AgentConfigRow {
auto_update: i64,
}
+#[derive(Debug, Deserialize)]
+struct ServerIdRow {
+ id: String,
+}
+
#[derive(Debug, Deserialize)]
pub struct AgentLiveContext {
pub report_interval: i64,
@@ -365,7 +370,6 @@ pub struct SettingsView {
pub site_description: String,
pub site_announcement: String,
pub logo_url: String,
- pub favicon_url: String,
pub locale: String,
pub public_dashboard: bool,
pub offline_threshold_seconds: i64,
@@ -512,6 +516,27 @@ pub async fn list_servers(db: &D1Database, include_hidden: bool) -> Result Result> {
+ if ids.is_empty() {
+ return Ok(Vec::new());
+ }
+ let requested = serde_json::to_string(ids)?;
+ let rows = db
+ .prepare(
+ r#"SELECT s.id
+ FROM json_each(?1) requested
+ JOIN servers s ON s.id = CAST(requested.value AS TEXT)
+ WHERE s.hidden = 0
+ ORDER BY CAST(requested.key AS INTEGER)"#,
+ )
+ .bind(&[text(&requested)])?
+ .all()
+ .await?
+ .results::()?;
+ Ok(rows.into_iter().map(|row| row.id).collect())
+}
+
pub async fn get_server(
db: &D1Database,
id: &str,
@@ -1204,7 +1229,6 @@ pub async fn settings(
site_description: string_setting(&values, "site_description", "轻量、实时的服务器运行状态"),
site_announcement: string_setting(&values, "site_announcement", ""),
logo_url: string_setting(&values, "logo_url", ""),
- favicon_url: string_setting(&values, "favicon_url", ""),
locale: string_setting(&values, "locale", "zh-CN"),
public_dashboard: bool_setting(&values, "public_dashboard", true),
offline_threshold_seconds: values
@@ -1280,9 +1304,6 @@ pub async fn update_settings(
if let Some(value) = input.logo_url.as_deref() {
push_setting!("logo_url", value.trim());
}
- if let Some(value) = input.favicon_url.as_deref() {
- push_setting!("favicon_url", value.trim());
- }
if let Some(value) = input.locale.as_deref() {
push_setting!("locale", value.trim());
}
@@ -1658,24 +1679,22 @@ pub async fn cleanup_history(db: &D1Database, retention_days: i64) -> Result<()>
let delete_archive = db
.prepare("DELETE FROM metric_history_hourly WHERE timestamp < ?1")
.bind(&[number(cutoff)])?;
- // DDL 不和上面的语句放进同一个 batch:batch 是隐式事务,而 SQLite 不允许
- // 在事务里 ALTER TABLE ... RENAME。压缩+删除先提交,轮换单独走;轮换中途
- // 失败下一次 cron 重来即可,重复压缩无害(见上)。
+ // 压缩和保留期删除先原子提交;轮换随后在另一个原子 batch 中完成。
+ // 两个阶段都可独立重试,且轮换失败时不会留下缺少当前表的半完成状态。
db.batch(vec![compact, delete_archive]).await?;
rotate_recent_history(db).await
}
/// 丢弃上一代分钟历史,把当前代降级为上一代,再建一张空的当前代。
async fn rotate_recent_history(db: &D1Database) -> Result<()> {
- db.prepare("DROP TABLE IF EXISTS metric_history_old")
- .run()
- .await?;
- db.prepare("ALTER TABLE metric_history RENAME TO metric_history_old")
- .run()
- .await?;
- db.prepare(metric_history_ddl("metric_history"))
- .run()
- .await?;
+ // D1 batch guarantees transactional execution, including SQLite DDL. If any
+ // statement fails, the prior pair of recent-history tables remains intact.
+ db.batch(vec![
+ db.prepare("DROP TABLE IF EXISTS metric_history_old"),
+ db.prepare("ALTER TABLE metric_history RENAME TO metric_history_old"),
+ db.prepare(metric_history_ddl("metric_history")),
+ ])
+ .await?;
Ok(())
}
diff --git a/src/live.rs b/src/live.rs
index a7ca410..47ddebf 100644
--- a/src/live.rs
+++ b/src/live.rs
@@ -1,6 +1,7 @@
use std::cell::RefCell;
use std::collections::HashSet;
+use futures_util::{stream, StreamExt};
use serde::{Deserialize, Serialize};
use worker::*;
@@ -610,6 +611,29 @@ pub struct LiveHub {
active_d1_flushes: RefCell>,
}
+struct ActiveD1FlushGuard<'a> {
+ active: &'a RefCell>,
+ server_id: String,
+}
+
+impl<'a> ActiveD1FlushGuard<'a> {
+ fn acquire(active: &'a RefCell>, server_id: &str) -> Option {
+ if !active.borrow_mut().insert(server_id.to_string()) {
+ return None;
+ }
+ Some(Self {
+ active,
+ server_id: server_id.to_string(),
+ })
+ }
+}
+
+impl Drop for ActiveD1FlushGuard<'_> {
+ fn drop(&mut self) {
+ self.active.borrow_mut().remove(&self.server_id);
+ }
+}
+
impl LiveHub {
async fn broadcast_to_global_dashboard(&self, server_id: &str, payload: &str) -> bool {
let result = async {
@@ -664,7 +688,7 @@ impl LiveHub {
.is_empty()
}
- fn hint_agents(&self, server_id: Option<&str>) {
+ fn hint_agents(&self, server_id: Option<&str>, force_realtime: bool) {
let sockets = server_id.map_or_else(
|| self.state.get_websockets_with_tag("agents"),
|server_id| {
@@ -680,7 +704,8 @@ impl LiveHub {
let Some(agent_server_id) = attachment.server_id.as_deref() else {
continue;
};
- let realtime_active = !attachment.hidden && self.dashboard_active_for(agent_server_id);
+ let realtime_active = !attachment.hidden
+ && (force_realtime || self.dashboard_active_for(agent_server_id));
let Ok(payload) = live_ack_payload(
now,
false,
@@ -846,6 +871,10 @@ impl DurableObject for LiveHub {
}
return Response::empty();
}
+ Some("hint-agents") => {
+ self.hint_agents(None, true);
+ return Response::empty();
+ }
Some("record-alert-sample") => {
let record = req.json::().await?;
self.record_alert_point(&record.server_id, &record.point)
@@ -947,7 +976,7 @@ impl DurableObject for LiveHub {
pair.server.serialize_attachment(dashboard_attachment())?;
self.state.accept_websocket_with_tags(&pair.server, &[&tag]);
self.replay_latest(&pair.server, server_id.as_deref());
- self.hint_agents(server_id.as_deref());
+ self.hint_agents(server_id.as_deref(), false);
Response::from_websocket(pair.client)
}
@@ -1043,16 +1072,21 @@ impl DurableObject for LiveHub {
let due_for_d1 = received_at.saturating_sub(attachment.last_d1_write_at)
>= HISTORY_WRITE_INTERVAL_SECONDS;
- let flush_active = self.active_d1_flushes.borrow().contains(&server_id);
- let mut flush = if !unpersisted_reports.is_empty()
- && (due_for_d1 || attachment.d1_flush.is_some())
- && !flush_active
- {
- let database = self.env.d1("DB")?;
- begin_d1_flush(&mut attachment).map(|snapshot| (database, snapshot))
- } else {
- None
- };
+ let should_flush =
+ !unpersisted_reports.is_empty() && (due_for_d1 || attachment.d1_flush.is_some());
+ let mut flush_guard = None;
+ let mut flush = None;
+ if should_flush {
+ // Acquire before any external await below. Durable Object events may
+ // interleave while broadcasting or writing D1.
+ if let Some(guard) = ActiveD1FlushGuard::acquire(&self.active_d1_flushes, &server_id) {
+ let database = self.env.d1("DB")?;
+ flush = begin_d1_flush(&mut attachment).map(|snapshot| (database, snapshot));
+ if flush.is_some() {
+ flush_guard = Some(guard);
+ }
+ }
+ }
if flush.is_some() {
if !trim_socket_attachment(&mut attachment) {
return ws.close(Some(1011), Some("live state exceeds attachment limit"));
@@ -1081,9 +1115,6 @@ impl DurableObject for LiveHub {
let mut persistence_error = false;
let mut attachment_serialized = false;
if let Some((database, snapshot)) = flush.take() {
- self.active_d1_flushes
- .borrow_mut()
- .insert(server_id.clone());
let history_point = snapshot
.history
.point()
@@ -1110,7 +1141,6 @@ impl DurableObject for LiveHub {
}
Ok(inserted) => (true, inserted),
};
- self.active_d1_flushes.borrow_mut().remove(&server_id);
let Some(mut current_attachment) = ws.deserialize_attachment::()?
else {
@@ -1134,6 +1164,7 @@ impl DurableObject for LiveHub {
attachment = current_attachment;
attachment_serialized = true;
persisted = write_succeeded;
+ drop(flush_guard.take());
if newly_written {
match crate::db::has_enabled_alert_rules(&database).await {
@@ -1314,6 +1345,36 @@ pub async fn disconnect_agents(env: &Env, server_ids: &[String]) -> Result<()> {
Ok(())
}
+pub async fn wake_agents(env: &Env, server_ids: &[String]) -> Result<()> {
+ if server_ids.is_empty() {
+ return Ok(());
+ }
+ let namespace = env.durable_object("LIVE_HUB")?;
+ let results = stream::iter(server_ids.iter())
+ .map(|server_id| {
+ let namespace = &namespace;
+ async move {
+ let stub = namespace
+ .id_from_name(&server_hub_name(server_id))?
+ .get_stub()?;
+ let headers = Headers::new();
+ headers.set("X-Live-Action", "hint-agents")?;
+ let mut init = RequestInit::new();
+ init.with_method(Method::Post).with_headers(headers);
+ let req = Request::new_with_init("https://live.internal/hint-agents", &init)?;
+ let response = stub.fetch_with_request(req).await?;
+ ensure_live_success(&response, "hint-agents")
+ }
+ })
+ .buffer_unordered(6)
+ .collect::>()
+ .await;
+ for result in results {
+ result?;
+ }
+ Ok(())
+}
+
pub async fn upgrade(req: Request, env: &Env) -> Result {
let namespace = env.durable_object("LIVE_HUB")?;
let server_id = req
@@ -1403,15 +1464,17 @@ mod tests {
agent_wss_interval_ms, apply_live_traffic, batch_update_parts_at, begin_d1_flush,
cached_update_value, consume_agent_message_budget, finish_d1_flush, live_ack_payload,
next_d1_write_ms, outbound_close_code, trim_cached_live_samples, trim_socket_attachment,
- update_alert_window, AgentMessageBudget, AlertEvaluationServer, AlertWindowSample,
- CachedLiveSample, SocketAttachment, TrafficAttachment, ALERT_WINDOW_SECONDS,
- BROWSER_OMITTED_REPORT_FIELDS, CACHED_LIVE_TTL_SECONDS, MAX_AGENT_BYTES_PER_WINDOW,
- MAX_AGENT_MESSAGES_PER_WINDOW, MAX_AGENT_SAMPLES_PER_WINDOW, MAX_CACHED_LIVE_BYTES,
- MAX_CACHED_LIVE_SAMPLES, MAX_SERIALIZED_ATTACHMENT_BYTES,
+ update_alert_window, ActiveD1FlushGuard, AgentMessageBudget, AlertEvaluationServer,
+ AlertWindowSample, CachedLiveSample, SocketAttachment, TrafficAttachment,
+ ALERT_WINDOW_SECONDS, BROWSER_OMITTED_REPORT_FIELDS, CACHED_LIVE_TTL_SECONDS,
+ MAX_AGENT_BYTES_PER_WINDOW, MAX_AGENT_MESSAGES_PER_WINDOW, MAX_AGENT_SAMPLES_PER_WINDOW,
+ MAX_CACHED_LIVE_BYTES, MAX_CACHED_LIVE_SAMPLES, MAX_SERIALIZED_ATTACHMENT_BYTES,
};
use crate::db::HistoryMetricAggregate;
use crate::latency::LatencyMetricAggregates;
use crate::models::{AgentLatencyResult, AgentReport, AlertRuleView, HistoryPoint};
+ use std::cell::RefCell;
+ use std::collections::HashSet;
fn alert_point(timestamp: i64, cpu: f64, net_in: f64) -> HistoryPoint {
HistoryPoint {
@@ -1499,6 +1562,16 @@ mod tests {
assert_eq!(outbound_close_code(5000), None);
}
+ #[test]
+ fn d1_flush_guard_is_exclusive_and_releases_on_drop() {
+ let active = RefCell::new(HashSet::new());
+ let guard = ActiveD1FlushGuard::acquire(&active, "node-a").expect("first flush lock");
+ assert!(ActiveD1FlushGuard::acquire(&active, "node-a").is_none());
+ assert!(ActiveD1FlushGuard::acquire(&active, "node-b").is_some());
+ drop(guard);
+ assert!(ActiveD1FlushGuard::acquire(&active, "node-a").is_some());
+ }
+
#[test]
fn bounds_agent_messages_per_connection_window() {
let mut budget = AgentMessageBudget::default();
diff --git a/src/models.rs b/src/models.rs
index 680e689..932ddd5 100644
--- a/src/models.rs
+++ b/src/models.rs
@@ -55,7 +55,6 @@ pub struct SettingsInput {
pub site_description: Option,
pub site_announcement: Option,
pub logo_url: Option,
- pub favicon_url: Option,
pub locale: Option,
pub public_dashboard: Option,
pub offline_threshold_seconds: Option,
diff --git a/src/routes/admin.rs b/src/routes/admin.rs
index 4c3a114..782cd5d 100644
--- a/src/routes/admin.rs
+++ b/src/routes/admin.rs
@@ -663,6 +663,7 @@ async fn settings_patch(req: &mut Request, ctx: &RouteContext) -> Result Result,
+}
+
fn request_hostname(req: &Request) -> Option {
req.url()
.ok()?
@@ -55,6 +63,9 @@ pub(crate) async fn route(mut req: Request, ctx: &RouteContext) -> Result serde_json::Value {
"site_description": settings.site_description,
"site_announcement": settings.site_announcement,
"logo_url": settings.logo_url,
- "favicon_url": settings.favicon_url,
"locale": settings.locale,
"public_dashboard": settings.public_dashboard,
"offline_threshold_seconds": settings.offline_threshold_seconds,
@@ -294,6 +304,28 @@ async fn servers(ctx: &RouteContext) -> Result {
json(&serde_json::json!({ "servers": servers }), 200)
}
+async fn wake_servers(req: &mut Request, ctx: &RouteContext) -> Result {
+ if let Some(denied) = public_access_denied(ctx) {
+ return denied;
+ }
+ let input: WakeServersRequest = match request_json(req, API_JSON_MAX_BYTES).await {
+ Ok(value) => value,
+ Err(_) => return error("请求格式无效", 400),
+ };
+ if input.server_ids.len() > MAX_WAKE_SERVER_IDS
+ || input
+ .server_ids
+ .iter()
+ .any(|id| id.is_empty() || id.len() > 80 || id.contains('/'))
+ || input.server_ids.iter().collect::>().len() != input.server_ids.len()
+ {
+ return error("节点列表无效", 400);
+ }
+ let visible_ids = db::visible_server_ids(&ctx.database, &input.server_ids).await?;
+ live::wake_agents(&ctx.env, &visible_ids).await?;
+ no_content()
+}
+
async fn history(req: &Request, ctx: &RouteContext) -> Result {
if let Some(denied) = public_access_denied(ctx) {
return denied;
diff --git a/wrangler.jsonc b/wrangler.jsonc
index 2e5cb37..e92100d 100644
--- a/wrangler.jsonc
+++ b/wrangler.jsonc
@@ -63,15 +63,13 @@
"crons": ["*/5 * * * *"]
},
"observability": {
- "enabled": true,
+ "enabled": false,
"logs": {
- "enabled": true,
- "head_sampling_rate": 1,
- "invocation_logs": true
+ "enabled": false,
+ "invocation_logs": false
},
"traces": {
- "enabled": true,
- "head_sampling_rate": 0.05
+ "enabled": false
}
},
"dev": {