mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-03-15 07:26:44 +08:00
feat: 增加WOL服务端历史记录并支持跨浏览器同步
This commit is contained in:
@@ -120,6 +120,26 @@ impl ConfigStore {
|
|||||||
.execute(pool)
|
.execute(pool)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
sqlx::query(
|
||||||
|
r#"
|
||||||
|
CREATE TABLE IF NOT EXISTS wol_history (
|
||||||
|
mac_address TEXT PRIMARY KEY,
|
||||||
|
updated_at INTEGER NOT NULL
|
||||||
|
)
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.execute(pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
sqlx::query(
|
||||||
|
r#"
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_wol_history_updated_at
|
||||||
|
ON wol_history(updated_at DESC)
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.execute(pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2660,6 +2660,10 @@ pub async fn msd_drive_mkdir(
|
|||||||
|
|
||||||
use crate::atx::{AtxState, PowerStatus};
|
use crate::atx::{AtxState, PowerStatus};
|
||||||
|
|
||||||
|
const WOL_HISTORY_MAX_ENTRIES: i64 = 50;
|
||||||
|
const WOL_HISTORY_DEFAULT_LIMIT: usize = 5;
|
||||||
|
const WOL_HISTORY_MAX_LIMIT: usize = 50;
|
||||||
|
|
||||||
/// ATX state response
|
/// ATX state response
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
pub struct AtxStateResponse {
|
pub struct AtxStateResponse {
|
||||||
@@ -2765,11 +2769,78 @@ pub struct WolRequest {
|
|||||||
pub mac_address: String,
|
pub mac_address: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Default)]
|
||||||
|
pub struct WolHistoryQuery {
|
||||||
|
/// Maximum history entries to return
|
||||||
|
pub limit: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct WolHistoryEntry {
|
||||||
|
pub mac_address: String,
|
||||||
|
pub updated_at: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct WolHistoryResponse {
|
||||||
|
pub history: Vec<WolHistoryEntry>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_wol_mac_address(mac_address: &str) -> String {
|
||||||
|
let normalized = mac_address.trim().to_uppercase().replace('-', ":");
|
||||||
|
|
||||||
|
if normalized.len() == 12 && normalized.chars().all(|c| c.is_ascii_hexdigit()) {
|
||||||
|
let mut mac_with_separator = String::with_capacity(17);
|
||||||
|
for (index, chunk) in normalized.as_bytes().chunks(2).enumerate() {
|
||||||
|
if index > 0 {
|
||||||
|
mac_with_separator.push(':');
|
||||||
|
}
|
||||||
|
mac_with_separator.push(chunk[0] as char);
|
||||||
|
mac_with_separator.push(chunk[1] as char);
|
||||||
|
}
|
||||||
|
mac_with_separator
|
||||||
|
} else {
|
||||||
|
normalized
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn record_wol_history(state: &Arc<AppState>, mac_address: &str) -> Result<()> {
|
||||||
|
sqlx::query(
|
||||||
|
r#"
|
||||||
|
INSERT INTO wol_history (mac_address, updated_at)
|
||||||
|
VALUES (?1, CAST(strftime('%s', 'now') AS INTEGER))
|
||||||
|
ON CONFLICT(mac_address) DO UPDATE SET
|
||||||
|
updated_at = excluded.updated_at
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(mac_address)
|
||||||
|
.execute(state.config.pool())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
sqlx::query(
|
||||||
|
r#"
|
||||||
|
DELETE FROM wol_history
|
||||||
|
WHERE mac_address NOT IN (
|
||||||
|
SELECT mac_address FROM wol_history
|
||||||
|
ORDER BY updated_at DESC
|
||||||
|
LIMIT ?1
|
||||||
|
)
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(WOL_HISTORY_MAX_ENTRIES)
|
||||||
|
.execute(state.config.pool())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Send Wake-on-LAN magic packet
|
/// Send Wake-on-LAN magic packet
|
||||||
pub async fn atx_wol(
|
pub async fn atx_wol(
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
Json(req): Json<WolRequest>,
|
Json(req): Json<WolRequest>,
|
||||||
) -> Result<Json<LoginResponse>> {
|
) -> Result<Json<LoginResponse>> {
|
||||||
|
let mac_address = normalize_wol_mac_address(&req.mac_address);
|
||||||
|
|
||||||
// Get WOL interface from config
|
// Get WOL interface from config
|
||||||
let config = state.config.get();
|
let config = state.config.get();
|
||||||
let interface = if config.atx.wol_interface.is_empty() {
|
let interface = if config.atx.wol_interface.is_empty() {
|
||||||
@@ -2779,14 +2850,51 @@ pub async fn atx_wol(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Send WOL packet
|
// Send WOL packet
|
||||||
crate::atx::send_wol(&req.mac_address, interface)?;
|
crate::atx::send_wol(&mac_address, interface)?;
|
||||||
|
|
||||||
|
if let Err(error) = record_wol_history(&state, &mac_address).await {
|
||||||
|
warn!("Failed to persist WOL history: {}", error);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(Json(LoginResponse {
|
Ok(Json(LoginResponse {
|
||||||
success: true,
|
success: true,
|
||||||
message: Some(format!("WOL packet sent to {}", req.mac_address)),
|
message: Some(format!("WOL packet sent to {}", mac_address)),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get WOL history
|
||||||
|
pub async fn atx_wol_history(
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
Query(query): Query<WolHistoryQuery>,
|
||||||
|
) -> Result<Json<WolHistoryResponse>> {
|
||||||
|
let limit = query
|
||||||
|
.limit
|
||||||
|
.unwrap_or(WOL_HISTORY_DEFAULT_LIMIT)
|
||||||
|
.clamp(1, WOL_HISTORY_MAX_LIMIT);
|
||||||
|
|
||||||
|
let rows: Vec<(String, i64)> = sqlx::query_as(
|
||||||
|
r#"
|
||||||
|
SELECT mac_address, updated_at
|
||||||
|
FROM wol_history
|
||||||
|
ORDER BY updated_at DESC
|
||||||
|
LIMIT ?1
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(limit as i64)
|
||||||
|
.fetch_all(state.config.pool())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let history = rows
|
||||||
|
.into_iter()
|
||||||
|
.map(|(mac_address, updated_at)| WolHistoryEntry {
|
||||||
|
mac_address,
|
||||||
|
updated_at,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(Json(WolHistoryResponse { history }))
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Audio Control
|
// Audio Control
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
@@ -166,6 +166,7 @@ pub fn create_router(state: Arc<AppState>) -> Router {
|
|||||||
.route("/atx/status", get(handlers::atx_status))
|
.route("/atx/status", get(handlers::atx_status))
|
||||||
.route("/atx/power", post(handlers::atx_power))
|
.route("/atx/power", post(handlers::atx_power))
|
||||||
.route("/atx/wol", post(handlers::atx_wol))
|
.route("/atx/wol", post(handlers::atx_wol))
|
||||||
|
.route("/atx/wol/history", get(handlers::atx_wol_history))
|
||||||
// Device discovery endpoints
|
// Device discovery endpoints
|
||||||
.route("/devices/atx", get(handlers::devices::list_atx_devices))
|
.route("/devices/atx", get(handlers::devices::list_atx_devices))
|
||||||
// Extension management endpoints
|
// Extension management endpoints
|
||||||
|
|||||||
@@ -136,6 +136,15 @@ export const msdConfigApi = {
|
|||||||
// ===== ATX 配置 API =====
|
// ===== ATX 配置 API =====
|
||||||
import type { AtxDevices } from '@/types/generated'
|
import type { AtxDevices } from '@/types/generated'
|
||||||
|
|
||||||
|
export interface WolHistoryEntry {
|
||||||
|
mac_address: string
|
||||||
|
updated_at: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WolHistoryResponse {
|
||||||
|
history: WolHistoryEntry[]
|
||||||
|
}
|
||||||
|
|
||||||
export const atxConfigApi = {
|
export const atxConfigApi = {
|
||||||
/**
|
/**
|
||||||
* 获取 ATX 配置
|
* 获取 ATX 配置
|
||||||
@@ -166,6 +175,13 @@ export const atxConfigApi = {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ mac_address: macAddress }),
|
body: JSON.stringify({ mac_address: macAddress }),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 WOL 历史记录(服务端持久化)
|
||||||
|
* @param limit 返回条数(1-50)
|
||||||
|
*/
|
||||||
|
getWolHistory: (limit = 5) =>
|
||||||
|
request<WolHistoryResponse>(`/atx/wol/history?limit=${Math.max(1, Math.min(50, limit))}`),
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== Audio 配置 API =====
|
// ===== Audio 配置 API =====
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed, watch } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
} from '@/components/ui/alert-dialog'
|
} from '@/components/ui/alert-dialog'
|
||||||
import { Power, RotateCcw, CircleDot, Wifi, Send } from 'lucide-vue-next'
|
import { Power, RotateCcw, CircleDot, Wifi, Send } from 'lucide-vue-next'
|
||||||
|
import { atxConfigApi } from '@/api/config'
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'close'): void
|
(e: 'close'): void
|
||||||
@@ -39,6 +40,7 @@ const confirmAction = ref<'short' | 'long' | 'reset' | null>(null)
|
|||||||
const wolMacAddress = ref('')
|
const wolMacAddress = ref('')
|
||||||
const wolHistory = ref<string[]>([])
|
const wolHistory = ref<string[]>([])
|
||||||
const wolSending = ref(false)
|
const wolSending = ref(false)
|
||||||
|
const wolLoadingHistory = ref(false)
|
||||||
|
|
||||||
const powerStateColor = computed(() => {
|
const powerStateColor = computed(() => {
|
||||||
switch (powerState.value) {
|
switch (powerState.value) {
|
||||||
@@ -103,16 +105,11 @@ function sendWol() {
|
|||||||
|
|
||||||
emit('wol', mac)
|
emit('wol', mac)
|
||||||
|
|
||||||
// Add to history if not exists
|
// Optimistic update, then sync from server after request likely completes
|
||||||
if (!wolHistory.value.includes(mac)) {
|
wolHistory.value = [mac, ...wolHistory.value.filter(item => item !== mac)].slice(0, 5)
|
||||||
wolHistory.value.unshift(mac)
|
setTimeout(() => {
|
||||||
// Keep only last 5
|
loadWolHistory().catch(() => {})
|
||||||
if (wolHistory.value.length > 5) {
|
}, 1200)
|
||||||
wolHistory.value.pop()
|
|
||||||
}
|
|
||||||
// Save to localStorage
|
|
||||||
localStorage.setItem('wol_history', JSON.stringify(wolHistory.value))
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
wolSending.value = false
|
wolSending.value = false
|
||||||
@@ -123,15 +120,27 @@ function selectFromHistory(mac: string) {
|
|||||||
wolMacAddress.value = mac
|
wolMacAddress.value = mac
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load WOL history on mount
|
async function loadWolHistory() {
|
||||||
const savedHistory = localStorage.getItem('wol_history')
|
wolLoadingHistory.value = true
|
||||||
if (savedHistory) {
|
|
||||||
try {
|
try {
|
||||||
wolHistory.value = JSON.parse(savedHistory)
|
const response = await atxConfigApi.getWolHistory(5)
|
||||||
} catch (e) {
|
wolHistory.value = response.history.map(item => item.mac_address)
|
||||||
|
} catch {
|
||||||
wolHistory.value = []
|
wolHistory.value = []
|
||||||
|
} finally {
|
||||||
|
wolLoadingHistory.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => activeTab.value,
|
||||||
|
(tab) => {
|
||||||
|
if (tab === 'wol') {
|
||||||
|
loadWolHistory().catch(() => {})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -227,6 +236,10 @@ if (savedHistory) {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<p v-if="wolLoadingHistory" class="text-xs text-muted-foreground">
|
||||||
|
{{ t('common.loading') }}
|
||||||
|
</p>
|
||||||
|
|
||||||
<!-- History -->
|
<!-- History -->
|
||||||
<div v-if="wolHistory.length > 0" class="space-y-2">
|
<div v-if="wolHistory.length > 0" class="space-y-2">
|
||||||
<Separator />
|
<Separator />
|
||||||
|
|||||||
Reference in New Issue
Block a user