%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 {