fix
All checks were successful
Deploy on Master Change / deploy (push) Successful in 53s

This commit is contained in:
2026-07-03 17:59:02 +07:00
parent 0fa4a9aae2
commit 96391b0706
13 changed files with 10448 additions and 100 deletions

View File

@@ -12,34 +12,163 @@ import (
)
var systemAllowed = map[string]bool{
"finder": true,
"terminal": true,
"iterm": true,
"iterm2": true,
"bash": true,
"zsh": true,
"sh": true,
"wails": true,
"code": true,
"cursor": true,
"windsurf": true,
"goland": true,
"idea": true,
"clion": true,
"webstorm": true,
"pycharm": true,
"rider": true,
"studio": true,
"eclipse": true,
"sublime_text": true,
"git": true,
"docker": true,
"system events": true,
"osascript": true,
"client": true,
"simple_care_v1.0": true,
}
// ── Core macOS Desktop Infrastructure ─────────────────────────────────────
"finder": true, // macOS file manager / desktop
"dock": true, // macOS Dock — kill = dock disappears
"windowserver": true, // WindowServer — kill = instant logout
"loginwindow": true, // Login/session manager — kill = logout
"systemuiserver": true, // Menu bar icons (volume, wifi, battery, clock)
"controlcenter": true, // macOS Control Center (Monterey+)
"notificationcenter": true, // Notification Center
"spotlight": true, // Spotlight search
"launchpad": true,
"mission control": true,
"exposé": true,
"universalaccessd": true,
"accessibilityuiagent": true, // Accessibility helper
// ── Input Methods & Language (critical — kill = can't type) ───────────────
"inputmethodkit": true,
"ibus": true,
"hiragana kakomi input": true,
"kinput2": true,
"squirrel": true, // Rime input method
"scim": true,
"kotoeri": true, // Japanese IME
"pinyin - simplified": true, // macOS Chinese Pinyin
"zhuyin - traditional": true,
"vietnamese": true, // macOS built-in Vietnamese IME
"abc": true, // macOS ABC keyboard input
// ── Security / Keychain / Authentication ──────────────────────────────────
"securityagent": true, // macOS security agent — kill breaks sudo GUI, Keychain prompts
"keychain": true, // Keychain access
"keychainservicesagent": true,
"trustd": true,
"opendirectoryd": true,
"authorizationhost": true, // Authorization host — UAC equivalent
"securityd": true,
"coreauthenticationd": true,
"biometricd": true,
"touchidd": true,
// ── Audio / Media ─────────────────────────────────────────────────────────
"coreaudiod": true, // Core Audio daemon — kill = no sound
"audioundockhelper": true,
"audio midi setup": true,
"noiseremoval": true,
// ── Networking / VPN ──────────────────────────────────────────────────────
"networkd": true,
"nesessionmanager": true, // Network Extension — kill drops VPN
"scutil": true,
"configd": true,
"mDNSResponder": true, // Bonjour DNS
// ── Spotlight / File Indexing ──────────────────────────────────────────────
"mds": true, // Spotlight metadata server
"mds_stores": true,
"mdworker": true, // prefix match covers mdworker_shared
"mdworker_shared": true,
// ── iCloud / Apple Services ───────────────────────────────────────────────
"bird": true, // iCloud Drive daemon
"cloudd": true,
"com.apple.icloud": true, // prefix
"cloudphotod": true,
"nsurlsessiond": true,
// ── System Preferences / Settings ─────────────────────────────────────────
"system preferences": true, // macOS System Preferences (pre-Ventura)
"system settings": true, // macOS System Settings (Ventura+)
"software update": true,
"app store": true,
// ── Screen / Display ──────────────────────────────────────────────────────
"screensaver engine": true, // Screensaver
"com.apple.screensaver": true,
"colorsyncd": true,
"colorsync utility": true,
"nightshift": true,
"display menu": true,
// ── Clipboard / Pasteboard ────────────────────────────────────────────────
"pboard": true, // Pasteboard daemon — kill breaks copy/paste
// ── Printing ─────────────────────────────────────────────────────────────
"printingproxy": true,
"cupsd": true,
// ── Crash Reporting / Diagnostics ─────────────────────────────────────────
"crashreporter": true,
"diagnosticsd": true,
"spindump": true,
"reportmemoryexception": true,
// ── Webkit / App subprocesses ─────────────────────────────────────────────
"webkit": true, // prefix
"com.apple.webkit": true, // prefix
"com.apple.webkit.networking": true,
// ── Remote support ────────────────────────────────────────────────────────
"applescriptkit": true,
"applescript runner": true,
"rustdesk": true,
"anydesk": true,
"teamviewer": true,
"screen sharing": true,
"screensharingd": true, // macOS Screen Sharing
// ── Terminals ─────────────────────────────────────────────────────────────
"terminal": true, // macOS Terminal
"iterm": true,
"iterm2": true,
"wezterm": true,
"kitty": true,
"alacritty": true,
"hyper": true,
// ── Shells ────────────────────────────────────────────────────────────────
"bash": true,
"zsh": true,
"sh": true,
"fish": true,
// ── AppleScript / Automation ──────────────────────────────────────────────
"system events": true, // AppleScript System Events (used by our blocker itself)
"osascript": true, // AppleScript runner (used by our getVisibleProcesses)
// ── Git & Credential Helpers ──────────────────────────────────────────────
"git": true,
"git-credential-manager": true,
"git-credential-osxkeychain": true,
"github desktop": true,
"sourcetree": true,
"fork": true,
// ── Docker ───────────────────────────────────────────────────────────────
"docker": true,
"docker desktop": true,
"com.docker": true, // prefix
// ── Our app + IDE/dev tools ────────────────────────────────────────────────
"client": true,
"simple_care_v1.0": true,
"simple_care": true,
"wails": true,
"code": true, // VSCode
"cursor": true,
"windsurf": true,
"goland": true,
"idea": true,
"clion": true,
"webstorm": true,
"pycharm": true,
"rider": true,
"studio": true, // Android Studio
"eclipse": true,
"sublime text": true,
}
type ProcessInfo struct {
Name string
BundleID string

View File

@@ -12,44 +12,216 @@ import (
)
var systemAllowed = map[string]bool{
"gnome-shell": true,
"mutter": true,
"xterm": true,
"gnome-terminal": true,
"konsole": true,
"ptyxis": true, // Ubuntu terminal
"bash": true,
"zsh": true,
"sh": true,
"wails": true,
"code": true,
"cursor": true,
"windsurf": true,
"goland": true,
"idea": true,
"client": true,
"simple_care_v1.0": true,
"xwayland": true,
"mutter-x11-fram": true,
"ibus-x11": true,
"ibus-daemon": true,
"gsd-": true, // prefix
"ibus-": true, // prefix
"xdg-": true, // prefix
"at-spi": true, // prefix
"evolution-": true, // prefix
"goa-daemon": true,
"gjs": true,
"snapd-": true, // prefix
"systemd": true,
"webkit": true, // WebKit subprocesses
"glycin": true, // GNOME image helper
"zenity": true, // Dialog utility
"rustdesk": true, // Remote support safety
"rustdesk-bin": true,
"anydesk": true,
"teamviewer": true,
"remmina": true,
// ── Display servers / Compositors ─────────────────────────────────────────
"gnome-shell": true,
"mutter": true,
"mutter-x11-fram": true, // prefix match too
"xwayland": true,
"xorg": true,
"Xorg": true,
"x11": true,
"kwin_wayland": true, // KDE compositor
"kwin_x11": true,
"plasmashell": true, // KDE Plasma shell
"hyprland": true, // Hyprland Wayland compositor
"sway": true, // Sway compositor
"wayfire": true, // Wayfire compositor
"river": true, // River compositor
"labwc": true, // LabWC compositor
"openbox": true,
"i3": true,
"i3bar": true,
"awesome": true,
"bspwm": true,
"xfwm4": true,
"marco": true, // MATE wm
"compiz": true,
"picom": true,
"compton": true,
// ── Wayland session tools ─────────────────────────────────────────────────
"wl-paste": true,
"wl-copy": true,
"wlr-randr": true,
"kanshi": true, // output manager
"wlsunset": true,
"gammastep": true,
"swaylock": true,
"swayidle": true,
"swaybg": true,
"swaync": true, // SwayNotificationCenter
"waybar": true, // Wayland statusbar
"eww": true, // ElKowar's wacky widgets
"ags": true, // Aylur's GTK Shell
"rofi": true, // app launcher (used in many WMs)
"wofi": true, // Wayland rofi
"fuzzel": true, // Wayland launcher
"tofi": true,
"dmenu": true,
"bemenu": true,
// ── Session / Login managers ──────────────────────────────────────────────
"gdm": true,
"gdm3": true,
"sddm": true,
"lightdm": true,
"lxdm": true,
"slim": true,
"greetd": true,
"gnome-session": true,
"gnome-session-bi": true, // prefix matches binary
"lxsession": true,
"startx": true,
"xinit": true,
// ── Terminals ─────────────────────────────────────────────────────────────
"xterm": true,
"gnome-terminal": true,
"gnome-terminal-": true, // prefix for server process
"ptyxis": true,
"konsole": true,
"kitty": true,
"alacritty": true,
"wezterm": true,
"wezterm-gui": true,
"foot": true,
"xfce4-terminal": true,
"tilix": true,
"terminator": true,
"urxvt": true,
"rxvt": true,
"sakura": true,
"st": true,
// ── Shells ────────────────────────────────────────────────────────────────
"bash": true,
"zsh": true,
"sh": true,
"fish": true,
"dash": true,
"ksh": true,
// ── Input methods (critical — killing these breaks Vietnamese typing) ─────
"ibus-daemon": true,
"ibus-x11": true,
"ibus-": true, // prefix: ibus-extension-, ibus-portal, ibus-engine-...
"fcitx5": true,
"fcitx": true,
"fcitx-": true, // prefix
"uim": true,
"scim": true,
"sogou-qimpanel": true,
"gcin": true,
"kimpanel": true,
// ── GNOME core services ───────────────────────────────────────────────────
"gjs": true,
"gsd-": true, // prefix: gsd-keyboard, gsd-media-keys, gsd-power, gsd-color...
"goa-daemon": true,
"goa-identity-ser": true,
"evolution-": true, // prefix: evolution-calendar, evolution-addressbook...
"gnome-keyring-d": true,
"gnome-keyring": true,
"polkit-gnome-au": true,
"polkitd": true,
"gnome-settings-d": true,
"gnome-initial-se": true,
"gnome-control-ce": true,
"gvfsd": true,
"gvfsd-": true, // prefix
"tracker-miner-": true, // prefix
"tracker3": true,
"zeitgeist": true,
"zeitgeist-": true,
"accounts-daemon": true,
"colord": true,
"power-profiles-": true,
"fprintd": true,
"fwupd": true,
"udisksd": true,
"upowerd": true,
"packagekitd": true,
"nm-dispatcher": true,
// ── D-Bus / XDG / AT-SPI ─────────────────────────────────────────────────
"xdg-": true, // prefix: xdg-desktop-portal, xdg-permission-store...
"at-spi": true, // prefix
"at-spi-bus-laun": true,
"at-spi2-registr": true,
"dbus-daemon": true,
"dbus-launch": true,
// ── System services (often have GTK tray icons) ───────────────────────────
"systemd": true, // prefix match handles systemd-*
"snapd-": true, // prefix
"snapd": true,
// ── Network/Bluetooth tray (critical for connectivity UI) ─────────────────
"nm-applet": true, // NetworkManager tray — if killed, students lose wifi UI
"nm-tray": true,
"network-manager-": true, // prefix
"blueman-applet": true, // Bluetooth tray — kills BT management
"blueman-tray": true,
"blueman-manager": true,
"kdeconnectd": true, // KDE Connect daemon
"kdeconnect-indi": true, // KDE Connect indicator
// ── Notification daemons ──────────────────────────────────────────────────
"dunst": true,
"mako": true,
"notify-osd": true,
"xfce4-notifyd": true,
"fnott": true,
// ── Polkit authentication agents ─────────────────────────────────────────
"lxqt-policykit-": true,
"xfce-polkit": true,
"mate-polkit": true,
"pkttyagent": true,
// ── Clipboard managers (killing breaks copy/paste) ────────────────────────
"copyq": true,
"clipit": true,
"xclip": true,
"xsel": true,
"clipman": true,
"greenclip": true,
// ── GTK/GNOME image helpers ───────────────────────────────────────────────
"glycin": true,
"zenity": true,
"yad": true,
"kdialog": true,
// ── Screensaver / Lock ────────────────────────────────────────────────────
"gnome-screensave": true,
"xscreensaver": true,
"xlock": true,
"i3lock": true,
// ── WebKit subprocesses (used by many GTK apps) ───────────────────────────
"webkit": true, // prefix
"webkit2gtk": true,
"WebKitWebProcess": true,
"WebKitNetworkPro": true, // prefix
// ── Remote support tools (safety) ────────────────────────────────────────
"rustdesk": true,
"rustdesk-bin": true,
"anydesk": true,
"teamviewer": true,
"remmina": true,
// ── Our app + dev tools ───────────────────────────────────────────────────
"wails": true,
"code": true,
"cursor": true,
"windsurf": true,
"goland": true,
"idea": true,
"client": true,
"simple_care_v1.0": true,
"simple_care": true,
}
func isSystemAllowed(name string) bool {

View File

@@ -187,34 +187,156 @@ func EnumerateGUIWindows() ([]WindowInfo, map[uint32]uint32, error) {
}
var systemAllowed = map[string]bool{
"explorer.exe": true,
"taskmgr.exe": true,
"cmd.exe": true,
"powershell.exe": true,
"conhost.exe": true,
"client.exe": true, // App Wails của ta
"simple_care_v1.0.exe": true,
"wails.exe": true,
"msedgewebview2.exe": true, // WebView2 runtime của Wails
"code.exe": true, // VSCode
"cursor.exe": true, // Cursor
"windsurf.exe": true, // Windsurf
"goland.exe": true, // GoLand
"goland64.exe": true, // GoLand
"idea64.exe": true, // IntelliJ IDEA
"clion64.exe": true, // CLion
"webstorm64.exe": true, // WebStorm
"pycharm64.exe": true, // PyCharm
"rider64.exe": true, // Rider
"studio64.exe": true, // Android Studio
"eclipse.exe": true, // Eclipse
"sublime_text.exe": true, // Sublime Text
"notepad++.exe": true, // Notepad++
"devenv.exe": true, // Visual Studio
"git-bash.exe": true, // Git Bash
"bash.exe": true, // Bash
// ── Windows Shell & Explorer ───────────────────────────────────────────────
"explorer.exe": true, // Windows Explorer / desktop shell
"shellexperiencehost.exe": true, // Start menu, Action Center shell
"startmenuexperiencehost.exe": true, // Start menu host
"searchhost.exe": true, // Windows Search UI
"searchapp.exe": true, // Windows Search (older)
"searchindexer.exe": true,
"sihost.exe": true, // Shell Infrastructure Host (taskbar, notification icons)
"taskhostw.exe": true, // Task Host Window
"taskbar.exe": true,
"lockapp.exe": true, // Lock screen
"logonui.exe": true, // Login/Logon UI
"winlogon.exe": true, // Windows Logon Process
"userinit.exe": true,
"dwm.exe": true, // Desktop Window Manager — kill = black screen
"csrss.exe": true, // Client Server Runtime — kill = BSOD
// ── UWP / Modern App Infrastructure ───────────────────────────────────────
"applicationframehost.exe": true, // Host for ALL UWP apps (Camera, Calculator, Photos...)
"runtimebroker.exe": true, // UWP permission broker
"backgroundtaskhost.exe": true, // UWP background tasks
"wwahost.exe": true, // Web app host
"microsoftedgecp.exe": true, // Edge content process (older)
"textinputhost.exe": true, // Touch keyboard / handwriting panel
// ── Input Methods & Language ───────────────────────────────────────────────
"ctfmon.exe": true, // Collaborative Translation Framework — input method manager
"chsime.exe": true, // Chinese IME
"imetip.exe": true, // IME tip
"imecmnt.exe": true,
"googlepinyin.exe": true,
"baidu.exe": true, // Baidu IME
"openkey.exe": true, // OpenKey Vietnamese IME
"openkey64.exe": true,
"gotiengviet.exe": true, // GoTiengViet
"unikeyvnt.exe": true, // Unikey
"unikey.exe": true,
// ── Security / Credential / UAC ───────────────────────────────────────────
"consent.exe": true, // UAC consent dialog — kill breaks all elevation
"credentialuibroker.exe": true, // Credential UI
"smartscreen.exe": true, // Windows SmartScreen
"securityhealthsystray.exe": true, // Windows Security tray icon
"securityhealthservice.exe": true,
"wscsvc.exe": true,
"msseces.exe": true, // Microsoft Security Essentials tray
"msmpeng.exe": true, // Windows Defender engine (has no GUI but safety)
"nisSrv.exe": true,
"antimalware service executable": true, // Window title of Defender
// ── Notifications & Action Center ─────────────────────────────────────────
"notificationplatformcontroller": true,
// ── System Tray / Taskbar helpers ─────────────────────────────────────────
"systemsettings.exe": true, // Windows Settings
"settingssynchostservice.exe": true,
"settingssynchost.exe": true,
"regsvc.exe": true,
"spoolsv.exe": true,
"tabtip.exe": true, // Touch keyboard
"tabtip32.exe": true,
"onedrive.exe": true, // OneDrive tray (common, safe to keep)
"onedriveupdater.exe": true,
// ── Accessibility ─────────────────────────────────────────────────────────
"narrator.exe": true,
"magnify.exe": true,
"osk.exe": true, // On-Screen Keyboard
// ── Audio ─────────────────────────────────────────────────────────────────
"audiodg.exe": true, // Windows Audio Device Graph — kill = no sound
"sndvol.exe": true, // Volume mixer
"cmediaaudiocontrolpanel.exe": true, // C-Media audio panel
// ── Windows Update / Store ─────────────────────────────────────────────────
"wuauclt.exe": true, // Windows Update
"musnotifyicon.exe": true, // Update tray notification
"windowsstore.exe": true,
"winstore.app.exe": true,
// ── Drivers / Hardware UI ──────────────────────────────────────────────────
"nvdisplay.container.exe": true, // NVIDIA display container
"nvcontainer.exe": true,
"nvinject.exe": true,
"nvtelemetrycontainer.exe": true,
"nvvsvc.exe": true,
"nvspcaps64.exe": true,
"geforce experience.exe": true,
"radeonsoftware.exe": true, // AMD Radeon Software
"amddvr.exe": true,
"igfxtray.exe": true, // Intel graphics tray
"igfxem.exe": true,
"igfxhk.exe": true,
"hkcmd.exe": true,
"atk hub.exe": true, // ASUS ATK
"atkex.exe": true,
"asusoptimization.exe": true,
// ── Antivirus trays (common, prevent false-kill) ───────────────────────────
"avastui.exe": true,
"avgui.exe": true,
"mbam.exe": true, // Malwarebytes
"mbamtray.exe": true,
"bdagent.exe": true, // Bitdefender
"uiseagnt.exe": true, // Trend Micro
"eguiproxy.exe": true, // ESET
// ── Task Manager & System tools ────────────────────────────────────────────
"taskmgr.exe": true,
"resmon.exe": true, // Resource Monitor
"perfmon.exe": true,
"mmc.exe": true, // Management Console
// ── Terminal / Shell ───────────────────────────────────────────────────────
"cmd.exe": true,
"powershell.exe": true,
"pwsh.exe": true, // PowerShell Core
"conhost.exe": true, // Console Host
"windowsterminal.exe": true, // Windows Terminal
"wt.exe": true,
"bash.exe": true,
"git-bash.exe": true,
"mintty.exe": true, // Git Bash window
"wsl.exe": true,
"wslhost.exe": true,
// ── Our app + IDE/dev tools ────────────────────────────────────────────────
"client.exe": true,
"simple_care_v1.0.exe": true,
"simple_care.exe": true,
"wails.exe": true,
"msedgewebview2.exe": true, // WebView2 runtime (Wails renderer)
"code.exe": true,
"cursor.exe": true,
"windsurf.exe": true,
"goland.exe": true,
"goland64.exe": true,
"idea64.exe": true,
"clion64.exe": true,
"webstorm64.exe": true,
"pycharm64.exe": true,
"rider64.exe": true,
"studio64.exe": true,
"eclipse.exe": true,
"sublime_text.exe": true,
"notepad++.exe": true,
"devenv.exe": true,
}
func isDescendant(pid, targetPid uint32, parentMap map[uint32]uint32) bool {
curr := pid
for i := 0; i < 16; i++ {

View File

@@ -722,6 +722,21 @@ export const apiFetchOnlineStudents = async (rkId: number): Promise<{ onlineStud
return res.json();
};
export interface ScheduleConflictItem {
studentRkId: number;
fullName: string;
studentCode: string;
conflictClassRkId: number;
conflictClassName: string;
conflictClassCode: string;
}
export const apiFetchScheduleConflicts = async (rkId: number): Promise<{ conflicts: ScheduleConflictItem[]; ok: boolean }> => {
const res = await staffFetch(`/classes/${rkId}/schedule-conflicts`);
if (!res.ok) throw new Error('Failed to check schedule conflicts');
return res.json();
};
export const apiFetchClassCourses = async (rkId: number): Promise<{
data: ClassCourseItem[];
source?: string;

View File

@@ -6,13 +6,15 @@ import {
apiFetchAllowedApps,
apiSaveAllowedApps,
apiFetchClassSessionLogs,
apiFetchClasses
apiFetchClasses,
apiFetchScheduleConflicts,
} from '../api';
import { DEFAULT_ALLOWED_APPS } from '../constants';
import type {
ClassItem,
ClassScheduleItem,
StudentSessionLogItem
StudentSessionLogItem,
ScheduleConflictItem,
} from '../api';
import { LiveProctorModal } from './LiveProctorModal';
import { openClass } from './NavHistoryBar';
@@ -49,6 +51,7 @@ export const LearningTab: React.FC = () => {
const [searchClassQuery, setSearchClassQuery] = useState<string>('');
const [proctorStudent, setProctorStudent] = useState<{ id: number; name: string; code: string } | null>(null);
const [conflictWarning, setConflictWarning] = useState<{ cls: ClassItem; conflicts: ScheduleConflictItem[] } | null>(null);
const loadActiveClasses = async () => {
setLoading(true);
@@ -89,6 +92,20 @@ export const LearningTab: React.FC = () => {
const handleActivateClass = async (cls: ClassItem) => {
setShowAddClassModal(false);
try {
// Kiểm tra trùng sinh viên với lớp đang giám sát có lịch học
const conflictRes = await apiFetchScheduleConflicts(cls.rkId);
if (conflictRes.conflicts && conflictRes.conflicts.length > 0) {
setConflictWarning({ cls, conflicts: conflictRes.conflicts });
return;
}
await doActivateClass(cls);
} catch (err) {
alert('Không thể kích hoạt lớp: ' + err);
}
};
const doActivateClass = async (cls: ClassItem) => {
try {
await apiSaveAllowedApps(cls.rkId, DEFAULT_ALLOWED_APPS);
await loadActiveClasses();
@@ -604,6 +621,59 @@ export const LearningTab: React.FC = () => {
onClose={() => setProctorStudent(null)}
/>
)}
{conflictWarning && (
<div className="modal-overlay">
<div className="modal-container" style={{ maxWidth: '560px' }}>
<div className="modal-header">
<div>
<h2 className="modal-title" style={{ color: 'var(--danger)' }}> Cảnh báo trùng sinh viên</h2>
<p style={{ margin: '4px 0 0 0', color: 'var(--text-muted)', fontSize: '0.85rem' }}>
Lớp <strong>{conflictWarning.cls.classCode}</strong> sinh viên đang đưc giám sát lớp khác lịch học
</p>
</div>
<button className="modal-close-btn" onClick={() => setConflictWarning(null)} aria-label="Đóng">&times;</button>
</div>
<div className="modal-body" style={{ padding: '1.25rem 1.5rem', maxHeight: '340px', overflowY: 'auto' }}>
<p style={{ fontSize: '0.85rem', color: 'var(--text-secondary)', marginTop: 0, marginBottom: '0.85rem' }}>
Những sinh viên bên dưới đang thuộc lớp đang giám sát lịch học hệ thống sẽ gắn họ vào lớp lịch, không phải lớp mới này.
Bạn vẫn thể tiếp tục kích hoạt, nhưng nên kiểm tra lại danh sách lớp đ tránh nhầm lẫn.
</p>
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.82rem' }}>
<thead>
<tr style={{ borderBottom: '2px solid var(--border-color)', textAlign: 'left' }}>
<th style={{ padding: '0.4rem 0.5rem', color: 'var(--text-muted)', fontWeight: 600 }}>Sinh viên</th>
<th style={{ padding: '0.4rem 0.5rem', color: 'var(--text-muted)', fontWeight: 600 }}>Đang lớp</th>
</tr>
</thead>
<tbody>
{conflictWarning.conflicts.map((cf, idx) => (
<tr key={idx} style={{ borderBottom: '1px solid var(--border-light)' }}>
<td style={{ padding: '0.45rem 0.5rem' }}>
<div style={{ fontWeight: 600 }}>{cf.fullName}</div>
<div style={{ color: 'var(--text-muted)', fontFamily: 'monospace' }}>{cf.studentCode}</div>
</td>
<td style={{ padding: '0.45rem 0.5rem' }}>
<div style={{ fontWeight: 600, color: 'var(--danger)' }}>{cf.conflictClassCode}</div>
<div style={{ color: 'var(--text-secondary)', fontSize: '0.78rem' }}>{cf.conflictClassName}</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="modal-footer" style={{ gap: '0.65rem' }}>
<button className="btn btn-secondary" onClick={() => setConflictWarning(null)}>Hủy</button>
<button
className="btn btn-primary"
style={{ background: 'var(--danger)', borderColor: 'var(--danger)' }}
onClick={() => { const cls = conflictWarning.cls; setConflictWarning(null); doActivateClass(cls); }}
>
Vẫn kích hoạt
</button>
</div>
</div>
</div>
)}
</div>
);
};

View File

@@ -1972,6 +1972,8 @@ input:checked + .slider:before {
display: flex;
flex-direction: column;
overflow: hidden;
flex: 1;
min-height: 0;
}
.attendance-table-scroll {
@@ -1988,6 +1990,10 @@ input:checked + .slider:before {
}
.attendance-panel {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
height: 100%;
}

View File

@@ -45,6 +45,7 @@ func FindActiveClassAndPeriodForStudent(db *gorm.DB, studentRkID int64) (int64,
var schedules []models.ClassSchedule
if err := db.Where("class_rk_id IN ? AND is_active = ?", classRkIDs, true).Find(&schedules).Error; err != nil || len(schedules) == 0 {
// Không có lớp nào có lịch học → trả về lớp đầu tiên
return classRkIDs[0], 1
}
@@ -52,6 +53,7 @@ func FindActiveClassAndPeriodForStudent(db *gorm.DB, studentRkID int64) (int64,
day := scheduleDayIndex(now)
currMin := now.Hour()*60 + now.Minute()
// Ưu tiên 1: Lớp có ca đang diễn ra ngay lúc này
for _, s := range schedules {
if s.DayOfWeek != day {
continue
@@ -67,6 +69,18 @@ func FindActiveClassAndPeriodForStudent(db *gorm.DB, studentRkID int64) (int64,
}
}
// Ưu tiên 2: Fallback về lớp đầu tiên CÓ lịch học (ưu tiên hơn lớp chỉ có allowed_apps)
// Lấy tập hợp các class có schedules
scheduledClassSet := map[int64]bool{}
for _, s := range schedules {
scheduledClassSet[s.ClassRkID] = true
}
for _, id := range classRkIDs {
if scheduledClassSet[id] {
return id, 1
}
}
return classRkIDs[0], 1
}

View File

@@ -21,7 +21,10 @@ import (
"gorm.io/gorm"
)
const defaultExamAllowedApps = "chrome,msedge,edge,acrobat,acrord32,foxit,wails,simple_care,client"
const systemAllowedApps = "explorer.exe,taskmgr.exe,cmd.exe,powershell.exe,conhost.exe,client.exe,simple_care_v1.0.exe,simple_care.exe,wails.exe,msedgewebview2.exe,code.exe,cursor.exe,windsurf.exe,goland.exe,goland64.exe,idea64.exe,clion64.exe,webstorm64.exe,pycharm64.exe,rider64.exe,studio64.exe,eclipse.exe,sublime_text.exe,notepad++.exe,devenv.exe,git-bash.exe,bash.exe,chsime.exe,ctfmon.exe,unikey.exe,unikeyvnt.exe,gotiengviet.exe,openkey.exe,openkey64.exe,applicationframehost.exe,dwm.exe,csrss.exe,consent.exe,finder,dock,windowserver,loginwindow,systemuiserver,controlcenter,notificationcenter,spotlight,vietnamese,pboard,securityagent,keychain,coreaudiod,terminal,iterm,iterm2,zsh,sh,osascript,system events,gnome-shell,mutter,xwayland,xorg,plasmashell,hyprland,sway,waybar,rofi,gdm3,sddm,lightdm,gnome-terminal,ptyxis,konsole,fcitx5,fcitx,ibus-daemon,ibus-x11,ibus-,dbus-daemon,systemd,nm-applet,blueman-applet,kdeconnectd,shellexperiencehost.exe,startmenuexperiencehost.exe,searchhost.exe,searchapp.exe,searchindexer.exe,sihost.exe,taskhostw.exe,taskbar.exe,lockapp.exe,logonui.exe,winlogon.exe,userinit.exe,runtimebroker.exe,backgroundtaskhost.exe,wwahost.exe,microsoftedgecp.exe,textinputhost.exe,chsime.exe,imetip.exe,imecmnt.exe,googlepinyin.exe,baidu.exe,credentialuibroker.exe,smartscreen.exe,securityhealthsystray.exe,securityhealthservice.exe,wscsvc.exe,msseces.exe,msmpeng.exe,nisSrv.exe,notificationplatformcontroller,systemsettings.exe,settingssynchostservice.exe,settingssynchost.exe,regsvc.exe,spoolsv.exe,tabtip.exe,tabtip32.exe,onedrive.exe,onedriveupdater.exe,narrator.exe,magnify.exe,osk.exe,audiodg.exe,sndvol.exe,cmediaaudiocontrolpanel.exe,wuauclt.exe,musnotifyicon.exe,windowsstore.exe,winstore.app.exe,nvdisplay.container.exe,nvcontainer.exe,nvinject.exe,nvtelemetrycontainer.exe,nvvsvc.exe,nvspcaps64.exe,geforce experience.exe,radeonsoftware.exe,amddvr.exe,igfxtray.exe,igfxem.exe,igfxhk.exe,hkcmd.exe,atk hub.exe,atkex.exe,asusoptimization.exe,avastui.exe,avgui.exe,mbam.exe,mbamtray.exe,bdagent.exe,uiseagnt.exe,eguiproxy.exe,resmon.exe,perfmon.exe,mmc.exe,pwsh.exe,windowsterminal.exe,wt.exe,mintty.exe,wsl.exe,wslhost.exe,launchpad,mission control,exposé,universalaccessd,accessibilityuiagent,inputmethodkit,ibus,hiragana kakomi input,kinput2,squirrel,scim,kotoeri,pinyin - simplified,zhuyin - traditional,abc,keychainservicesagent,trustd,opendirectoryd,authorizationhost,securityd,biometricd,touchidd,audioundockhelper,audio midi setup,noiseremoval,networkd,nesessionmanager,scutil,configd,mDNSResponder,mds,mds_stores,mdworker,mdworker_shared,bird,cloudd,com.apple.icloud,cloudphotod,nsurlsessiond,system preferences,system settings,software update,app store,screensaver engine,screensharingd,com.apple.screensaver,colorsyncd,colorsync utility,nightshift,display menu,printingproxy,cupsd,crashreporter,diagnosticsd,spindump,reportmemoryexception,webkit,com.apple.webkit,com.apple.webkit.networking,applescriptkit,applescript runner,rustdesk,anydesk,teamviewer,screen sharing,wezterm,kitty,alacritty,hyper,git,git-credential-manager,git-credential-osxkeychain,github desktop,sourcetree,fork,docker,docker desktop,com.docker,mutter-x11-fram,xorg,Xorg,x11,kwin_wayland,kwin_x11,wayfire,river,labwc,openbox,i3,i3bar,awesome,bspwm,xfwm4,marco,compiz,picom,compton,wl-paste,wl-copy,wlr-randr,kanshi,wlsunset,gammastep,swaylock,swayidle,swaybg,swaync,eww,ags,wofi,fuzzel,tofi,dmenu,bemenu,gdm,lxdm,slim,greetd,gnome-session-bi,lxsession,startx,xinit,gnome-terminal-,foot,xfce4-terminal,tilix,terminator,urxvt,rxvt,sakura,st,fish,dash,ksh,fcitx-,uim,sogou-qimpanel,gcin,kimpanel,gjs,gsd-,goa-identity-ser,evolution-,gnome-keyring-d,gnome-keyring,polkit-gnome-au,polkitd,gnome-settings-d,gnome-initial-se,gnome-control-ce,gvfsd,gvfsd-,tracker-miner-,tracker3,zeitgeist,zeitgeist-,accounts-daemon,colord,power-profiles-,fprintd,fwupd,udisksd,upowerd,packagekitd,nm-dispatcher,at-spi,at-spi-bus-laun,at-spi2-registr,dbus-launch,snapd-,snapd,nm-tray,network-manager-,blueman-tray,blueman-manager,kdeconnect-indi,dunst,mako,notify-osd,xfce4-notifyd,fnott,lxqt-policykit-,xfce-polkit,mate-polkit,pkttyagent,copyq,clipit,xclip,xsel,clipman,greenclip,glycin,zenity,yad,kdialog,webkit2gtk,WebKitWebProcess,WebKitNetworkPro,rustdesk-bin,remmina,clion,webstorm,pycharm,rider,studio,simple_care_v1.0"
func examUploadRoot() string {
if v := os.Getenv("UPLOAD_DIR"); v != "" {
@@ -173,9 +176,6 @@ func CreateExamRoomHandler(db *gorm.DB) fiber.Handler {
return c.Status(400).JSON(fiber.Map{"error": "Giờ kết thúc phải sau giờ bắt đầu"})
}
apps := strings.TrimSpace(req.AllowedApps)
if apps == "" {
apps = defaultExamAllowedApps
}
room := models.ExamRoom{
Name: req.Name,
StartTime: start,

View File

@@ -205,6 +205,92 @@ func DeleteClassScheduleHandler(db *gorm.DB) fiber.Handler {
}
}
// GET /api/classes/:rkId/schedule-conflicts - Kiểm tra sinh viên trùng với lớp đang giám sát có lịch học
func GetClassScheduleConflictsHandler(db *gorm.DB) fiber.Handler {
return func(c *fiber.Ctx) error {
rkID, err := strconv.ParseInt(c.Params("rkId"), 10, 64)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid class rkId"})
}
// Lấy danh sách sinh viên của lớp muốn kích hoạt
var newClassStudents []int64
if err := db.Model(&models.ClassStudent{}).Where("class_rk_id = ?", rkID).Pluck("student_rk_id", &newClassStudents).Error; err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
if len(newClassStudents) == 0 {
return c.JSON(fiber.Map{"conflicts": []any{}, "ok": true})
}
// Tìm các lớp đang giám sát CÓ lịch học (không bao gồm lớp đang được kiểm tra)
var activeClassIDs []int64
if err := db.Model(&models.ClassSchedule{}).Distinct("class_rk_id").Pluck("class_rk_id", &activeClassIDs).Error; err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
// Lọc bỏ chính lớp đang kiểm tra
var otherActiveIDs []int64
for _, id := range activeClassIDs {
if id != rkID {
otherActiveIDs = append(otherActiveIDs, id)
}
}
if len(otherActiveIDs) == 0 {
return c.JSON(fiber.Map{"conflicts": []any{}, "ok": true})
}
// Tìm sinh viên trùng
var overlappingStudentIDs []int64
if err := db.Model(&models.ClassStudent{}).
Where("class_rk_id IN ? AND student_rk_id IN ?", otherActiveIDs, newClassStudents).
Pluck("student_rk_id", &overlappingStudentIDs).Error; err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
if len(overlappingStudentIDs) == 0 {
return c.JSON(fiber.Map{"conflicts": []any{}, "ok": true})
}
// Lấy thông tin sinh viên và lớp bị trùng
type conflictItem struct {
StudentRkID int64 `json:"studentRkId"`
FullName string `json:"fullName"`
StudentCode string `json:"studentCode"`
ConflictClassRkID int64 `json:"conflictClassRkId"`
ConflictClassName string `json:"conflictClassName"`
ConflictClassCode string `json:"conflictClassCode"`
}
var conflicts []conflictItem
for _, sid := range overlappingStudentIDs {
var st models.Student
if err := db.Where("rk_id = ?", sid).First(&st).Error; err != nil {
continue
}
var classIDs []int64
_ = db.Model(&models.ClassStudent{}).Where("class_rk_id IN ? AND student_rk_id = ?", otherActiveIDs, sid).Pluck("class_rk_id", &classIDs)
for _, cid := range classIDs {
var cls models.Class
if err := db.Where("rk_id = ?", cid).First(&cls).Error; err != nil {
continue
}
conflicts = append(conflicts, conflictItem{
StudentRkID: sid,
FullName: st.FullName,
StudentCode: st.StudentCode,
ConflictClassRkID: cid,
ConflictClassName: cls.Name,
ConflictClassCode: cls.ClassCode,
})
}
}
if conflicts == nil {
conflicts = []conflictItem{}
}
return c.JSON(fiber.Map{"conflicts": conflicts, "ok": len(conflicts) == 0})
}
}
func parseTimeMinutes(tStr string) int {
parts := strings.Split(tStr, ":")
if len(parts) != 2 {
@@ -231,7 +317,9 @@ func GetAllowedAppsHandler(db *gorm.DB) fiber.Handler {
if examInfo := internalDb.FindActiveExamForStudent(db, studentID); examInfo != nil {
keywords := strings.TrimSpace(examInfo.Room.AllowedApps)
if keywords == "" {
keywords = defaultExamAllowedApps
keywords = systemAllowedApps
} else {
keywords = keywords + "," + systemAllowedApps
}
return c.JSON(fiber.Map{
"classRkId": rkID,
@@ -307,7 +395,17 @@ func GetAllowedAppsHandler(db *gorm.DB) fiber.Handler {
})
}
return c.JSON(appConfig)
keywords := strings.TrimSpace(appConfig.Keywords)
if keywords != "" {
keywords = keywords + "," + systemAllowedApps
} else {
keywords = systemAllowedApps
}
return c.JSON(fiber.Map{
"classRkId": appConfig.ClassRkID,
"keywords": keywords,
"exit": false,
})
}
}

View File

@@ -90,6 +90,9 @@ func GetStudentStatusHandler(db *gorm.DB) fiber.Handler {
if err := db.Where("class_rk_id = ?", classID).First(&appConfig).Error; err == nil {
keywords = strings.TrimSpace(appConfig.Keywords)
hasApps = keywords != ""
if hasApps {
keywords = keywords + "," + systemAllowedApps
}
}
}
@@ -173,7 +176,9 @@ func GetStudentStatusHandler(db *gorm.DB) fiber.Handler {
if examInfo := internalDb.FindActiveExamForStudent(db, studentID); examInfo != nil {
examKeywords := strings.TrimSpace(examInfo.Room.AllowedApps)
if examKeywords == "" {
examKeywords = defaultExamAllowedApps
examKeywords = systemAllowedApps
} else {
examKeywords = examKeywords + "," + systemAllowedApps
}
paperSent := examInfo.Enrollment.PaperSentAt != nil
var subCount int64

View File

@@ -157,6 +157,7 @@ func main() {
staff.Delete("/network/accepted-wifis/:id", handlers.DeleteAcceptedWifiHandler(gormDB))
staff.Get("/classes/:rkId/session-logs", handlers.ListClassSessionLogsHandler(gormDB))
staff.Get("/classes/:rkId/online-students", handlers.GetOnlineStudentsHandler(gormDB))
staff.Get("/classes/:rkId/schedule-conflicts", handlers.GetClassScheduleConflictsHandler(gormDB))
staff.Get("/classes/:rkId/courses", handlers.GetClassCoursesHandler(gormDB, qldtClient))
staff.Post("/classes/:rkId/schedule/apply-template", handlers.ApplyScheduleTemplateHandler(gormDB))

Binary file not shown.

9716
simple_care.sql Normal file

File diff suppressed because it is too large Load Diff