diff --git a/client/app.go b/client/app.go index 8f6b3c2..8666904 100644 --- a/client/app.go +++ b/client/app.go @@ -296,7 +296,7 @@ func (a *App) HandleBeforeClose() (prevent bool) { a.mu.Lock() mode := a.dashboard.MonitorMode a.mu.Unlock() - if mode == "" || mode == "not_configured" { + if !shouldRecordViolation(mode) { a.clearRunLock() return false } @@ -359,6 +359,16 @@ type pendingViolation struct { StudentRkID int64 `json:"studentRkId"` } +// shouldRecordViolation — chỉ lưu vi phạm khi đang thi hoặc đang học (trong giờ). +func shouldRecordViolation(monitorMode string) bool { + switch strings.TrimSpace(monitorMode) { + case "exam", "learning": + return true + default: + return false + } +} + func (a *App) markRunLock() { a.mu.Lock() studentID := int64(0) @@ -367,7 +377,7 @@ func (a *App) markRunLock() { studentID = a.student.StudentID } a.mu.Unlock() - if studentID <= 0 || mode == "" || mode == "not_configured" { + if studentID <= 0 || !shouldRecordViolation(mode) { return } payload, _ := json.Marshal(map[string]any{ @@ -402,7 +412,7 @@ func (a *App) detectUncleanShutdown() { } a.mu.Unlock() } - if meta.StudentRkID <= 0 { + if meta.StudentRkID <= 0 || !shouldRecordViolation(meta.MonitorMode) { return } reason := "Ứng dụng bị tắt đột ngột (Task Manager / kill process) khi đang giám sát" @@ -457,8 +467,8 @@ func (a *App) flushPendingViolations() { } func (a *App) postViolation(v pendingViolation) bool { - if v.StudentRkID <= 0 { - return true + if v.StudentRkID <= 0 || !shouldRecordViolation(v.MonitorMode) { + return true // bỏ qua — không thi / không học thì không lưu } payload := map[string]any{ "studentRkId": v.StudentRkID, @@ -492,7 +502,7 @@ func (a *App) reportViolation(kind, reason string) { studentID = a.student.StudentID } a.mu.Unlock() - if studentID <= 0 { + if studentID <= 0 || !shouldRecordViolation(mode) { return } v := pendingViolation{ @@ -590,7 +600,7 @@ func (a *App) startLocalServer() { w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS") return } - // Chỉ purge disk — JS trên trang exam-view tự clear storage + reload iframe. + // Chỉ xóa HTTP disk cache — không đụng cookie / localStorage (giữ phiên đăng nhập trang thi). go a.purgeWebViewDiskCache() w.Write([]byte("ok")) }) @@ -609,18 +619,18 @@ const examViewHTML = ` %s @@ -629,7 +639,7 @@ iframe{width:100%%;height:100%%;border:0;display:block}
%s -Nếu trang lỗi / trắng: F5 hoặc Xóa cache +Trang trắng/lỗi: F5 hoặc Xóa cache (giữ đăng nhập)
@@ -640,58 +650,35 @@ iframe{width:100%%;height:100%%;border:0;display:block} function goBack(){ fetch('http://127.0.0.1:34115/exam-close').catch(function(){}); } -function refreshExam(){ +function reloadFrameSoft(){ var f = document.getElementById('exam-frame'); if (!f) return; - var btn = document.getElementById('btn-refresh'); - if (btn) btn.disabled = true; try { f.contentWindow.location.reload(); } catch (e) { - var src = f.src; + var src = f.getAttribute('src') || f.src; f.src = 'about:blank'; setTimeout(function(){ f.src = src; }, 50); } +} +function refreshExam(){ + var btn = document.getElementById('btn-refresh'); + if (btn) btn.disabled = true; + reloadFrameSoft(); setTimeout(function(){ if (btn) btn.disabled = false; }, 800); } async function clearExamCache(){ var btn = document.getElementById('btn-cache'); if (btn) btn.disabled = true; - try { - if (window.caches) { - var keys = await caches.keys(); - await Promise.all(keys.map(function(k){ return caches.delete(k); })); - } - } catch (e) {} - try { localStorage.clear(); } catch (e) {} - try { sessionStorage.clear(); } catch (e) {} - try { - var f = document.getElementById('exam-frame'); - if (f && f.contentWindow) { - try { f.contentWindow.localStorage.clear(); } catch (e) {} - try { f.contentWindow.sessionStorage.clear(); } catch (e) {} - try { - if (f.contentWindow.caches) { - var ikeys = await f.contentWindow.caches.keys(); - await Promise.all(ikeys.map(function(k){ return f.contentWindow.caches.delete(k); })); - } - } catch (e) {} - } - } catch (e) {} + // Không xóa localStorage/sessionStorage/cookie — tránh văng đăng nhập trang thi. try { await fetch('http://127.0.0.1:34115/exam-clear-cache'); } catch (e) {} - var f = document.getElementById('exam-frame'); - if (f) { - try { - var u = new URL(f.src, window.location.href); - u.searchParams.set('_sc_cb', String(Date.now())); - f.src = u.toString(); - } catch (e) { - refreshExam(); - } - } - setTimeout(function(){ if (btn) btn.disabled = false; }, 1200); + // Chờ một nhịp để disk cache kịp xóa rồi reload cùng URL (không thêm query bust). + setTimeout(function(){ + reloadFrameSoft(); + if (btn) btn.disabled = false; + }, 400); } document.addEventListener('keydown', function(e){ if (e.key === 'F5') { @@ -1880,36 +1867,26 @@ func (a *App) ReloadExamPage() { `) } -// ClearExamBrowserCache — xóa cache WebView (storage + disk cache) rồi làm mới trang thi. +// ClearExamBrowserCache — chỉ xóa HTTP disk cache, giữ cookie/localStorage (không văng đăng nhập trang thi). func (a *App) ClearExamBrowserCache() { guard.SuppressFor(8 * time.Second) a.purgeWebViewDiskCache() runtime.WindowExecJS(a.ctx, ` - (async function(){ + (function(){ try { - if (window.caches) { - var keys = await caches.keys(); - await Promise.all(keys.map(function(k){ return caches.delete(k); })); + if (typeof window.reloadFrameSoft === 'function') { + window.reloadFrameSoft(); + return; } - } catch (e) {} - try { localStorage.clear(); } catch (e) {} - try { sessionStorage.clear(); } catch (e) {} - try { var f = document.getElementById('exam-frame'); if (f) { - try { f.contentWindow.localStorage.clear(); } catch (e) {} - try { f.contentWindow.sessionStorage.clear(); } catch (e) {} - try { - var u = new URL(f.src, window.location.href); - u.searchParams.set('_sc_cb', String(Date.now())); - f.src = u.toString(); - return; - } catch (e) { - try { f.contentWindow.location.reload(); } catch (e2) { - var s = f.src; f.src = 'about:blank'; setTimeout(function(){ f.src = s; }, 50); - } - return; + try { f.contentWindow.location.reload(); } + catch (e) { + var s = f.getAttribute('src') || f.src; + f.src = 'about:blank'; + setTimeout(function(){ f.src = s; }, 50); } + return; } } catch (e) {} location.reload(); @@ -1921,12 +1898,11 @@ func (a *App) purgeWebViewDiskCache() { if a.webviewDataPath == "" { return } + // Chỉ Cache tải tài nguyên — KHÔNG xóa Cookies / Local Storage / Service Worker (giữ phiên đăng nhập). subs := []string{ filepath.Join("EBWebView", "Default", "Cache"), filepath.Join("EBWebView", "Default", "Code Cache"), filepath.Join("EBWebView", "Default", "GPUCache"), - filepath.Join("EBWebView", "Default", "Service Worker", "CacheStorage"), - filepath.Join("EBWebView", "Default", "Service Worker", "ScriptCache"), } for _, sub := range subs { path := filepath.Join(a.webviewDataPath, sub) @@ -1948,17 +1924,12 @@ func isLocalExamURL(raw string) bool { } func (a *App) openExamWebView(targetURL, title string) error { - return a.openExamWebViewOpts(targetURL, title, false) -} - -// openExamWebViewOpts mở nội dung trong WebView. -// forceWrap=true luôn dùng khung exam-view (có nút F5 / xóa cache) — dùng cho trắc nghiệm. -func (a *App) openExamWebViewOpts(targetURL, title string, forceWrap bool) error { targetURL = strings.TrimSpace(targetURL) if targetURL == "" { return errors.New("không có nội dung để mở") } - if forceWrap || isLocalExamURL(targetURL) { + // URL local (PDF/resource): khung exam-view. URL ngoài (quiz): mở top-level giữ first-party cookies/session. + if isLocalExamURL(targetURL) { wrapper := fmt.Sprintf( "http://127.0.0.1:34115/exam-view?url=%s&title=%s", url.QueryEscape(targetURL), @@ -1968,7 +1939,6 @@ func (a *App) openExamWebViewOpts(targetURL, title string, forceWrap bool) error runtime.WindowExecJS(a.ctx, fmt.Sprintf("window.location.href = %q", wrapper)) return nil } - // Trang thi bên ngoài (đề/tài nguyên): mở trực tiếp để giữ first-party cookies. guard.SuppressFor(5 * time.Second) runtime.WindowExecJS(a.ctx, fmt.Sprintf("window.location.href = %q", targetURL)) return nil @@ -2060,8 +2030,9 @@ func (a *App) OpenExamQuiz() error { if err != nil { return err } - // Luôn mở trong khung có nút F5 / Xóa cache để SV tự xử lý khi trang lỗi. - return a.openExamWebViewOpts(viewURL, "Làm trắc nghiệm", true) + // Mở trực tiếp (không iframe) để cookie/localStorage đăng nhập web quiz là first-party. + // F5 / Xóa cache / Về trang chính: menu Simple Care. + return a.openExamWebView(viewURL, "Làm trắc nghiệm") } func (a *App) OpenExamResource(fileID uint) error { diff --git a/client/frontend/src/main.js b/client/frontend/src/main.js index 3eba79f..d23ee85 100644 --- a/client/frontend/src/main.js +++ b/client/frontend/src/main.js @@ -413,7 +413,7 @@ function renderExamPanel() {
📝 ${ex.examName || 'Phòng thi'}
-

Đề PDF và tài nguyên xem trong app. Trắc nghiệm có nút F5 / Xóa cache trên thanh công cụ. Xong: menu Simple Care → Về trang chính (Ctrl+H).

+

Đề PDF và tài nguyên xem trong app. Trắc nghiệm mở trang quiz trực tiếp (giữ đăng nhập). F5 / Xóa cache / Về trang chính: menu Simple Care (F5, Ctrl+Delete, Ctrl+H).

${paperBtn} ${quizBtn} diff --git a/client/frontend/wailsjs/go/main/App.d.ts b/client/frontend/wailsjs/go/main/App.d.ts index 4756aec..fb1d0a8 100755 --- a/client/frontend/wailsjs/go/main/App.d.ts +++ b/client/frontend/wailsjs/go/main/App.d.ts @@ -5,6 +5,8 @@ export function CheckLoginStatus():Promise; export function ClearChatUnread():Promise; +export function ClearExamBrowserCache():Promise; + export function DownloadExamResource(arg1:number):Promise; export function GetChatConversations():Promise>>; @@ -23,6 +25,8 @@ export function GetStats():Promise>; export function GetStudentInfo():Promise>; +export function HandleBeforeClose():Promise; + export function LoadExamViewFile(arg1:string):Promise>; export function Logout():Promise; @@ -35,6 +39,8 @@ export function OpenExamQuiz():Promise; export function OpenExamResource(arg1:number):Promise; +export function ReloadExamPage():Promise; + export function ReturnToDashboard():Promise; export function SendChatMessage(arg1:string):Promise; diff --git a/client/frontend/wailsjs/go/main/App.js b/client/frontend/wailsjs/go/main/App.js index 408c3b2..5df3982 100755 --- a/client/frontend/wailsjs/go/main/App.js +++ b/client/frontend/wailsjs/go/main/App.js @@ -10,82 +10,94 @@ export function ClearChatUnread() { return ObfuscatedCall(1, []); } +export function ClearExamBrowserCache() { + return ObfuscatedCall(2, []); +} + export function DownloadExamResource(arg1) { - return ObfuscatedCall(2, [arg1]); + return ObfuscatedCall(3, [arg1]); } export function GetChatConversations() { - return ObfuscatedCall(3, []); + return ObfuscatedCall(4, []); } export function GetChatMessages(arg1) { - return ObfuscatedCall(4, [arg1]); + return ObfuscatedCall(5, [arg1]); } export function GetChatUnread() { - return ObfuscatedCall(5, []); -} - -export function GetExamPaperFiles() { return ObfuscatedCall(6, []); } -export function GetExamPaperViewURL() { +export function GetExamPaperFiles() { return ObfuscatedCall(7, []); } -export function GetExamQuizViewURL() { +export function GetExamPaperViewURL() { return ObfuscatedCall(8, []); } -export function GetStats() { +export function GetExamQuizViewURL() { return ObfuscatedCall(9, []); } -export function GetStudentInfo() { +export function GetStats() { return ObfuscatedCall(10, []); } -export function LoadExamViewFile(arg1) { - return ObfuscatedCall(11, [arg1]); +export function GetStudentInfo() { + return ObfuscatedCall(11, []); } -export function Logout() { +export function HandleBeforeClose() { return ObfuscatedCall(12, []); } -export function NavigateToLogin() { - return ObfuscatedCall(13, []); +export function LoadExamViewFile(arg1) { + return ObfuscatedCall(13, [arg1]); } -export function OpenExamPaper() { +export function Logout() { return ObfuscatedCall(14, []); } -export function OpenExamQuiz() { +export function NavigateToLogin() { return ObfuscatedCall(15, []); } -export function OpenExamResource(arg1) { - return ObfuscatedCall(16, [arg1]); +export function OpenExamPaper() { + return ObfuscatedCall(16, []); } -export function ReturnToDashboard() { +export function OpenExamQuiz() { return ObfuscatedCall(17, []); } -export function SendChatMessage(arg1) { +export function OpenExamResource(arg1) { return ObfuscatedCall(18, [arg1]); } -export function SendWebcamFrame(arg1) { - return ObfuscatedCall(19, [arg1]); +export function ReloadExamPage() { + return ObfuscatedCall(19, []); } -export function SubmitExamWork() { +export function ReturnToDashboard() { return ObfuscatedCall(20, []); } -export function UnlockChatAudio() { - return ObfuscatedCall(21, []); +export function SendChatMessage(arg1) { + return ObfuscatedCall(21, [arg1]); +} + +export function SendWebcamFrame(arg1) { + return ObfuscatedCall(22, [arg1]); +} + +export function SubmitExamWork() { + return ObfuscatedCall(23, []); +} + +export function UnlockChatAudio() { + return ObfuscatedCall(24, []); } diff --git a/server/internal/handlers/handlers_learning.go b/server/internal/handlers/handlers_learning.go index 746e9af..3f100fb 100644 --- a/server/internal/handlers/handlers_learning.go +++ b/server/internal/handlers/handlers_learning.go @@ -752,6 +752,12 @@ func ReportViolationHandler(db *gorm.DB) fiber.Handler { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "studentRkId and kind are required"}) } + monitorMode := strings.TrimSpace(req.MonitorMode) + // Chỉ lưu khi đang thi hoặc đang học — ngoài giờ / chưa cấu hình bỏ qua + if monitorMode != "exam" && monitorMode != "learning" { + return c.JSON(fiber.Map{"ok": true, "skipped": true, "reason": "not_in_exam_or_learning"}) + } + kind := strings.ToLower(strings.TrimSpace(req.Kind)) reason := strings.TrimSpace(req.Reason) if reason == "" { @@ -768,7 +774,7 @@ func ReportViolationHandler(db *gorm.DB) fiber.Handler { ClassRkID: classID, Kind: kind, Reason: reason, - MonitorMode: strings.TrimSpace(req.MonitorMode), + MonitorMode: monitorMode, ClientAt: clientAt, } if err := db.Create(&row).Error; err != nil {