diff --git a/.gitignore b/.gitignore
index 7207c4e..c09cea9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,15 +6,22 @@ node_modules
# Build outputs
dist
dist-ssr
+out
*.local
# Tauri
src-tauri/target
src-tauri/Cargo.lock
src-tauri/gen
+.cargo-local/
# Database
data.sql
+*.sqlite
+*.sqlite3
+*.db
+*.db-shm
+*.db-wal
# Logs
logs
diff --git a/index.html b/index.html
index 63d0740..5dc77af 100644
--- a/index.html
+++ b/index.html
@@ -3,6 +3,7 @@
+
SecScore
diff --git a/public/favicon.svg b/public/favicon.svg
new file mode 100644
index 0000000..ae889c5
--- /dev/null
+++ b/public/favicon.svg
@@ -0,0 +1,11 @@
+
diff --git a/src-tauri/src/commands/data.rs b/src-tauri/src/commands/data.rs
index 1a4f81b..266219e 100644
--- a/src-tauri/src/commands/data.rs
+++ b/src-tauri/src/commands/data.rs
@@ -1,5 +1,4 @@
use parking_lot::RwLock;
-use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tauri::State;
diff --git a/src-tauri/src/commands/event.rs b/src-tauri/src/commands/event.rs
index f0dec05..b095006 100644
--- a/src-tauri/src/commands/event.rs
+++ b/src-tauri/src/commands/event.rs
@@ -1,4 +1,4 @@
-use chrono::{Datelike, Duration, TimeZone, Timelike, Utc};
+use chrono::{Datelike, Duration, Timelike, Utc};
use parking_lot::RwLock;
use sea_orm::{
ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, QueryOrder, QuerySelect, Set,
diff --git a/src-tauri/src/commands/reason.rs b/src-tauri/src/commands/reason.rs
index dfa772d..cbb7010 100644
--- a/src-tauri/src/commands/reason.rs
+++ b/src-tauri/src/commands/reason.rs
@@ -1,5 +1,5 @@
use parking_lot::RwLock;
-use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryOrder, Set};
+use sea_orm::{ActiveModelTrait, EntityTrait, QueryOrder, Set};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tauri::State;
diff --git a/src-tauri/src/commands/window.rs b/src-tauri/src/commands/window.rs
index 9377ae0..2b9f6d1 100644
--- a/src-tauri/src/commands/window.rs
+++ b/src-tauri/src/commands/window.rs
@@ -4,8 +4,6 @@ use tauri::{AppHandle, Manager};
use crate::state::AppState;
-use super::response::IpcResponse;
-
#[tauri::command]
pub async fn window_minimize(
app: AppHandle,
diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs
index 0303812..c9e06f0 100644
--- a/src-tauri/src/lib.rs
+++ b/src-tauri/src/lib.rs
@@ -29,7 +29,7 @@ pub fn setup_app(app: &mut App) -> Result<(), Box> {
}
fn setup_database(app: &mut App) -> Result<(), Box> {
- let handle = app.handle();
+ let handle = app.handle().clone();
let db_path = if cfg!(debug_assertions) {
std::path::PathBuf::from("data.sql")
} else {
@@ -52,7 +52,7 @@ fn setup_database(app: &mut App) -> Result<(), Box> {
match create_sqlite_connection(&db_path_str).await {
Ok(conn) => {
let state = handle.state::();
- let mut state_guard = state.write();
+ let state_guard = state.write();
let mut db_guard = state_guard.db.write();
*db_guard = Some(conn);
eprintln!("Database connected to: {}", db_path_str);
diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs
index 774c263..e47aa28 100644
--- a/src-tauri/src/main.rs
+++ b/src-tauri/src/main.rs
@@ -9,9 +9,9 @@ fn main() {
tauri::Builder::default()
.plugin(tauri_plugin_shell::init())
.setup(|app| {
- setup_app(app)?;
let state = AppState::new(app.handle().clone());
app.manage(Arc::new(RwLock::new(state)));
+ setup_app(app)?;
Ok(())
})
.invoke_handler(tauri::generate_handler![
diff --git a/src/App.tsx b/src/App.tsx
index a825e06..38a99c2 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -15,16 +15,34 @@ function MainContent(): React.JSX.Element {
const [messageApi, contextHolder] = message.useMessage()
useEffect(() => {
- if (!(window as any).api) return
- const unlisten = (window as any).api.onNavigate((route: string) => {
- const currentPath = location.pathname === "/" ? "/home" : location.pathname
- const targetPath = route === "/" ? "/home" : route
+ const api = (window as any).api
+ if (!api) return
- if (currentPath !== targetPath) {
- navigate(route)
- }
- })
- return () => unlisten()
+ let disposed = false
+ let unlisten: (() => void) | null = null
+
+ api
+ .onNavigate((route: string) => {
+ const currentPath = location.pathname === "/" ? "/home" : location.pathname
+ const targetPath = route === "/" ? "/home" : route
+
+ if (currentPath !== targetPath) {
+ navigate(route)
+ }
+ })
+ .then((fn: () => void) => {
+ if (disposed) {
+ fn()
+ return
+ }
+ unlisten = fn
+ })
+ .catch(() => void 0)
+
+ return () => {
+ disposed = true
+ if (unlisten) unlisten()
+ }
}, [navigate, location.pathname])
const [wizardVisible, setWizardVisible] = useState(false)
diff --git a/src/components/ContentArea.tsx b/src/components/ContentArea.tsx
index b175d49..b968003 100644
--- a/src/components/ContentArea.tsx
+++ b/src/components/ContentArea.tsx
@@ -2,6 +2,7 @@ import React, { Suspense, lazy } from "react"
import { Layout, Space, Button, Tag, Spin } from "antd"
import { Routes, Route, Navigate } from "react-router-dom"
import { useTranslation } from "react-i18next"
+import { WindowControls } from "./WindowControls"
const Home = lazy(() => import("./Home").then((m) => ({ default: m.Home })))
const StudentManager = lazy(() =>
@@ -67,6 +68,7 @@ export function ContentArea({
background: "var(--ss-header-bg)",
borderBottom: "1px solid var(--ss-border-color)",
flexShrink: 0,
+ WebkitAppRegion: "drag",
} as React.CSSProperties
}
>
@@ -76,7 +78,8 @@ export function ContentArea({
{
display: "flex",
alignItems: "center",
- paddingRight: "12px",
+ paddingRight: "8px",
+ WebkitAppRegion: "no-drag",
} as React.CSSProperties
}
>
@@ -94,6 +97,7 @@ export function ContentArea({
)}
+
diff --git a/src/components/Settings.tsx b/src/components/Settings.tsx
index ccc231c..25f6ee7 100644
--- a/src/components/Settings.tsx
+++ b/src/components/Settings.tsx
@@ -95,20 +95,36 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission
useEffect(() => {
loadAll()
- if (!(window as any).api) return
- const unsubscribe = (window as any).api.onSettingChanged((change: any) => {
- setSettings((prev) => {
- if (change?.key === "log_level") return { ...prev, log_level: change.value }
- if (change?.key === "is_wizard_completed")
- return { ...prev, is_wizard_completed: change.value }
- if (change?.key === "window_zoom") return { ...prev, window_zoom: change.value }
- if (change?.key === "auto_score_enabled")
- return { ...prev, auto_score_enabled: change.value }
- return prev
+ const api = (window as any).api
+ if (!api) return
+
+ let disposed = false
+ let unlisten: (() => void) | null = null
+
+ api
+ .onSettingChanged((change: any) => {
+ setSettings((prev) => {
+ if (change?.key === "log_level") return { ...prev, log_level: change.value }
+ if (change?.key === "is_wizard_completed")
+ return { ...prev, is_wizard_completed: change.value }
+ if (change?.key === "window_zoom") return { ...prev, window_zoom: change.value }
+ if (change?.key === "auto_score_enabled")
+ return { ...prev, auto_score_enabled: change.value }
+ return prev
+ })
})
- })
+ .then((fn: () => void) => {
+ if (disposed) {
+ fn()
+ return
+ }
+ unlisten = fn
+ })
+ .catch(() => void 0)
+
return () => {
- if (typeof unsubscribe === "function") unsubscribe()
+ disposed = true
+ if (unlisten) unlisten()
}
}, [])
diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx
index 4d34745..a3a508d 100644
--- a/src/components/Sidebar.tsx
+++ b/src/components/Sidebar.tsx
@@ -39,15 +39,30 @@ export function Sidebar({ activeMenu, permission, onMenuChange }: SidebarProps):
const handleStatusChange = () => {
loadDbStatus()
}
- if ((window as any).api) {
- const unsubscribe = (window as any).api.onSettingChanged(
- (change: { key: string; value: any }) => {
- if (change.key === "pg_connection_status") {
- handleStatusChange()
- }
+ const api = (window as any).api
+ if (!api) return
+
+ let disposed = false
+ let unlisten: (() => void) | null = null
+
+ api
+ .onSettingChanged((change: { key: string; value: any }) => {
+ if (change.key === "pg_connection_status") {
+ handleStatusChange()
}
- )
- return unsubscribe
+ })
+ .then((fn: () => void) => {
+ if (disposed) {
+ fn()
+ return
+ }
+ unlisten = fn
+ })
+ .catch(() => void 0)
+
+ return () => {
+ disposed = true
+ if (unlisten) unlisten()
}
}, [])
diff --git a/src/components/TitleBar.tsx b/src/components/TitleBar.tsx
index 1b333eb..75e592f 100644
--- a/src/components/TitleBar.tsx
+++ b/src/components/TitleBar.tsx
@@ -16,13 +16,34 @@ export function TitleBar({ children }: TitleBarProps): React.JSX.Element {
const [isMaximized, setIsMaximized] = useState(false)
useEffect(() => {
- if (!(window as any).api) return
- ;(window as any).api.windowIsMaximized().then((v: boolean) => setIsMaximized(v))
+ const api = (window as any).api
+ if (!api) return
- const cleanup = (window as any).api.onWindowMaximizedChanged((maximized: boolean) => {
- setIsMaximized(maximized)
- })
- return cleanup
+ let disposed = false
+ let unlisten: (() => void) | null = null
+
+ api
+ .windowIsMaximized()
+ .then((v: boolean) => setIsMaximized(v))
+ .catch(() => void 0)
+
+ api
+ .onWindowMaximizedChanged((maximized: boolean) => {
+ setIsMaximized(maximized)
+ })
+ .then((fn: () => void) => {
+ if (disposed) {
+ fn()
+ return
+ }
+ unlisten = fn
+ })
+ .catch(() => void 0)
+
+ return () => {
+ disposed = true
+ if (unlisten) unlisten()
+ }
}, [])
const minimize = () => {
diff --git a/src/components/WindowControls.tsx b/src/components/WindowControls.tsx
index b6c4596..f24d01e 100644
--- a/src/components/WindowControls.tsx
+++ b/src/components/WindowControls.tsx
@@ -11,13 +11,34 @@ export function WindowControls(): React.JSX.Element {
const [isMaximized, setIsMaximized] = useState(false)
useEffect(() => {
- if (!(window as any).api) return
- ;(window as any).api.windowIsMaximized().then((v: boolean) => setIsMaximized(v))
+ const api = (window as any).api
+ if (!api) return
- const cleanup = (window as any).api.onWindowMaximizedChanged((maximized: boolean) => {
- setIsMaximized(maximized)
- })
- return cleanup
+ let disposed = false
+ let unlisten: (() => void) | null = null
+
+ api
+ .windowIsMaximized()
+ .then((v: boolean) => setIsMaximized(v))
+ .catch(() => void 0)
+
+ api
+ .onWindowMaximizedChanged((maximized: boolean) => {
+ setIsMaximized(maximized)
+ })
+ .then((fn: () => void) => {
+ if (disposed) {
+ fn()
+ return
+ }
+ unlisten = fn
+ })
+ .catch(() => void 0)
+
+ return () => {
+ disposed = true
+ if (unlisten) unlisten()
+ }
}, [])
const minimize = () => {
diff --git a/src/contexts/ThemeContext.tsx b/src/contexts/ThemeContext.tsx
index 23b3e59..3f9b4e0 100644
--- a/src/contexts/ThemeContext.tsx
+++ b/src/contexts/ThemeContext.tsx
@@ -120,23 +120,36 @@ export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ childre
}, [applyThemeConfig])
useEffect(() => {
- if (!(window as any).api) return
+ const api = (window as any).api
+ if (!api) return
+
;(async () => {
await loadThemes()
await loadCurrentTheme()
})()
- const unsubscribe = (window as any).api.onThemeChanged((theme: themeConfig) => {
- setCurrentTheme(theme)
- currentThemeRef.current = theme
- applyThemeConfig(theme)
- loadThemes()
- })
+ let disposed = false
+ let unlisten: (() => void) | null = null
+
+ api
+ .onThemeChanged((theme: themeConfig) => {
+ setCurrentTheme(theme)
+ currentThemeRef.current = theme
+ applyThemeConfig(theme)
+ loadThemes()
+ })
+ .then((fn: () => void) => {
+ if (disposed) {
+ fn()
+ return
+ }
+ unlisten = fn
+ })
+ .catch(() => void 0)
return () => {
- if (typeof unsubscribe === "function") {
- unsubscribe()
- }
+ disposed = true
+ if (unlisten) unlisten()
}
}, [applyThemeConfig, loadCurrentTheme, loadThemes])
diff --git a/src/main.tsx b/src/main.tsx
index 0f8455c..a0d52c0 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -7,6 +7,11 @@ import App from "./App"
import { ClientContext } from "./ClientContext"
import { StudentService } from "./services/StudentService"
import { ServiceProvider } from "./contexts/ServiceContext"
+import { api } from "./preload/types"
+
+if (!(window as any).api) {
+ ;(window as any).api = api
+}
const ctx = new ClientContext()
new StudentService(ctx)
@@ -19,7 +24,7 @@ const safeWriteLog = (payload: {
try {
const api = (window as any).api
if (!api?.writeLog) return
- api.writeLog(payload)
+ Promise.resolve(api.writeLog(payload)).catch(() => void 0)
} catch {
return
}
diff --git a/src/preload/types.ts b/src/preload/types.ts
index 8179ae5..908cd63 100644
--- a/src/preload/types.ts
+++ b/src/preload/types.ts
@@ -173,6 +173,9 @@ const api = {
windowMaximize: (): Promise => invoke("window_maximize"),
windowClose: (): Promise => invoke("window_close"),
windowIsMaximized: (): Promise => invoke("window_is_maximized"),
+ toggleDevTools: (): Promise => invoke("toggle_devtools"),
+ windowResize: (width: number, height: number): Promise =>
+ invoke("window_resize", { width, height }),
onWindowMaximizedChanged: (callback: (maximized: boolean) => void): Promise => {
return listen("window:maximized-changed", (event) => {
callback(event.payload)
@@ -185,15 +188,19 @@ const api = {
},
// Logger
- queryLogs: (params?: { lines?: number }): Promise<{ success: boolean; data: string[] }> =>
- invoke("log_query", { params }),
+ queryLogs: (
+ input?: number | { lines?: number }
+ ): Promise<{ success: boolean; data: string[] }> => {
+ const lines = typeof input === "number" ? input : input?.lines
+ return invoke("log_query", { lines })
+ },
clearLogs: (): Promise<{ success: boolean }> => invoke("log_clear"),
setLogLevel: (level: string): Promise<{ success: boolean }> => invoke("log_set_level", { level }),
writeLog: (payload: {
level: string
message: string
meta?: any
- }): Promise<{ success: boolean }> => invoke("log_write", payload),
+ }): Promise<{ success: boolean }> => invoke("log_write", { payload }),
// Database Connection
dbTestConnection: (
@@ -291,6 +298,28 @@ const api = {
ruleIds: number[]
): Promise<{ success: boolean; data?: boolean; message?: string }> =>
invoke("auto_score_sort_rules", { ruleIds }),
+
+ // Generic invoke wrapper for backward compatibility with callers using `api.invoke`
+ invoke: async (channel: string, ...args: any[]): Promise => {
+ switch (channel) {
+ case "auto-score:getRules":
+ return api.autoScoreGetRules()
+ case "auto-score:addRule":
+ return api.autoScoreAddRule(args[0])
+ case "auto-score:updateRule":
+ return api.autoScoreUpdateRule(args[0])
+ case "auto-score:deleteRule":
+ return api.autoScoreDeleteRule(args[0])
+ case "auto-score:toggleRule":
+ return api.autoScoreToggleRule(args[0])
+ case "auto-score:sortRules":
+ return api.autoScoreSortRules(args[0])
+ case "auto-score:getStatus":
+ return api.autoScoreGetStatus()
+ default:
+ throw new Error(`Unsupported legacy invoke channel: ${channel}`)
+ }
+ },
}
export default api
diff --git a/vite.config.ts b/vite.config.ts
index de7e70b..efac6a8 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -9,6 +9,10 @@ export default defineConfig({
"@": resolve(__dirname, "src"),
},
},
+ optimizeDeps: {
+ // Avoid scanning legacy `old-ss` entries under project root.
+ entries: ["index.html"],
+ },
build: {
outDir: "dist",
emptyOutDir: true,