From a2ec923788b39168c991f7f21762b21496bc86ea Mon Sep 17 00:00:00 2001 From: Yukino_fox Date: Mon, 4 May 2026 12:11:18 +0800 Subject: [PATCH] =?UTF-8?q?feat(URL=E5=8D=8F=E8=AE=AE):=20=E5=A2=9E?= =?UTF-8?q?=E5=BC=BAURL=E5=8D=8F=E8=AE=AE=E6=B3=A8=E5=86=8C=E5=81=A5?= =?UTF-8?q?=E5=A3=AE=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增check_url_protocol_status命令检测注册状态 - 新增unregister_url_protocol命令取消注册 - 改进register_url_protocol命令增加验证逻辑 - 前端增加状态检测、自动重试和操作日志功能 - 更新多语言翻译支持新功能 --- .eslintcache | 2 +- .../documents/url-registration-robustness.md | 238 ++++++ public/about-content.json | 6 +- src-tauri/Cargo.lock | 2 + src-tauri/Cargo.toml | 4 + src-tauri/src/commands/app.rs | 705 +++++++++++++++++- src-tauri/src/commands/auth.rs | 15 +- src-tauri/src/lib.rs | 4 + src/App.tsx | 23 + src/components/AutoScore/AutoScoreUtils.ts | 12 +- src/components/ContentArea.tsx | 1 - src/components/OAuth/OAuthLogin.tsx | 6 +- src/components/SectlCloudSyncPanel.tsx | 402 ++++++++++ src/components/Settings.tsx | 662 ++++++++++++---- src/i18n/locales/ar-SA.json | 75 +- src/i18n/locales/de-DE.json | 77 +- src/i18n/locales/en-US.json | 75 +- src/i18n/locales/es-ES.json | 75 +- src/i18n/locales/fr-FR.json | 75 +- src/i18n/locales/ja-JP.json | 77 +- src/i18n/locales/ko-KR.json | 75 +- src/i18n/locales/pt-BR.json | 75 +- src/i18n/locales/ru-RU.json | 75 +- src/i18n/locales/zh-CN.json | 75 +- src/preload/types.ts | 42 +- src/services/scoreSyncService.ts | 10 +- src/services/sectlCloudSync.ts | 507 +++++++++++++ src/services/sectlKVStorage.ts | 280 ++----- 28 files changed, 3246 insertions(+), 429 deletions(-) create mode 100644 .trae/documents/url-registration-robustness.md create mode 100644 src/components/SectlCloudSyncPanel.tsx create mode 100644 src/services/sectlCloudSync.ts diff --git a/.eslintcache b/.eslintcache index 35e5d15..7dfb08d 100644 --- a/.eslintcache +++ b/.eslintcache @@ -1 +1 @@ -[{"E:\\Document\\Coding\\SecScore\\eslint.config.mjs":"1","E:\\Document\\Coding\\SecScore\\src\\App.tsx":"2","E:\\Document\\Coding\\SecScore\\src\\ClientContext.ts":"3","E:\\Document\\Coding\\SecScore\\src\\components\\BoardManager.tsx":"4","E:\\Document\\Coding\\SecScore\\src\\components\\ContentArea.tsx":"5","E:\\Document\\Coding\\SecScore\\src\\components\\EventHistory.tsx":"6","E:\\Document\\Coding\\SecScore\\src\\components\\Home.tsx":"7","E:\\Document\\Coding\\SecScore\\src\\components\\Leaderboard.tsx":"8","E:\\Document\\Coding\\SecScore\\src\\components\\OOBE\\OOBE.tsx":"9","E:\\Document\\Coding\\SecScore\\src\\components\\OOBE\\OOBEBackground.tsx":"10","E:\\Document\\Coding\\SecScore\\src\\components\\ReasonManager.tsx":"11","E:\\Document\\Coding\\SecScore\\src\\components\\RewardExchange.tsx":"12","E:\\Document\\Coding\\SecScore\\src\\components\\RewardSettings.tsx":"13","E:\\Document\\Coding\\SecScore\\src\\components\\ScoreManager.tsx":"14","E:\\Document\\Coding\\SecScore\\src\\components\\Settings.tsx":"15","E:\\Document\\Coding\\SecScore\\src\\components\\SettlementHistory.tsx":"16","E:\\Document\\Coding\\SecScore\\src\\components\\Sidebar.tsx":"17","E:\\Document\\Coding\\SecScore\\src\\components\\StudentManager.tsx":"18","E:\\Document\\Coding\\SecScore\\src\\components\\TagEditorDialog.tsx":"19","E:\\Document\\Coding\\SecScore\\src\\components\\ThemeEditor.tsx":"20","E:\\Document\\Coding\\SecScore\\src\\components\\ThemeQuickSettings.tsx":"21","E:\\Document\\Coding\\SecScore\\src\\components\\TitleBar.tsx":"22","E:\\Document\\Coding\\SecScore\\src\\components\\Versions.tsx":"23","E:\\Document\\Coding\\SecScore\\src\\components\\WindowControls.tsx":"24","E:\\Document\\Coding\\SecScore\\src\\components\\Wizard.tsx":"25","E:\\Document\\Coding\\SecScore\\src\\contexts\\ServiceContext.tsx":"26","E:\\Document\\Coding\\SecScore\\src\\contexts\\ThemeContext.tsx":"27","E:\\Document\\Coding\\SecScore\\src\\contexts\\ThemeEditorContext.tsx":"28","E:\\Document\\Coding\\SecScore\\src\\env.d.ts":"29","E:\\Document\\Coding\\SecScore\\src\\hooks\\useResponsive.ts":"30","E:\\Document\\Coding\\SecScore\\src\\i18n\\index.ts":"31","E:\\Document\\Coding\\SecScore\\src\\main.tsx":"32","E:\\Document\\Coding\\SecScore\\src\\preload\\types.ts":"33","E:\\Document\\Coding\\SecScore\\src\\react-19-patch.ts":"34","E:\\Document\\Coding\\SecScore\\src\\services\\StudentService.ts":"35","E:\\Document\\Coding\\SecScore\\src\\shared\\kernel.ts":"36","E:\\Document\\Coding\\SecScore\\src\\shared\\mobileNavigation.ts":"37","E:\\Document\\Coding\\SecScore\\src\\utils\\color.ts":"38","E:\\Document\\Coding\\SecScore\\src\\utils\\studentAvatar.ts":"39","E:\\Document\\Coding\\SecScore\\src\\workers\\xlsxWorker.ts":"40","E:\\Document\\Coding\\SecScore\\vite.config.ts":"41","E:\\Document\\Coding\\SecScore\\src\\components\\AutoScore\\ActionEditor.tsx":"42","E:\\Document\\Coding\\SecScore\\src\\components\\AutoScore\\AutoScoreUtils.ts":"43","E:\\Document\\Coding\\SecScore\\src\\components\\AutoScore\\IntervalValueCodec.ts":"44","E:\\Document\\Coding\\SecScore\\src\\components\\AutoScore\\IntervalValueWidget.tsx":"45","E:\\Document\\Coding\\SecScore\\src\\components\\AutoScore\\TriggerRuleBuilder.tsx":"46","E:\\Document\\Coding\\SecScore\\src\\components\\AutoScoreManager.tsx":"47","E:\\Document\\Coding\\SecScore\\src\\components\\OAuth\\OAuthCallback.tsx":"48","E:\\Document\\Coding\\SecScore\\src\\components\\OAuth\\OAuthLogin.tsx":"49","E:\\Document\\Coding\\SecScore\\src\\components\\SectlCloudStorageManager.tsx":"50","E:\\Document\\Coding\\SecScore\\src\\components\\SectlLoginButton.tsx":"51","E:\\Document\\Coding\\SecScore\\src\\components\\SectlSettingsPanel.tsx":"52","E:\\Document\\Coding\\SecScore\\src\\contexts\\SectlContext.tsx":"53","E:\\Document\\Coding\\SecScore\\src\\services\\sectl.ts":"54","E:\\Document\\Coding\\SecScore\\src\\services\\sectlAuth.ts":"55","E:\\Document\\Coding\\SecScore\\src\\services\\sectlCloudStorage.ts":"56","E:\\Document\\Coding\\SecScore\\src\\services\\sectlKVStorage.ts":"57","E:\\Document\\Coding\\SecScore\\src\\services\\sectlNotification.ts":"58","E:\\Document\\Coding\\SecScore\\src\\views\\SectlCloudView.tsx":"59","E:\\Document\\Coding\\SecScore\\src\\components\\ScoreSyncPanel.tsx":"60","E:\\Document\\Coding\\SecScore\\src\\services\\scoreSyncService.ts":"61","E:\\Document\\Coding\\SecScore\\src\\shared\\fontFamily.ts":"62","E:\\Document\\Coding\\SecScore\\src\\components\\PluginManager.tsx":"63","E:\\Document\\Coding\\SecScore\\src\\components\\SectlKVStorageManager.tsx":"64","E:\\Document\\Coding\\SecScore\\src\\hooks\\useOAuthPersist.ts":"65","E:\\Document\\Coding\\SecScore\\src\\plugins\\runtime.ts":"66"},{"size":1413,"mtime":1774084765386,"results":"67","hashOfConfig":"68"},{"size":42741,"mtime":1776572521716,"results":"69","hashOfConfig":"70"},{"size":124,"mtime":1774084765537,"results":"71","hashOfConfig":"70"},{"size":52571,"mtime":1776572523863,"results":"72","hashOfConfig":"70"},{"size":23453,"mtime":1776572524142,"results":"73","hashOfConfig":"70"},{"size":2044,"mtime":1774084765557,"results":"74","hashOfConfig":"70"},{"size":119471,"mtime":1776572525236,"results":"75","hashOfConfig":"70"},{"size":7976,"mtime":1775906174095,"results":"76","hashOfConfig":"70"},{"size":36271,"mtime":1774176288912,"results":"77","hashOfConfig":"70"},{"size":4203,"mtime":1774084765566,"results":"78","hashOfConfig":"70"},{"size":5983,"mtime":1774084765569,"results":"79","hashOfConfig":"70"},{"size":11972,"mtime":1776572526627,"results":"80","hashOfConfig":"70"},{"size":6151,"mtime":1774084765572,"results":"81","hashOfConfig":"70"},{"size":13165,"mtime":1775882074040,"results":"82","hashOfConfig":"70"},{"size":63577,"mtime":1776572527590,"results":"83","hashOfConfig":"70"},{"size":5950,"mtime":1774084765578,"results":"84","hashOfConfig":"70"},{"size":10638,"mtime":1775984538679,"results":"85","hashOfConfig":"70"},{"size":80155,"mtime":1776572075970,"results":"86","hashOfConfig":"70"},{"size":6331,"mtime":1774084765584,"results":"87","hashOfConfig":"70"},{"size":8079,"mtime":1774084765586,"results":"88","hashOfConfig":"70"},{"size":10954,"mtime":1774084765587,"results":"89","hashOfConfig":"70"},{"size":3858,"mtime":1774084765589,"results":"90","hashOfConfig":"70"},{"size":431,"mtime":1774084765590,"results":"91","hashOfConfig":"70"},{"size":2367,"mtime":1774084765592,"results":"92","hashOfConfig":"70"},{"size":1560,"mtime":1774084765594,"results":"93","hashOfConfig":"70"},{"size":362,"mtime":1774084765604,"results":"94","hashOfConfig":"70"},{"size":5985,"mtime":1774084765606,"results":"95","hashOfConfig":"70"},{"size":2824,"mtime":1774084765608,"results":"96","hashOfConfig":"70"},{"size":38,"mtime":1774084765611,"results":"97","hashOfConfig":"70"},{"size":2097,"mtime":1774084765613,"results":"98","hashOfConfig":"70"},{"size":2771,"mtime":1775828716269,"results":"99","hashOfConfig":"70"},{"size":6919,"mtime":1774704831300,"results":"100","hashOfConfig":"70"},{"size":26732,"mtime":1776603103264,"results":"101","hashOfConfig":"70"},{"size":1150,"mtime":1774084765630,"results":"102","hashOfConfig":"70"},{"size":667,"mtime":1774084765638,"results":"103","hashOfConfig":"70"},{"size":3061,"mtime":1774176077013,"results":"104","hashOfConfig":"70"},{"size":1909,"mtime":1775984538700,"results":"105","hashOfConfig":"70"},{"size":3256,"mtime":1774084765655,"results":"106","hashOfConfig":"70"},{"size":1115,"mtime":1774084765658,"results":"107","hashOfConfig":"70"},{"size":947,"mtime":1774084765660,"results":"108","hashOfConfig":"70"},{"size":709,"mtime":1774665241507,"results":"109","hashOfConfig":"70"},{"size":4778,"mtime":1775878592427,"results":"110","hashOfConfig":"70"},{"size":17053,"mtime":1775885815750,"results":"111","hashOfConfig":"70"},{"size":3739,"mtime":1775878592450,"results":"112","hashOfConfig":"70"},{"size":3033,"mtime":1775882069874,"results":"113","hashOfConfig":"70"},{"size":1907,"mtime":1775878592519,"results":"114","hashOfConfig":"70"},{"size":30144,"mtime":1775984538667,"results":"115","hashOfConfig":"70"},{"size":1047,"mtime":1775224756086,"results":"116","hashOfConfig":"70"},{"size":10330,"mtime":1776572525687,"results":"117","hashOfConfig":"70"},{"size":8410,"mtime":1775383418692,"results":"118","hashOfConfig":"70"},{"size":1661,"mtime":1775382857634,"results":"119","hashOfConfig":"70"},{"size":5070,"mtime":1776602716634,"results":"120","hashOfConfig":"70"},{"size":3773,"mtime":1775383419736,"results":"121","hashOfConfig":"70"},{"size":683,"mtime":1775383317477,"results":"122","hashOfConfig":"70"},{"size":9500,"mtime":1775383420349,"results":"123","hashOfConfig":"70"},{"size":14984,"mtime":1775382860520,"results":"124","hashOfConfig":"70"},{"size":6643,"mtime":1776602610352,"results":"125","hashOfConfig":"70"},{"size":5437,"mtime":1775382860586,"results":"126","hashOfConfig":"70"},{"size":429,"mtime":1775383291875,"results":"127","hashOfConfig":"70"},{"size":8415,"mtime":1775828714587,"results":"128","hashOfConfig":"70"},{"size":6935,"mtime":1776602862121,"results":"129","hashOfConfig":"70"},{"size":1359,"mtime":1775878592716,"results":"130","hashOfConfig":"70"},{"size":9135,"mtime":1775985879564,"results":"131","hashOfConfig":"70"},{"size":12119,"mtime":1776602943160,"results":"132","hashOfConfig":"70"},{"size":3623,"mtime":1776492469178,"results":"133","hashOfConfig":"70"},{"size":6871,"mtime":1775985883560,"results":"134","hashOfConfig":"70"},{"filePath":"135","messages":"136","suppressedMessages":"137","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"oyxevq",{"filePath":"138","messages":"139","suppressedMessages":"140","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"f6a32r",{"filePath":"141","messages":"142","suppressedMessages":"143","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"144","messages":"145","suppressedMessages":"146","errorCount":0,"fatalErrorCount":0,"warningCount":2,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},{"filePath":"147","messages":"148","suppressedMessages":"149","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"150","messages":"151","suppressedMessages":"152","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"153","messages":"154","suppressedMessages":"155","errorCount":0,"fatalErrorCount":0,"warningCount":3,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},{"filePath":"156","messages":"157","suppressedMessages":"158","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"159","messages":"160","suppressedMessages":"161","errorCount":0,"fatalErrorCount":0,"warningCount":2,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},{"filePath":"162","messages":"163","suppressedMessages":"164","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"165","messages":"166","suppressedMessages":"167","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"168","messages":"169","suppressedMessages":"170","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"171","messages":"172","suppressedMessages":"173","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"174","messages":"175","suppressedMessages":"176","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"177","messages":"178","suppressedMessages":"179","errorCount":0,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},{"filePath":"180","messages":"181","suppressedMessages":"182","errorCount":0,"fatalErrorCount":0,"warningCount":2,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},{"filePath":"183","messages":"184","suppressedMessages":"185","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"186","messages":"187","suppressedMessages":"188","errorCount":0,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},{"filePath":"189","messages":"190","suppressedMessages":"191","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"192","messages":"193","suppressedMessages":"194","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"195","messages":"196","suppressedMessages":"197","errorCount":0,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},{"filePath":"198","messages":"199","suppressedMessages":"200","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"201","messages":"202","suppressedMessages":"203","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"204","messages":"205","suppressedMessages":"206","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"207","messages":"208","suppressedMessages":"209","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"210","messages":"211","suppressedMessages":"212","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"213","messages":"214","suppressedMessages":"215","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"216","messages":"217","suppressedMessages":"218","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"219","messages":"220","suppressedMessages":"221","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"222","messages":"223","suppressedMessages":"224","errorCount":0,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},{"filePath":"225","messages":"226","suppressedMessages":"227","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"228","messages":"229","suppressedMessages":"230","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"231","messages":"232","suppressedMessages":"233","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"234","messages":"235","suppressedMessages":"236","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"237","messages":"238","suppressedMessages":"239","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"240","messages":"241","suppressedMessages":"242","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"243","messages":"244","suppressedMessages":"245","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"246","messages":"247","suppressedMessages":"248","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"249","messages":"250","suppressedMessages":"251","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"252","messages":"253","suppressedMessages":"254","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"255","messages":"256","suppressedMessages":"257","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"258","messages":"259","suppressedMessages":"260","errorCount":0,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},{"filePath":"261","messages":"262","suppressedMessages":"263","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"264","messages":"265","suppressedMessages":"266","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"267","messages":"268","suppressedMessages":"269","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"270","messages":"271","suppressedMessages":"272","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"273","messages":"274","suppressedMessages":"275","errorCount":0,"fatalErrorCount":0,"warningCount":3,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},{"filePath":"276","messages":"277","suppressedMessages":"278","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"279","messages":"280","suppressedMessages":"281","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"282","messages":"283","suppressedMessages":"284","errorCount":0,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},{"filePath":"285","messages":"286","suppressedMessages":"287","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"288","messages":"289","suppressedMessages":"290","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"291","messages":"292","suppressedMessages":"293","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"294","messages":"295","suppressedMessages":"296","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"297","messages":"298","suppressedMessages":"299","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"300","messages":"301","suppressedMessages":"302","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"303","messages":"304","suppressedMessages":"305","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"306","messages":"307","suppressedMessages":"308","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"309","messages":"310","suppressedMessages":"311","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"312","messages":"313","suppressedMessages":"314","errorCount":0,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},{"filePath":"315","messages":"316","suppressedMessages":"317","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"318","messages":"319","suppressedMessages":"320","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"321","messages":"322","suppressedMessages":"323","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"324","messages":"325","suppressedMessages":"326","errorCount":0,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},{"filePath":"327","messages":"328","suppressedMessages":"329","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"330","messages":"331","suppressedMessages":"332","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"E:\\Document\\Coding\\SecScore\\eslint.config.mjs",[],[],"E:\\Document\\Coding\\SecScore\\src\\App.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\ClientContext.ts",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\BoardManager.tsx",["333","334"],[],"E:\\Document\\Coding\\SecScore\\src\\components\\ContentArea.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\EventHistory.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\Home.tsx",["335","336","337"],[],"E:\\Document\\Coding\\SecScore\\src\\components\\Leaderboard.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\OOBE\\OOBE.tsx",["338","339"],[],"E:\\Document\\Coding\\SecScore\\src\\components\\OOBE\\OOBEBackground.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\ReasonManager.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\RewardExchange.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\RewardSettings.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\ScoreManager.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\Settings.tsx",["340"],[],"E:\\Document\\Coding\\SecScore\\src\\components\\SettlementHistory.tsx",["341","342"],[],"E:\\Document\\Coding\\SecScore\\src\\components\\Sidebar.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\StudentManager.tsx",["343"],[],"E:\\Document\\Coding\\SecScore\\src\\components\\TagEditorDialog.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\ThemeEditor.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\ThemeQuickSettings.tsx",["344"],[],"E:\\Document\\Coding\\SecScore\\src\\components\\TitleBar.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\Versions.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\WindowControls.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\Wizard.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\contexts\\ServiceContext.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\contexts\\ThemeContext.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\contexts\\ThemeEditorContext.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\env.d.ts",[],[],"E:\\Document\\Coding\\SecScore\\src\\hooks\\useResponsive.ts",["345"],[],"E:\\Document\\Coding\\SecScore\\src\\i18n\\index.ts",[],[],"E:\\Document\\Coding\\SecScore\\src\\main.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\preload\\types.ts",[],[],"E:\\Document\\Coding\\SecScore\\src\\react-19-patch.ts",[],[],"E:\\Document\\Coding\\SecScore\\src\\services\\StudentService.ts",[],[],"E:\\Document\\Coding\\SecScore\\src\\shared\\kernel.ts",[],[],"E:\\Document\\Coding\\SecScore\\src\\shared\\mobileNavigation.ts",[],[],"E:\\Document\\Coding\\SecScore\\src\\utils\\color.ts",[],[],"E:\\Document\\Coding\\SecScore\\src\\utils\\studentAvatar.ts",[],[],"E:\\Document\\Coding\\SecScore\\src\\workers\\xlsxWorker.ts",[],[],"E:\\Document\\Coding\\SecScore\\vite.config.ts",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\AutoScore\\ActionEditor.tsx",["346"],[],"E:\\Document\\Coding\\SecScore\\src\\components\\AutoScore\\AutoScoreUtils.ts",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\AutoScore\\IntervalValueCodec.ts",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\AutoScore\\IntervalValueWidget.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\AutoScore\\TriggerRuleBuilder.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\AutoScoreManager.tsx",["347","348","349"],[],"E:\\Document\\Coding\\SecScore\\src\\components\\OAuth\\OAuthCallback.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\OAuth\\OAuthLogin.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\SectlCloudStorageManager.tsx",["350"],[],"E:\\Document\\Coding\\SecScore\\src\\components\\SectlLoginButton.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\SectlSettingsPanel.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\contexts\\SectlContext.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\services\\sectl.ts",[],[],"E:\\Document\\Coding\\SecScore\\src\\services\\sectlAuth.ts",[],[],"E:\\Document\\Coding\\SecScore\\src\\services\\sectlCloudStorage.ts",[],[],"E:\\Document\\Coding\\SecScore\\src\\services\\sectlKVStorage.ts",[],[],"E:\\Document\\Coding\\SecScore\\src\\services\\sectlNotification.ts",[],[],"E:\\Document\\Coding\\SecScore\\src\\views\\SectlCloudView.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\ScoreSyncPanel.tsx",["351"],[],"E:\\Document\\Coding\\SecScore\\src\\services\\scoreSyncService.ts",[],[],"E:\\Document\\Coding\\SecScore\\src\\shared\\fontFamily.ts",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\PluginManager.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\SectlKVStorageManager.tsx",["352"],[],"E:\\Document\\Coding\\SecScore\\src\\hooks\\useOAuthPersist.ts",[],[],"E:\\Document\\Coding\\SecScore\\src\\plugins\\runtime.ts",[],[],{"ruleId":"353","severity":1,"message":"354","line":635,"column":6,"nodeType":"355","endLine":635,"endColumn":44,"suggestions":"356"},{"ruleId":"353","severity":1,"message":"354","line":643,"column":6,"nodeType":"355","endLine":643,"endColumn":59,"suggestions":"357"},{"ruleId":"353","severity":1,"message":"358","line":556,"column":6,"nodeType":"355","endLine":556,"endColumn":76,"suggestions":"359"},{"ruleId":"353","severity":1,"message":"360","line":580,"column":6,"nodeType":"355","endLine":580,"endColumn":15,"suggestions":"361"},{"ruleId":"353","severity":1,"message":"362","line":2418,"column":6,"nodeType":"355","endLine":2418,"endColumn":8,"suggestions":"363"},{"ruleId":"353","severity":1,"message":"364","line":160,"column":6,"nodeType":"355","endLine":160,"endColumn":20,"suggestions":"365"},{"ruleId":"353","severity":1,"message":"366","line":334,"column":5,"nodeType":"355","endLine":334,"endColumn":27,"suggestions":"367"},{"ruleId":"353","severity":1,"message":"368","line":414,"column":6,"nodeType":"355","endLine":414,"endColumn":8,"suggestions":"369"},{"ruleId":"353","severity":1,"message":"370","line":54,"column":6,"nodeType":"355","endLine":54,"endColumn":18,"suggestions":"371"},{"ruleId":"353","severity":1,"message":"360","line":106,"column":5,"nodeType":"355","endLine":106,"endColumn":7,"suggestions":"372"},{"ruleId":"353","severity":1,"message":"373","line":1373,"column":6,"nodeType":"355","endLine":1373,"endColumn":50,"suggestions":"374"},{"ruleId":"353","severity":1,"message":"364","line":100,"column":6,"nodeType":"355","endLine":100,"endColumn":20,"suggestions":"375"},{"ruleId":"353","severity":1,"message":"376","line":25,"column":9,"nodeType":"377","endLine":25,"endColumn":70},{"ruleId":"353","severity":1,"message":"378","line":23,"column":9,"nodeType":"377","endLine":23,"endColumn":63},{"ruleId":"353","severity":1,"message":"360","line":167,"column":5,"nodeType":"355","endLine":167,"endColumn":55,"suggestions":"379"},{"ruleId":"353","severity":1,"message":"380","line":329,"column":6,"nodeType":"355","endLine":329,"endColumn":15,"suggestions":"381"},{"ruleId":"353","severity":1,"message":"382","line":393,"column":6,"nodeType":"355","endLine":393,"endColumn":55,"suggestions":"383"},{"ruleId":"353","severity":1,"message":"384","line":79,"column":6,"nodeType":"355","endLine":79,"endColumn":23,"suggestions":"385"},{"ruleId":"353","severity":1,"message":"386","line":69,"column":6,"nodeType":"355","endLine":69,"endColumn":23,"suggestions":"387"},{"ruleId":"353","severity":1,"message":"388","line":99,"column":6,"nodeType":"355","endLine":99,"endColumn":31,"suggestions":"389"},"react-hooks/exhaustive-deps","React Hook useEffect has a missing dependency: 'activeBoard'. Either include it or remove the dependency array.","ArrayExpression",["390"],["391"],"React Hook useMemo has an unnecessary dependency: 'layoutType'. Either exclude it or remove the dependency array.",["392"],"React Hook useMemo has a missing dependency: 't'. Either include it or remove the dependency array.",["393"],"React Hook useEffect has missing dependencies: 'onGlobalMouseMove' and 'onGlobalMouseUp'. Either include them or remove the dependency array.",["394"],"React Hook useEffect has a missing dependency: 't'. Either include it or remove the dependency array.",["395"],"React Hook useCallback has missing dependencies: 'showOobeMessage' and 't'. Either include them or remove the dependency array.",["396"],"React Hook useEffect has a missing dependency: 'loadAll'. Either include it or remove the dependency array.",["397"],"React Hook useCallback has a missing dependency: 't'. Either include it or remove the dependency array.",["398"],["399"],"React Hook useMemo has missing dependencies: 'handleDelete', 'handleOpenAvatarEditor', 'handleOpenGroupEditor', and 'handleOpenTagEditor'. Either include them or remove the dependency array.",["400"],["401"],"The 'breakpoints' object makes the dependencies of useEffect Hook (at line 49) change on every render. Move it inside the useEffect callback. Alternatively, wrap the initialization of 'breakpoints' in its own useMemo() Hook.","VariableDeclarator","The 'safeValue' conditional could make the dependencies of useMemo Hook (at line 59) change on every render. To fix this, wrap the initialization of 'safeValue' in its own useMemo() Hook.",["402"],"React Hook useEffect has missing dependencies: 'fetchBatches', 'fetchRules', 'fetchStudents', and 'fetchTags'. Either include them or remove the dependency array.",["403"],"React Hook useEffect has missing dependencies: 'fetchBatches' and 'fetchRules'. Either include them or remove the dependency array.",["404"],"React Hook useEffect has missing dependencies: 'loadFiles' and 'loadStorageUsage'. Either include them or remove the dependency array.",["405"],"React Hook useEffect has a missing dependency: 'checkCloudStatus'. Either include it or remove the dependency array.",["406"],"React Hook useEffect has a missing dependency: 'loadKVList'. Either include it or remove the dependency array.",["407"],{"desc":"408","fix":"409"},{"desc":"410","fix":"411"},{"desc":"412","fix":"413"},{"desc":"414","fix":"415"},{"desc":"416","fix":"417"},{"desc":"418","fix":"419"},{"desc":"420","fix":"421"},{"desc":"422","fix":"423"},{"desc":"424","fix":"425"},{"desc":"426","fix":"427"},{"desc":"428","fix":"429"},{"desc":"418","fix":"430"},{"desc":"431","fix":"432"},{"desc":"433","fix":"434"},{"desc":"435","fix":"436"},{"desc":"437","fix":"438"},{"desc":"439","fix":"440"},{"desc":"441","fix":"442"},"Update the dependencies array to be: [activeBoard, activeBoard.id, activeBoard.layout]",{"range":"443","text":"444"},"Update the dependencies array to be: [activeBoard, activeBoard.id, listConfigSignature, runAllInBoard]",{"range":"445","text":"446"},"Update the dependencies array to be: [sortedStudents, sortType, searchKeyword, getGroupName, t]",{"range":"447","text":"448"},"Update the dependencies array to be: [reasons, t]",{"range":"449","text":"450"},"Update the dependencies array to be: [onGlobalMouseMove, onGlobalMouseUp]",{"range":"451","text":"452"},"Update the dependencies array to be: [currentTheme, t]",{"range":"453","text":"454"},"Update the dependencies array to be: [students, showOobeMessage, t]",{"range":"455","text":"456"},"Update the dependencies array to be: [loadAll]",{"range":"457","text":"458"},"Update the dependencies array to be: [messageApi, t]",{"range":"459","text":"460"},"Update the dependencies array to be: [t]",{"range":"461","text":"462"},"Update the dependencies array to be: [t, isMobile, canEdit, handleOpenTagEditor, handleOpenGroupEditor, handleOpenAvatarEditor, handleDelete]",{"range":"463","text":"464"},{"range":"465","text":"454"},"Update the dependencies array to be: [t, tagOptions]",{"range":"466","text":"467"},"Update the dependencies array to be: [canEdit, fetchBatches, fetchRules, fetchStudents, fetchTags]",{"range":"468","text":"469"},"Update the dependencies array to be: [backfillPrompted, canEdit, fetchBatches, fetchRules, messageApi, rules, t]",{"range":"470","text":"471"},"Update the dependencies array to be: [isAuthenticated, loadFiles, loadStorageUsage]",{"range":"472","text":"473"},"Update the dependencies array to be: [checkCloudStatus, isAuthenticated]",{"range":"474","text":"475"},"Update the dependencies array to be: [isAuthenticated, loadKVList, prefix]",{"range":"476","text":"477"},[18439,18477],"[activeBoard, activeBoard.id, activeBoard.layout]",[18687,18740],"[activeBoard, activeBoard.id, listConfigSignature, runAllInBoard]",[17285,17355],"[sortedStudents, sortType, searchKeyword, getGroupName, t]",[18130,18139],"[reasons, t]",[77698,77700],"[onGlobalMouseMove, onGlobalMouseUp]",[4674,4688],"[currentTheme, t]",[10389,10411],"[students, showOobeMessage, t]",[13208,13210],"[loadAll]",[1697,1709],"[messageApi, t]",[3343,3345],"[t]",[46442,46486],"[t, isMobile, canEdit, handleOpenTagEditor, handleOpenGroupEditor, handleOpenAvatarEditor, handleDelete]",[3185,3199],[4240,4290],"[t, tagOptions]",[9181,9190],"[canEdit, fetchBatches, fetchRules, fetchStudents, fetchTags]",[11353,11402],"[backfillPrompted, canEdit, fetchBatches, fetchRules, messageApi, rules, t]",[1826,1843],"[isAuthenticated, loadFiles, loadStorageUsage]",[1783,1800],"[checkCloudStatus, isAuthenticated]",[2216,2241],"[isAuthenticated, loadKVList, prefix]"] \ No newline at end of file +[{"E:\\Document\\Coding\\SecScore\\eslint.config.mjs":"1","E:\\Document\\Coding\\SecScore\\src\\App.tsx":"2","E:\\Document\\Coding\\SecScore\\src\\ClientContext.ts":"3","E:\\Document\\Coding\\SecScore\\src\\components\\BoardManager.tsx":"4","E:\\Document\\Coding\\SecScore\\src\\components\\ContentArea.tsx":"5","E:\\Document\\Coding\\SecScore\\src\\components\\EventHistory.tsx":"6","E:\\Document\\Coding\\SecScore\\src\\components\\Home.tsx":"7","E:\\Document\\Coding\\SecScore\\src\\components\\Leaderboard.tsx":"8","E:\\Document\\Coding\\SecScore\\src\\components\\OOBE\\OOBE.tsx":"9","E:\\Document\\Coding\\SecScore\\src\\components\\OOBE\\OOBEBackground.tsx":"10","E:\\Document\\Coding\\SecScore\\src\\components\\ReasonManager.tsx":"11","E:\\Document\\Coding\\SecScore\\src\\components\\RewardExchange.tsx":"12","E:\\Document\\Coding\\SecScore\\src\\components\\RewardSettings.tsx":"13","E:\\Document\\Coding\\SecScore\\src\\components\\ScoreManager.tsx":"14","E:\\Document\\Coding\\SecScore\\src\\components\\Settings.tsx":"15","E:\\Document\\Coding\\SecScore\\src\\components\\SettlementHistory.tsx":"16","E:\\Document\\Coding\\SecScore\\src\\components\\Sidebar.tsx":"17","E:\\Document\\Coding\\SecScore\\src\\components\\StudentManager.tsx":"18","E:\\Document\\Coding\\SecScore\\src\\components\\TagEditorDialog.tsx":"19","E:\\Document\\Coding\\SecScore\\src\\components\\ThemeEditor.tsx":"20","E:\\Document\\Coding\\SecScore\\src\\components\\ThemeQuickSettings.tsx":"21","E:\\Document\\Coding\\SecScore\\src\\components\\TitleBar.tsx":"22","E:\\Document\\Coding\\SecScore\\src\\components\\Versions.tsx":"23","E:\\Document\\Coding\\SecScore\\src\\components\\WindowControls.tsx":"24","E:\\Document\\Coding\\SecScore\\src\\components\\Wizard.tsx":"25","E:\\Document\\Coding\\SecScore\\src\\contexts\\ServiceContext.tsx":"26","E:\\Document\\Coding\\SecScore\\src\\contexts\\ThemeContext.tsx":"27","E:\\Document\\Coding\\SecScore\\src\\contexts\\ThemeEditorContext.tsx":"28","E:\\Document\\Coding\\SecScore\\src\\env.d.ts":"29","E:\\Document\\Coding\\SecScore\\src\\hooks\\useResponsive.ts":"30","E:\\Document\\Coding\\SecScore\\src\\i18n\\index.ts":"31","E:\\Document\\Coding\\SecScore\\src\\main.tsx":"32","E:\\Document\\Coding\\SecScore\\src\\preload\\types.ts":"33","E:\\Document\\Coding\\SecScore\\src\\react-19-patch.ts":"34","E:\\Document\\Coding\\SecScore\\src\\services\\StudentService.ts":"35","E:\\Document\\Coding\\SecScore\\src\\shared\\kernel.ts":"36","E:\\Document\\Coding\\SecScore\\src\\shared\\mobileNavigation.ts":"37","E:\\Document\\Coding\\SecScore\\src\\utils\\color.ts":"38","E:\\Document\\Coding\\SecScore\\src\\utils\\studentAvatar.ts":"39","E:\\Document\\Coding\\SecScore\\src\\workers\\xlsxWorker.ts":"40","E:\\Document\\Coding\\SecScore\\vite.config.ts":"41","E:\\Document\\Coding\\SecScore\\src\\components\\AutoScore\\ActionEditor.tsx":"42","E:\\Document\\Coding\\SecScore\\src\\components\\AutoScore\\AutoScoreUtils.ts":"43","E:\\Document\\Coding\\SecScore\\src\\components\\AutoScore\\IntervalValueCodec.ts":"44","E:\\Document\\Coding\\SecScore\\src\\components\\AutoScore\\IntervalValueWidget.tsx":"45","E:\\Document\\Coding\\SecScore\\src\\components\\AutoScore\\TriggerRuleBuilder.tsx":"46","E:\\Document\\Coding\\SecScore\\src\\components\\AutoScoreManager.tsx":"47","E:\\Document\\Coding\\SecScore\\src\\components\\OAuth\\OAuthCallback.tsx":"48","E:\\Document\\Coding\\SecScore\\src\\components\\OAuth\\OAuthLogin.tsx":"49","E:\\Document\\Coding\\SecScore\\src\\components\\SectlCloudStorageManager.tsx":"50","E:\\Document\\Coding\\SecScore\\src\\components\\SectlLoginButton.tsx":"51","E:\\Document\\Coding\\SecScore\\src\\components\\SectlSettingsPanel.tsx":"52","E:\\Document\\Coding\\SecScore\\src\\contexts\\SectlContext.tsx":"53","E:\\Document\\Coding\\SecScore\\src\\services\\sectl.ts":"54","E:\\Document\\Coding\\SecScore\\src\\services\\sectlAuth.ts":"55","E:\\Document\\Coding\\SecScore\\src\\services\\sectlCloudStorage.ts":"56","E:\\Document\\Coding\\SecScore\\src\\services\\sectlKVStorage.ts":"57","E:\\Document\\Coding\\SecScore\\src\\services\\sectlNotification.ts":"58","E:\\Document\\Coding\\SecScore\\src\\views\\SectlCloudView.tsx":"59","E:\\Document\\Coding\\SecScore\\src\\components\\ScoreSyncPanel.tsx":"60","E:\\Document\\Coding\\SecScore\\src\\services\\scoreSyncService.ts":"61","E:\\Document\\Coding\\SecScore\\src\\shared\\fontFamily.ts":"62","E:\\Document\\Coding\\SecScore\\src\\components\\PluginManager.tsx":"63","E:\\Document\\Coding\\SecScore\\src\\components\\SectlKVStorageManager.tsx":"64","E:\\Document\\Coding\\SecScore\\src\\hooks\\useOAuthPersist.ts":"65","E:\\Document\\Coding\\SecScore\\src\\plugins\\runtime.ts":"66","E:\\Document\\Coding\\SecScore\\src\\components\\SectlCloudSyncPanel.tsx":"67","E:\\Document\\Coding\\SecScore\\src\\services\\sectlCloudSync.ts":"68"},{"size":1413,"mtime":1774084765386,"results":"69","hashOfConfig":"70"},{"size":43176,"mtime":1777704342104,"results":"71","hashOfConfig":"72"},{"size":124,"mtime":1777702123958,"results":"73","hashOfConfig":"72"},{"size":52571,"mtime":1777702308145,"results":"74","hashOfConfig":"72"},{"size":23220,"mtime":1777705100125,"results":"75","hashOfConfig":"72"},{"size":2044,"mtime":1777702308187,"results":"76","hashOfConfig":"72"},{"size":119865,"mtime":1777702308216,"results":"77","hashOfConfig":"72"},{"size":7976,"mtime":1777702308224,"results":"78","hashOfConfig":"72"},{"size":36271,"mtime":1777702308330,"results":"79","hashOfConfig":"72"},{"size":4203,"mtime":1777702308347,"results":"80","hashOfConfig":"72"},{"size":5983,"mtime":1777702308352,"results":"81","hashOfConfig":"72"},{"size":11972,"mtime":1777702308355,"results":"82","hashOfConfig":"72"},{"size":6151,"mtime":1777702308363,"results":"83","hashOfConfig":"72"},{"size":13165,"mtime":1777702308385,"results":"84","hashOfConfig":"72"},{"size":76475,"mtime":1777785687566,"results":"85","hashOfConfig":"72"},{"size":5950,"mtime":1777702308438,"results":"86","hashOfConfig":"72"},{"size":10638,"mtime":1777702123994,"results":"87","hashOfConfig":"72"},{"size":80155,"mtime":1777702308449,"results":"88","hashOfConfig":"72"},{"size":6331,"mtime":1777702308453,"results":"89","hashOfConfig":"72"},{"size":8079,"mtime":1777702308456,"results":"90","hashOfConfig":"72"},{"size":10954,"mtime":1777702308460,"results":"91","hashOfConfig":"72"},{"size":3858,"mtime":1777702308471,"results":"92","hashOfConfig":"72"},{"size":431,"mtime":1777702308475,"results":"93","hashOfConfig":"72"},{"size":2367,"mtime":1777702308481,"results":"94","hashOfConfig":"72"},{"size":1560,"mtime":1777702308484,"results":"95","hashOfConfig":"72"},{"size":362,"mtime":1777702308492,"results":"96","hashOfConfig":"72"},{"size":5985,"mtime":1777702308505,"results":"97","hashOfConfig":"72"},{"size":2824,"mtime":1777702308512,"results":"98","hashOfConfig":"72"},{"size":38,"mtime":1777702308522,"results":"99","hashOfConfig":"72"},{"size":2097,"mtime":1777702308538,"results":"100","hashOfConfig":"72"},{"size":2771,"mtime":1777702308548,"results":"101","hashOfConfig":"72"},{"size":6919,"mtime":1777702124001,"results":"102","hashOfConfig":"72"},{"size":27739,"mtime":1777794839138,"results":"103","hashOfConfig":"72"},{"size":1150,"mtime":1777702308667,"results":"104","hashOfConfig":"72"},{"size":667,"mtime":1777702308678,"results":"105","hashOfConfig":"72"},{"size":3061,"mtime":1777702308733,"results":"106","hashOfConfig":"72"},{"size":1909,"mtime":1777702308763,"results":"107","hashOfConfig":"72"},{"size":3256,"mtime":1777702308776,"results":"108","hashOfConfig":"72"},{"size":1115,"mtime":1777702308782,"results":"109","hashOfConfig":"72"},{"size":947,"mtime":1777702308795,"results":"110","hashOfConfig":"72"},{"size":709,"mtime":1777702124174,"results":"111","hashOfConfig":"72"},{"size":6870,"mtime":1777702307924,"results":"112","hashOfConfig":"72"},{"size":20459,"mtime":1777705095081,"results":"113","hashOfConfig":"72"},{"size":3739,"mtime":1777702307930,"results":"114","hashOfConfig":"72"},{"size":3033,"mtime":1777702307946,"results":"115","hashOfConfig":"72"},{"size":1749,"mtime":1777702307956,"results":"116","hashOfConfig":"72"},{"size":31549,"mtime":1777702307991,"results":"117","hashOfConfig":"72"},{"size":1047,"mtime":1777702308232,"results":"118","hashOfConfig":"72"},{"size":10072,"mtime":1777794803195,"results":"119","hashOfConfig":"72"},{"size":8410,"mtime":1777702308404,"results":"120","hashOfConfig":"72"},{"size":1661,"mtime":1777702308420,"results":"121","hashOfConfig":"72"},{"size":5070,"mtime":1777702308429,"results":"122","hashOfConfig":"72"},{"size":3773,"mtime":1777702308490,"results":"123","hashOfConfig":"72"},{"size":683,"mtime":1777702308692,"results":"124","hashOfConfig":"72"},{"size":9500,"mtime":1777702308704,"results":"125","hashOfConfig":"72"},{"size":14984,"mtime":1777702308710,"results":"126","hashOfConfig":"72"},{"size":3980,"mtime":1777707095408,"results":"127","hashOfConfig":"72"},{"size":5437,"mtime":1777702308720,"results":"128","hashOfConfig":"72"},{"size":429,"mtime":1777702308789,"results":"129","hashOfConfig":"72"},{"size":8415,"mtime":1777702308392,"results":"130","hashOfConfig":"72"},{"size":7065,"mtime":1777706944006,"results":"131","hashOfConfig":"72"},{"size":1359,"mtime":1777702308724,"results":"132","hashOfConfig":"72"},{"size":9135,"mtime":1777702123981,"results":"133","hashOfConfig":"72"},{"size":12119,"mtime":1777702308414,"results":"134","hashOfConfig":"72"},{"size":3533,"mtime":1777702308531,"results":"135","hashOfConfig":"72"},{"size":6871,"mtime":1777702308658,"results":"136","hashOfConfig":"72"},{"size":12679,"mtime":1777705108862,"results":"137","hashOfConfig":"72"},{"size":14288,"mtime":1777705131157,"results":"138","hashOfConfig":"72"},{"filePath":"139","messages":"140","suppressedMessages":"141","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"oyxevq",{"filePath":"142","messages":"143","suppressedMessages":"144","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"f6a32r",{"filePath":"145","messages":"146","suppressedMessages":"147","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"148","messages":"149","suppressedMessages":"150","errorCount":0,"fatalErrorCount":0,"warningCount":2,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},{"filePath":"151","messages":"152","suppressedMessages":"153","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"154","messages":"155","suppressedMessages":"156","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"157","messages":"158","suppressedMessages":"159","errorCount":0,"fatalErrorCount":0,"warningCount":3,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},{"filePath":"160","messages":"161","suppressedMessages":"162","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"163","messages":"164","suppressedMessages":"165","errorCount":0,"fatalErrorCount":0,"warningCount":2,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},{"filePath":"166","messages":"167","suppressedMessages":"168","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"169","messages":"170","suppressedMessages":"171","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"172","messages":"173","suppressedMessages":"174","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"175","messages":"176","suppressedMessages":"177","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"178","messages":"179","suppressedMessages":"180","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"181","messages":"182","suppressedMessages":"183","errorCount":0,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},{"filePath":"184","messages":"185","suppressedMessages":"186","errorCount":0,"fatalErrorCount":0,"warningCount":2,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},{"filePath":"187","messages":"188","suppressedMessages":"189","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"190","messages":"191","suppressedMessages":"192","errorCount":0,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},{"filePath":"193","messages":"194","suppressedMessages":"195","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"196","messages":"197","suppressedMessages":"198","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"199","messages":"200","suppressedMessages":"201","errorCount":0,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},{"filePath":"202","messages":"203","suppressedMessages":"204","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"205","messages":"206","suppressedMessages":"207","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"208","messages":"209","suppressedMessages":"210","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"211","messages":"212","suppressedMessages":"213","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"214","messages":"215","suppressedMessages":"216","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"217","messages":"218","suppressedMessages":"219","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"220","messages":"221","suppressedMessages":"222","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"223","messages":"224","suppressedMessages":"225","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"226","messages":"227","suppressedMessages":"228","errorCount":0,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},{"filePath":"229","messages":"230","suppressedMessages":"231","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"232","messages":"233","suppressedMessages":"234","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"235","messages":"236","suppressedMessages":"237","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"238","messages":"239","suppressedMessages":"240","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"241","messages":"242","suppressedMessages":"243","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"244","messages":"245","suppressedMessages":"246","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"247","messages":"248","suppressedMessages":"249","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"250","messages":"251","suppressedMessages":"252","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"253","messages":"254","suppressedMessages":"255","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"256","messages":"257","suppressedMessages":"258","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"259","messages":"260","suppressedMessages":"261","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"262","messages":"263","suppressedMessages":"264","errorCount":0,"fatalErrorCount":0,"warningCount":2,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},{"filePath":"265","messages":"266","suppressedMessages":"267","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"268","messages":"269","suppressedMessages":"270","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"271","messages":"272","suppressedMessages":"273","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"274","messages":"275","suppressedMessages":"276","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"277","messages":"278","suppressedMessages":"279","errorCount":0,"fatalErrorCount":0,"warningCount":3,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},{"filePath":"280","messages":"281","suppressedMessages":"282","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"283","messages":"284","suppressedMessages":"285","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"286","messages":"287","suppressedMessages":"288","errorCount":0,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},{"filePath":"289","messages":"290","suppressedMessages":"291","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"292","messages":"293","suppressedMessages":"294","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"295","messages":"296","suppressedMessages":"297","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"298","messages":"299","suppressedMessages":"300","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"301","messages":"302","suppressedMessages":"303","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"304","messages":"305","suppressedMessages":"306","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"307","messages":"308","suppressedMessages":"309","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"310","messages":"311","suppressedMessages":"312","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"313","messages":"314","suppressedMessages":"315","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"316","messages":"317","suppressedMessages":"318","errorCount":0,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},{"filePath":"319","messages":"320","suppressedMessages":"321","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"322","messages":"323","suppressedMessages":"324","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"325","messages":"326","suppressedMessages":"327","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"328","messages":"329","suppressedMessages":"330","errorCount":0,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},{"filePath":"331","messages":"332","suppressedMessages":"333","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"334","messages":"335","suppressedMessages":"336","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"337","messages":"338","suppressedMessages":"339","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"340","messages":"341","suppressedMessages":"342","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"E:\\Document\\Coding\\SecScore\\eslint.config.mjs",[],[],"E:\\Document\\Coding\\SecScore\\src\\App.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\ClientContext.ts",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\BoardManager.tsx",["343","344"],[],"E:\\Document\\Coding\\SecScore\\src\\components\\ContentArea.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\EventHistory.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\Home.tsx",["345","346","347"],[],"E:\\Document\\Coding\\SecScore\\src\\components\\Leaderboard.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\OOBE\\OOBE.tsx",["348","349"],[],"E:\\Document\\Coding\\SecScore\\src\\components\\OOBE\\OOBEBackground.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\ReasonManager.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\RewardExchange.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\RewardSettings.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\ScoreManager.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\Settings.tsx",["350"],[],"E:\\Document\\Coding\\SecScore\\src\\components\\SettlementHistory.tsx",["351","352"],[],"E:\\Document\\Coding\\SecScore\\src\\components\\Sidebar.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\StudentManager.tsx",["353"],[],"E:\\Document\\Coding\\SecScore\\src\\components\\TagEditorDialog.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\ThemeEditor.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\ThemeQuickSettings.tsx",["354"],[],"E:\\Document\\Coding\\SecScore\\src\\components\\TitleBar.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\Versions.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\WindowControls.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\Wizard.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\contexts\\ServiceContext.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\contexts\\ThemeContext.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\contexts\\ThemeEditorContext.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\env.d.ts",[],[],"E:\\Document\\Coding\\SecScore\\src\\hooks\\useResponsive.ts",["355"],[],"E:\\Document\\Coding\\SecScore\\src\\i18n\\index.ts",[],[],"E:\\Document\\Coding\\SecScore\\src\\main.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\preload\\types.ts",[],[],"E:\\Document\\Coding\\SecScore\\src\\react-19-patch.ts",[],[],"E:\\Document\\Coding\\SecScore\\src\\services\\StudentService.ts",[],[],"E:\\Document\\Coding\\SecScore\\src\\shared\\kernel.ts",[],[],"E:\\Document\\Coding\\SecScore\\src\\shared\\mobileNavigation.ts",[],[],"E:\\Document\\Coding\\SecScore\\src\\utils\\color.ts",[],[],"E:\\Document\\Coding\\SecScore\\src\\utils\\studentAvatar.ts",[],[],"E:\\Document\\Coding\\SecScore\\src\\workers\\xlsxWorker.ts",[],[],"E:\\Document\\Coding\\SecScore\\vite.config.ts",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\AutoScore\\ActionEditor.tsx",["356","357"],[],"E:\\Document\\Coding\\SecScore\\src\\components\\AutoScore\\AutoScoreUtils.ts",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\AutoScore\\IntervalValueCodec.ts",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\AutoScore\\IntervalValueWidget.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\AutoScore\\TriggerRuleBuilder.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\AutoScoreManager.tsx",["358","359","360"],[],"E:\\Document\\Coding\\SecScore\\src\\components\\OAuth\\OAuthCallback.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\OAuth\\OAuthLogin.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\SectlCloudStorageManager.tsx",["361"],[],"E:\\Document\\Coding\\SecScore\\src\\components\\SectlLoginButton.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\SectlSettingsPanel.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\contexts\\SectlContext.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\services\\sectl.ts",[],[],"E:\\Document\\Coding\\SecScore\\src\\services\\sectlAuth.ts",[],[],"E:\\Document\\Coding\\SecScore\\src\\services\\sectlCloudStorage.ts",[],[],"E:\\Document\\Coding\\SecScore\\src\\services\\sectlKVStorage.ts",[],[],"E:\\Document\\Coding\\SecScore\\src\\services\\sectlNotification.ts",[],[],"E:\\Document\\Coding\\SecScore\\src\\views\\SectlCloudView.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\ScoreSyncPanel.tsx",["362"],[],"E:\\Document\\Coding\\SecScore\\src\\services\\scoreSyncService.ts",[],[],"E:\\Document\\Coding\\SecScore\\src\\shared\\fontFamily.ts",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\PluginManager.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\SectlKVStorageManager.tsx",["363"],[],"E:\\Document\\Coding\\SecScore\\src\\hooks\\useOAuthPersist.ts",[],[],"E:\\Document\\Coding\\SecScore\\src\\plugins\\runtime.ts",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\SectlCloudSyncPanel.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\services\\sectlCloudSync.ts",[],[],{"ruleId":"364","severity":1,"message":"365","line":635,"column":6,"nodeType":"366","endLine":635,"endColumn":44,"suggestions":"367"},{"ruleId":"364","severity":1,"message":"365","line":643,"column":6,"nodeType":"366","endLine":643,"endColumn":59,"suggestions":"368"},{"ruleId":"364","severity":1,"message":"369","line":556,"column":6,"nodeType":"366","endLine":556,"endColumn":76,"suggestions":"370"},{"ruleId":"364","severity":1,"message":"371","line":580,"column":6,"nodeType":"366","endLine":580,"endColumn":15,"suggestions":"372"},{"ruleId":"364","severity":1,"message":"373","line":2424,"column":6,"nodeType":"366","endLine":2424,"endColumn":8,"suggestions":"374"},{"ruleId":"364","severity":1,"message":"375","line":160,"column":6,"nodeType":"366","endLine":160,"endColumn":20,"suggestions":"376"},{"ruleId":"364","severity":1,"message":"377","line":334,"column":5,"nodeType":"366","endLine":334,"endColumn":27,"suggestions":"378"},{"ruleId":"364","severity":1,"message":"379","line":567,"column":6,"nodeType":"366","endLine":567,"endColumn":8,"suggestions":"380"},{"ruleId":"364","severity":1,"message":"381","line":54,"column":6,"nodeType":"366","endLine":54,"endColumn":18,"suggestions":"382"},{"ruleId":"364","severity":1,"message":"371","line":106,"column":5,"nodeType":"366","endLine":106,"endColumn":7,"suggestions":"383"},{"ruleId":"364","severity":1,"message":"384","line":1373,"column":6,"nodeType":"366","endLine":1373,"endColumn":50,"suggestions":"385"},{"ruleId":"364","severity":1,"message":"375","line":100,"column":6,"nodeType":"366","endLine":100,"endColumn":20,"suggestions":"386"},{"ruleId":"364","severity":1,"message":"387","line":25,"column":9,"nodeType":"388","endLine":25,"endColumn":70},{"ruleId":"364","severity":1,"message":"389","line":34,"column":9,"nodeType":"388","endLine":34,"endColumn":63},{"ruleId":"364","severity":1,"message":"390","line":34,"column":9,"nodeType":"388","endLine":34,"endColumn":63},{"ruleId":"364","severity":1,"message":"371","line":188,"column":5,"nodeType":"366","endLine":188,"endColumn":55,"suggestions":"391"},{"ruleId":"364","severity":1,"message":"392","line":365,"column":6,"nodeType":"366","endLine":365,"endColumn":15,"suggestions":"393"},{"ruleId":"364","severity":1,"message":"394","line":429,"column":6,"nodeType":"366","endLine":429,"endColumn":55,"suggestions":"395"},{"ruleId":"364","severity":1,"message":"396","line":79,"column":6,"nodeType":"366","endLine":79,"endColumn":23,"suggestions":"397"},{"ruleId":"364","severity":1,"message":"398","line":69,"column":6,"nodeType":"366","endLine":69,"endColumn":23,"suggestions":"399"},{"ruleId":"364","severity":1,"message":"400","line":99,"column":6,"nodeType":"366","endLine":99,"endColumn":31,"suggestions":"401"},"react-hooks/exhaustive-deps","React Hook useEffect has a missing dependency: 'activeBoard'. Either include it or remove the dependency array.","ArrayExpression",["402"],["403"],"React Hook useMemo has an unnecessary dependency: 'layoutType'. Either exclude it or remove the dependency array.",["404"],"React Hook useMemo has a missing dependency: 't'. Either include it or remove the dependency array.",["405"],"React Hook useEffect has missing dependencies: 'onGlobalMouseMove' and 'onGlobalMouseUp'. Either include them or remove the dependency array.",["406"],"React Hook useEffect has a missing dependency: 't'. Either include it or remove the dependency array.",["407"],"React Hook useCallback has missing dependencies: 'showOobeMessage' and 't'. Either include them or remove the dependency array.",["408"],"React Hook useEffect has a missing dependency: 'loadAll'. Either include it or remove the dependency array.",["409"],"React Hook useCallback has a missing dependency: 't'. Either include it or remove the dependency array.",["410"],["411"],"React Hook useMemo has missing dependencies: 'handleDelete', 'handleOpenAvatarEditor', 'handleOpenGroupEditor', and 'handleOpenTagEditor'. Either include them or remove the dependency array.",["412"],["413"],"The 'breakpoints' object makes the dependencies of useEffect Hook (at line 49) change on every render. Move it inside the useEffect callback. Alternatively, wrap the initialization of 'breakpoints' in its own useMemo() Hook.","VariableDeclarator","The 'safeValue' conditional could make the dependencies of useMemo Hook (at line 71) change on every render. To fix this, wrap the initialization of 'safeValue' in its own useMemo() Hook.","The 'safeValue' conditional could make the dependencies of useMemo Hook (at line 91) change on every render. To fix this, wrap the initialization of 'safeValue' in its own useMemo() Hook.",["414"],"React Hook useEffect has missing dependencies: 'fetchBatches', 'fetchRewards', 'fetchRules', 'fetchStudents', and 'fetchTags'. Either include them or remove the dependency array.",["415"],"React Hook useEffect has missing dependencies: 'fetchBatches' and 'fetchRules'. Either include them or remove the dependency array.",["416"],"React Hook useEffect has missing dependencies: 'loadFiles' and 'loadStorageUsage'. Either include them or remove the dependency array.",["417"],"React Hook useEffect has a missing dependency: 'checkCloudStatus'. Either include it or remove the dependency array.",["418"],"React Hook useEffect has a missing dependency: 'loadKVList'. Either include it or remove the dependency array.",["419"],{"desc":"420","fix":"421"},{"desc":"422","fix":"423"},{"desc":"424","fix":"425"},{"desc":"426","fix":"427"},{"desc":"428","fix":"429"},{"desc":"430","fix":"431"},{"desc":"432","fix":"433"},{"desc":"434","fix":"435"},{"desc":"436","fix":"437"},{"desc":"438","fix":"439"},{"desc":"440","fix":"441"},{"desc":"430","fix":"442"},{"desc":"443","fix":"444"},{"desc":"445","fix":"446"},{"desc":"447","fix":"448"},{"desc":"449","fix":"450"},{"desc":"451","fix":"452"},{"desc":"453","fix":"454"},"Update the dependencies array to be: [activeBoard, activeBoard.id, activeBoard.layout]",{"range":"455","text":"456"},"Update the dependencies array to be: [activeBoard, activeBoard.id, listConfigSignature, runAllInBoard]",{"range":"457","text":"458"},"Update the dependencies array to be: [sortedStudents, sortType, searchKeyword, getGroupName, t]",{"range":"459","text":"460"},"Update the dependencies array to be: [reasons, t]",{"range":"461","text":"462"},"Update the dependencies array to be: [onGlobalMouseMove, onGlobalMouseUp]",{"range":"463","text":"464"},"Update the dependencies array to be: [currentTheme, t]",{"range":"465","text":"466"},"Update the dependencies array to be: [students, showOobeMessage, t]",{"range":"467","text":"468"},"Update the dependencies array to be: [loadAll]",{"range":"469","text":"470"},"Update the dependencies array to be: [messageApi, t]",{"range":"471","text":"472"},"Update the dependencies array to be: [t]",{"range":"473","text":"474"},"Update the dependencies array to be: [t, isMobile, canEdit, handleOpenTagEditor, handleOpenGroupEditor, handleOpenAvatarEditor, handleDelete]",{"range":"475","text":"476"},{"range":"477","text":"466"},"Update the dependencies array to be: [t, tagOptions]",{"range":"478","text":"479"},"Update the dependencies array to be: [canEdit, fetchBatches, fetchRewards, fetchRules, fetchStudents, fetchTags]",{"range":"480","text":"481"},"Update the dependencies array to be: [backfillPrompted, canEdit, fetchBatches, fetchRules, messageApi, rules, t]",{"range":"482","text":"483"},"Update the dependencies array to be: [isAuthenticated, loadFiles, loadStorageUsage]",{"range":"484","text":"485"},"Update the dependencies array to be: [checkCloudStatus, isAuthenticated]",{"range":"486","text":"487"},"Update the dependencies array to be: [isAuthenticated, loadKVList, prefix]",{"range":"488","text":"489"},[18439,18477],"[activeBoard, activeBoard.id, activeBoard.layout]",[18687,18740],"[activeBoard, activeBoard.id, listConfigSignature, runAllInBoard]",[17285,17355],"[sortedStudents, sortType, searchKeyword, getGroupName, t]",[18130,18139],"[reasons, t]",[77920,77922],"[onGlobalMouseMove, onGlobalMouseUp]",[4674,4688],"[currentTheme, t]",[10389,10411],"[students, showOobeMessage, t]",[17999,18001],"[loadAll]",[1697,1709],"[messageApi, t]",[3343,3345],"[t]",[46442,46486],"[t, isMobile, canEdit, handleOpenTagEditor, handleOpenGroupEditor, handleOpenAvatarEditor, handleDelete]",[3185,3199],[4755,4805],"[t, tagOptions]",[10028,10037],"[canEdit, fetchBatches, fetchRewards, fetchRules, fetchStudents, fetchTags]",[12200,12249],"[backfillPrompted, canEdit, fetchBatches, fetchRules, messageApi, rules, t]",[1826,1843],"[isAuthenticated, loadFiles, loadStorageUsage]",[1783,1800],"[checkCloudStatus, isAuthenticated]",[2216,2241],"[isAuthenticated, loadKVList, prefix]"] \ No newline at end of file diff --git a/.trae/documents/url-registration-robustness.md b/.trae/documents/url-registration-robustness.md new file mode 100644 index 0000000..ad5d252 --- /dev/null +++ b/.trae/documents/url-registration-robustness.md @@ -0,0 +1,238 @@ +# URL 协议注册健壮性增强计划 + +## 目标 + +为 SecScore 的 URL 协议注册功能实现完整的健壮性增强,包括:状态检测、注册验证、自动重试(最多7次)、按钮状态自动切换、取消注册、以及全流程日志记录。 + +--- + +## 涉及文件 + +| 文件 | 操作 | 说明 | +| -------------------------------- | ---- | ---------------------------------------------------------------------------------------------- | +| `src-tauri/src/commands/app.rs` | 修改 | 新增 `check_url_protocol_status`、`unregister_url_protocol` 命令;增强 `register_url_protocol` | +| `src-tauri/src/lib.rs` | 修改 | 注册新命令到 invoke_handler | +| `src/preload/types.ts` | 修改 | 新增 API 类型定义 | +| `src/components/Settings.tsx` | 修改 | 重写 URL 协议 UI,加入状态显示、重试逻辑、按钮切换 | +| `src/i18n/locales/*.json` (10个) | 修改 | 新增 i18n 翻译键 | + +--- + +## 步骤 1:Rust 后端 - 新增 `UrlProtocolStatus` 结构体和 `check_url_protocol_status` 命令 + +**文件**: `src-tauri/src/commands/app.rs` + +### 1.1 新增结构体 + +```rust +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UrlProtocolStatus { + pub registered: bool, + pub protocol: String, + pub platform: String, + pub details: String, +} +``` + +### 1.2 实现 `check_url_protocol_status` 命令 + +按平台实现 URL 协议状态检测: + +- **Windows**: 查询注册表 `HKCU\Software\Classes\secscore` 是否存在,检查 `URL Protocol` 值和 `shell\open\command` 中的路径是否指向当前 exe + +- **macOS**: + 1. 先尝试通过 `lsregister -dump` 查询 `secscore://` scheme 的注册状态 + 2. 检查 `~/Library/LaunchAgents/com.secscore.url-handler.plist` 是否存在(非 .app bundle 场景) + +- **Linux**: + 1. 检查 `~/.local/share/applications/secscore.desktop` 是否存在且包含 `x-scheme-handler/secscore` + 2. 通过 `xdg-settings get default-url-scheme-handler secscore` 验证是否为默认处理器 + +返回 `UrlProtocolStatus` 结构体,包含注册状态、当前平台、检测详情。 + +--- + +## 步骤 2:Rust 后端 - 新增 `unregister_url_protocol` 命令 + +**文件**: `src-tauri/src/commands/app.rs` + +按平台实现取消注册: + +- **Windows**: 删除注册表 `HKCU\Software\Classes\secscore` 键 (`reg delete "HKCU\Software\Classes\secscore" /f`) + +- **macOS**: + 1. 如果存在 .app bundle,通过 `lsregister -u` 取消注册 + 2. 删除 `~/Library/LaunchAgents/com.secscore.url-handler.plist` + +- **Linux**: + 1. 删除 `~/.local/share/applications/secscore.desktop` + 2. 运行 `update-desktop-database` + +--- + +## 步骤 3:Rust 后端 - 增强 `register_url_protocol` 命令 + +**文件**: `src-tauri/src/commands/app.rs` + +改造现有 `register_url_protocol`: + +1. 执行注册操作后,不再盲目返回 `registered: Some(true)` +2. 注册完成后,自动调用状态检测逻辑验证注册是否成功 +3. 返回实际检测到的注册状态(`registered` 字段反映真实结果) +4. 在每个关键步骤添加 `eprintln!` 日志输出(Rust 端日志) + +--- + +## 步骤 4:Rust 后端 - 注册新命令 + +**文件**: `src-tauri/src/lib.rs` + +在 `invoke_handler` 中添加: + +- `check_url_protocol_status` + +- `unregister_url_protocol` + +--- + +## 步骤 5:前端 API 类型定义 + +**文件**: `src/preload/types.ts` + +新增 API 方法: + +```typescript +checkUrlProtocolStatus: (): Promise<{ + success: boolean + data?: { + registered: boolean + protocol: string + platform: string + details: string + } + message?: string +}> => invoke("check_url_protocol_status"), + +unregisterUrlProtocol: (): Promise<{ + success: boolean + data?: { registered: boolean } + message?: string +}> => invoke("unregister_url_protocol"), +``` + +--- + +## 步骤 6:前端 Settings.tsx - 重写 URL 协议卡片 UI + +**文件**: `src/components/Settings.tsx` + +### 6.1 新增状态变量 + +```typescript +const [urlStatus, setUrlStatus] = useState<{ registered: boolean; details: string } | null>(null) +const [urlStatusLoading, setUrlStatusLoading] = useState(false) +const [urlOperationLogs, setUrlOperationLogs] = useState([]) +``` + +### 6.2 状态检测逻辑 + +- 组件加载 URL tab 时自动执行一次 `checkUrlProtocolStatus` + +- 在 UI 中显示当前注册状态(Tag: 已注册/未注册 + 详情文本) + +### 6.3 注册流程(含自动重试) + +``` +handleRegister(): + 1. 记录日志:"开始注册 URL 协议..." + 2. 调用 registerUrlProtocol() + 3. 记录日志:"注册命令已执行,开始验证..." + 4. 调用 checkUrlProtocolStatus() 验证 + 5. IF 注册成功: + - 记录日志:"注册验证成功" + - 更新状态显示 + - 完成 + 6. ELSE (注册失败): + - FOR retry = 1 TO 7: + - 记录日志:"注册失败,第 {retry}/7 次重试..." + - 延迟 retry * 1000 ms (递增延迟: 1s, 2s, 3s, ..., 7s) + - 调用 registerUrlProtocol() + - 调用 checkUrlProtocolStatus() 验证 + - IF 成功: + - 记录日志:"第 {retry} 次重试成功" + - 更新状态,退出循环 + - ELSE: + - 记录日志:"第 {retry} 次重试失败" + - IF 7次全部失败: + - 记录日志:"已达最大重试次数(7次),注册失败" + - 显示错误消息 +``` + +### 6.4 按钮状态自动切换 + +根据 `urlStatus.registered` 动态渲染: + +| 状态 | 按钮文本 | 按钮类型 | 点击行为 | +| ------ | --------------- | -------- | --------------------------------- | +| 未注册 | "注册 URL 协议" | primary | 执行注册(含重试) | +| 已注册 | "重新注册" | default | 执行注册(覆盖注册+验证) | +| 已注册 | "取消注册" | danger | 执行 unregisterUrlProtocol + 验证 | + +### 6.5 操作日志展示 + +在 URL 协议卡片底部增加一个可折叠的 `` 或简洁的 `` 区域,展示最近的操作日志(最多保留最新 20 条),使用 `fontSize: 12px, fontFamily: monospace` 样式。 + +--- + +## 步骤 7:i18n 翻译 + +**文件**: `src/i18n/locales/` 下所有 10 个语言文件 + +在 `settings.url` 下新增以下键: + +| 键 | zh-CN | en-US | +| ---------------------- | ----------------------------- | --------------------------------------------- | +| `checkStatus` | "检测状态" | "Check Status" | +| `reRegister` | "重新注册" | "Re-register" | +| `unregister` | "取消注册" | "Unregister" | +| `unregistered` | "URL 协议已取消注册" | "URL protocol unregistered" | +| `unregisterFailed` | "取消注册失败" | "Failed to unregister" | +| `statusRegistered` | "已注册" | "Registered" | +| `statusNotRegistered` | "未注册" | "Not Registered" | +| `statusCheckFailed` | "状态检测失败" | "Status check failed" | +| `retrying` | "重试中 ({current}/{max})" | "Retrying ({current}/{max})" | +| `retryFailed` | "已达最大重试次数,注册失败" | "Max retries reached, registration failed" | +| `retrySuccess` | "第 {n} 次重试成功" | "Retry #{n} succeeded" | +| `operationLog` | "操作日志" | "Operation Log" | +| `logRegisterStart` | "开始注册 URL 协议..." | "Starting URL protocol registration..." | +| `logRegisterVerify` | "注册命令已执行,正在验证..." | "Registration command executed, verifying..." | +| `logRegisterSuccess` | "注册验证成功" | "Registration verified successfully" | +| `logRegisterFailed` | "注册验证失败" | "Registration verification failed" | +| `logUnregisterStart` | "正在取消注册..." | "Unregistering..." | +| `logUnregisterSuccess` | "取消注册成功" | "Unregistered successfully" | +| `logUnregisterFailed` | "取消注册失败" | "Unregister failed" | +| `logMaxRetries` | "已达最大重试次数 (7次)" | "Max retries (7) reached" | + +其余 9 个语言文件同步翻译。 + +--- + +## 步骤 8:运行测试与格式化 + +1. 执行 `pnpm run typecheck`(如有)检查前端类型 +2. 执行 `cargo check` 检查 Rust 编译 +3. 执行格式化命令 +4. 运行测试套件 + +--- + +## 实现顺序 + +1. ✅ 步骤 1:Rust - `check_url_protocol_status` +2. ✅ 步骤 2:Rust - `unregister_url_protocol` +3. ✅ 步骤 3:Rust - 增强 `register_url_protocol` +4. ✅ 步骤 4:Rust - 注册新命令到 lib.rs +5. ✅ 步骤 5:前端 API 类型 +6. ✅ 步骤 6:前端 Settings.tsx UI +7. ✅ 步骤 7:i18n 翻译 +8. ✅ 步骤 8:测试与格式化 diff --git a/public/about-content.json b/public/about-content.json index ea40f2b..ff606a4 100644 --- a/public/about-content.json +++ b/public/about-content.json @@ -2,6 +2,6 @@ "title": "SecScore", "description": "教育积分管理软件", "version": "v1.0.0", - "content": "

SecScore

\r
\r
\r

\r
\"GitHub\r
\"Downloads\"\r
\"License\"\r
\"GitHub\r
\"GitHub\r
\"Electron\"\r
\"React\"\r
\"TypeScript\"\r
\"Vite\"\r

\r
\r
SecScore 是一款教育积分管理软件,基于 Electron + React + TypeScript 开发,用于管理学生名单、记录加/扣分、查看排行榜与结算历史,并提供权限保护与数据备份。\r
\r

主要功能

\r
\r
  • 学生管理
\r
- 添加/删除学生\r
- 通过 xlsx 批量导入名单(支持导入前预览与选择“姓名列”)\r
  • 积分管理
\r
- 选择学生并提交加分/扣分\r
- 支持“预设理由”一键填充理由与分值\r
- 支持撤销最近的积分记录(撤销后学生积分会回滚)\r
  • 理由管理
\r
- 维护“预设理由”(分类、理由内容、预设分值)\r
  • 排行榜
\r
- 支持按“今天 / 本周 / 本月”查看积分变化\r
- 支持导出排行榜为 XLSX\r
- 支持查看单个学生的操作记录(文本列表)\r
  • 结算与历史
\r
- “结算并重新开始”:把当前未结算记录归档为一个阶段,并将所有学生当前积分清零\r
- 在“结算历史”查看每个阶段的排行榜\r
  • 系统设置
\r
- 主题切换\r
- 日志查看/导出/清空\r
- 数据导入/导出(JSON)\r
- 密码保护(管理密码 / 积分密码)与找回字符串\r
\r

使用方法

\r
\r

1. 权限与解锁

\r
\r
  • 右上角会显示当前权限:管理权限 / 积分权限 / 只读
\r
  • 若设置了密码,可通过右上角“输入密码”进行解锁
\r
- 管理密码:全功能(学生管理、理由管理、结算、数据管理等)\r
- 积分密码:仅允许积分相关操作\r
  • 点击“锁定”可切回只读状态
\r
  • 无密码时默认视为管理权限
\r
\r

2. 学生管理(导入名单)

\r
\r
入口:左侧菜单 → 学生管理 → 导入名单\r
\r
  • 通过 xlsx 导入
\r
1. 选择一个 .xlsx 文件(默认读取第一个工作表)\r
2. 在预览表格里点击表头,选择“姓名列”\r
3. 点击“导入”\r
4. 导入完成后会提示“新增 / 跳过”数量\r
\r
建议:姓名列尽量只包含纯姓名文本;同名学生会被视为重复并跳过。\r
\r

3. 积分管理(加分/扣分)

\r
\r
入口:左侧菜单 → 积分管理\r
\r
1. 在“姓名”选择一个学生(支持搜索)\r
2. 选择“加分/扣分”,并输入分值\r
3. 在“理由内容”填写原因(可手动输入)\r
4. 点击“确认提交”\r
\r
快捷理由:\r
\r
  • 可在“快捷理由”下拉框中选择预设理由,一键填充理由内容/分值(优先尊重你当前是否已手动输入分值)
\r
\r
撤销最近记录:\r
\r
  • “最近记录”默认折叠,展开后可对记录点击“撤销”
\r
  • 撤销会回滚该条记录对学生积分的影响
\r
\r

4. 排行榜与导出

\r
\r
入口:左侧菜单 → 排行榜\r
\r
  • 可切换统计范围:今天 / 本周 / 本月
\r
  • 点击“导出 XLSX”可导出当前排行榜
\r
  • 点击某个学生的“查看”可打开该学生的操作记录
\r
\r

5. 结算与数据备份

\r
\r
入口:左侧菜单 → 系统设置 → 数据管理\r
\r
  • 结算并重新开始
\r
- 会把当前未结算的积分记录归档为一个阶段\r
- 会将所有学生当前积分清零\r
- 学生名单不变;结算后的历史在“结算历史”查看\r
  • 导出 JSON(强烈建议定期备份)
\r
- 导入会覆盖现有学生/理由/积分记录/设置\r
- 安全相关设置(密码等)不会随导入写入\r
\r

开发与运行(面向贡献者)

\r
\r

MCP 文档

\r
\r
  • [MCP-使用说明](./MCP-使用说明.md)
\r
\r

环境要求

\r
\r
  • Node.js(建议使用 LTS 版本)
\r
  • pnpm
\r
\r

安装依赖

\r
\r
``bash\r
pnpm install\r
`\r
\r

开发模式

\r
\r
`bash\r
pnpm dev\r
`\r
\r

构建

\r
\r
`bash\r
pnpm build\r
`\r
\r
(可选)打包:\r
\r
`bash\r
pnpm build:win\r
pnpm build:unpack\r
`\r
\r

常用检查

\r
\r
`bash\r
pnpm lint\r
pnpm typecheck\r
``\r
", - "rawMarkdown": "# SecScore\r\n\r\n\r\n

\r\n \"GitHub\r\n \"Downloads\"\r\n \"License\"\r\n \"GitHub\r\n \"GitHub\r\n \"Electron\"\r\n \"React\"\r\n \"TypeScript\"\r\n \"Vite\"\r\n

\r\n\r\nSecScore 是一款教育积分管理软件,基于 Electron + React + TypeScript 开发,用于管理学生名单、记录加/扣分、查看排行榜与结算历史,并提供权限保护与数据备份。\r\n\r\n## 主要功能\r\n\r\n- 学生管理\r\n - 添加/删除学生\r\n - 通过 xlsx 批量导入名单(支持导入前预览与选择“姓名列”)\r\n- 积分管理\r\n - 选择学生并提交加分/扣分\r\n - 支持“预设理由”一键填充理由与分值\r\n - 支持撤销最近的积分记录(撤销后学生积分会回滚)\r\n- 理由管理\r\n - 维护“预设理由”(分类、理由内容、预设分值)\r\n- 排行榜\r\n - 支持按“今天 / 本周 / 本月”查看积分变化\r\n - 支持导出排行榜为 XLSX\r\n - 支持查看单个学生的操作记录(文本列表)\r\n- 结算与历史\r\n - “结算并重新开始”:把当前未结算记录归档为一个阶段,并将所有学生当前积分清零\r\n - 在“结算历史”查看每个阶段的排行榜\r\n- 系统设置\r\n - 主题切换\r\n - 日志查看/导出/清空\r\n - 数据导入/导出(JSON)\r\n - 密码保护(管理密码 / 积分密码)与找回字符串\r\n\r\n## 使用方法\r\n\r\n### 1. 权限与解锁\r\n\r\n- 右上角会显示当前权限:管理权限 / 积分权限 / 只读\r\n- 若设置了密码,可通过右上角“输入密码”进行解锁\r\n - 管理密码:全功能(学生管理、理由管理、结算、数据管理等)\r\n - 积分密码:仅允许积分相关操作\r\n- 点击“锁定”可切回只读状态\r\n- 无密码时默认视为管理权限\r\n\r\n### 2. 学生管理(导入名单)\r\n\r\n入口:左侧菜单 → 学生管理 → 导入名单\r\n\r\n- 通过 xlsx 导入\r\n 1. 选择一个 `.xlsx` 文件(默认读取第一个工作表)\r\n 2. 在预览表格里点击表头,选择“姓名列”\r\n 3. 点击“导入”\r\n 4. 导入完成后会提示“新增 / 跳过”数量\r\n\r\n建议:姓名列尽量只包含纯姓名文本;同名学生会被视为重复并跳过。\r\n\r\n### 3. 积分管理(加分/扣分)\r\n\r\n入口:左侧菜单 → 积分管理\r\n\r\n1. 在“姓名”选择一个学生(支持搜索)\r\n2. 选择“加分/扣分”,并输入分值\r\n3. 在“理由内容”填写原因(可手动输入)\r\n4. 点击“确认提交”\r\n\r\n快捷理由:\r\n\r\n- 可在“快捷理由”下拉框中选择预设理由,一键填充理由内容/分值(优先尊重你当前是否已手动输入分值)\r\n\r\n撤销最近记录:\r\n\r\n- “最近记录”默认折叠,展开后可对记录点击“撤销”\r\n- 撤销会回滚该条记录对学生积分的影响\r\n\r\n### 4. 排行榜与导出\r\n\r\n入口:左侧菜单 → 排行榜\r\n\r\n- 可切换统计范围:今天 / 本周 / 本月\r\n- 点击“导出 XLSX”可导出当前排行榜\r\n- 点击某个学生的“查看”可打开该学生的操作记录\r\n\r\n### 5. 结算与数据备份\r\n\r\n入口:左侧菜单 → 系统设置 → 数据管理\r\n\r\n- 结算并重新开始\r\n - 会把当前未结算的积分记录归档为一个阶段\r\n - 会将所有学生当前积分清零\r\n - 学生名单不变;结算后的历史在“结算历史”查看\r\n- 导出 JSON(强烈建议定期备份)\r\n - 导入会覆盖现有学生/理由/积分记录/设置\r\n - 安全相关设置(密码等)不会随导入写入\r\n\r\n## 开发与运行(面向贡献者)\r\n\r\n## MCP 文档\r\n\r\n- [MCP-使用说明](./MCP-使用说明.md)\r\n\r\n### 环境要求\r\n\r\n- Node.js(建议使用 LTS 版本)\r\n- pnpm\r\n\r\n### 安装依赖\r\n\r\n```bash\r\npnpm install\r\n```\r\n\r\n### 开发模式\r\n\r\n```bash\r\npnpm dev\r\n```\r\n\r\n### 构建\r\n\r\n```bash\r\npnpm build\r\n```\r\n\r\n(可选)打包:\r\n\r\n```bash\r\npnpm build:win\r\npnpm build:unpack\r\n```\r\n\r\n### 常用检查\r\n\r\n```bash\r\npnpm lint\r\npnpm typecheck\r\n```\r\n" -} + "content": "

SecScore





\"GitHub
\"Downloads\"
\"License\"
\"GitHub
\"GitHub
\"Electron\"
\"React\"
\"TypeScript\"
\"Vite\"



SecScore 是一款教育积分管理软件,基于 Electron + React + TypeScript 开发,用于管理学生名单、记录加/扣分、查看排行榜与结算历史,并提供权限保护与数据备份。

主要功能



  • 学生管理

- 添加/删除学生
- 通过 xlsx 批量导入名单(支持导入前预览与选择“姓名列”)
  • 积分管理

- 选择学生并提交加分/扣分
- 支持“预设理由”一键填充理由与分值
- 支持撤销最近的积分记录(撤销后学生积分会回滚)
  • 理由管理

- 维护“预设理由”(分类、理由内容、预设分值)
  • 排行榜

- 支持按“今天 / 本周 / 本月”查看积分变化
- 支持导出排行榜为 XLSX
- 支持查看单个学生的操作记录(文本列表)
  • 结算与历史

- “结算并重新开始”:把当前未结算记录归档为一个阶段,并将所有学生当前积分清零
- 在“结算历史”查看每个阶段的排行榜
  • 系统设置

- 主题切换
- 日志查看/导出/清空
- 数据导入/导出(JSON)
- 密码保护(管理密码 / 积分密码)与找回字符串

使用方法



1. 权限与解锁



  • 右上角会显示当前权限:管理权限 / 积分权限 / 只读

  • 若设置了密码,可通过右上角“输入密码”进行解锁

- 管理密码:全功能(学生管理、理由管理、结算、数据管理等)
- 积分密码:仅允许积分相关操作
  • 点击“锁定”可切回只读状态

  • 无密码时默认视为管理权限


2. 学生管理(导入名单)



入口:左侧菜单 → 学生管理 → 导入名单

  • 通过 xlsx 导入

1. 选择一个 .xlsx 文件(默认读取第一个工作表)
2. 在预览表格里点击表头,选择“姓名列”
3. 点击“导入”
4. 导入完成后会提示“新增 / 跳过”数量

建议:姓名列尽量只包含纯姓名文本;同名学生会被视为重复并跳过。

3. 积分管理(加分/扣分)



入口:左侧菜单 → 积分管理

1. 在“姓名”选择一个学生(支持搜索)
2. 选择“加分/扣分”,并输入分值
3. 在“理由内容”填写原因(可手动输入)
4. 点击“确认提交”

快捷理由:

  • 可在“快捷理由”下拉框中选择预设理由,一键填充理由内容/分值(优先尊重你当前是否已手动输入分值)


撤销最近记录:

  • “最近记录”默认折叠,展开后可对记录点击“撤销”

  • 撤销会回滚该条记录对学生积分的影响


4. 排行榜与导出



入口:左侧菜单 → 排行榜

  • 可切换统计范围:今天 / 本周 / 本月

  • 点击“导出 XLSX”可导出当前排行榜

  • 点击某个学生的“查看”可打开该学生的操作记录


5. 结算与数据备份



入口:左侧菜单 → 系统设置 → 数据管理

  • 结算并重新开始

- 会把当前未结算的积分记录归档为一个阶段
- 会将所有学生当前积分清零
- 学生名单不变;结算后的历史在“结算历史”查看
  • 导出 JSON(强烈建议定期备份)

- 导入会覆盖现有学生/理由/积分记录/设置
- 安全相关设置(密码等)不会随导入写入

开发与运行(面向贡献者)



MCP 文档



  • [MCP-使用说明](./MCP-使用说明.md)


环境要求



  • Node.js(建议使用 LTS 版本)

  • pnpm


安装依赖



``bash
pnpm install
`

开发模式



`bash
pnpm dev
`

构建



`bash
pnpm build
`

(可选)打包:

`bash
pnpm build:win
pnpm build:unpack
`

常用检查



`bash
pnpm lint
pnpm typecheck
``
", + "rawMarkdown": "# SecScore\n\n\n

\n \"GitHub\n \"Downloads\"\n \"License\"\n \"GitHub\n \"GitHub\n \"Electron\"\n \"React\"\n \"TypeScript\"\n \"Vite\"\n

\n\nSecScore 是一款教育积分管理软件,基于 Electron + React + TypeScript 开发,用于管理学生名单、记录加/扣分、查看排行榜与结算历史,并提供权限保护与数据备份。\n\n## 主要功能\n\n- 学生管理\n - 添加/删除学生\n - 通过 xlsx 批量导入名单(支持导入前预览与选择“姓名列”)\n- 积分管理\n - 选择学生并提交加分/扣分\n - 支持“预设理由”一键填充理由与分值\n - 支持撤销最近的积分记录(撤销后学生积分会回滚)\n- 理由管理\n - 维护“预设理由”(分类、理由内容、预设分值)\n- 排行榜\n - 支持按“今天 / 本周 / 本月”查看积分变化\n - 支持导出排行榜为 XLSX\n - 支持查看单个学生的操作记录(文本列表)\n- 结算与历史\n - “结算并重新开始”:把当前未结算记录归档为一个阶段,并将所有学生当前积分清零\n - 在“结算历史”查看每个阶段的排行榜\n- 系统设置\n - 主题切换\n - 日志查看/导出/清空\n - 数据导入/导出(JSON)\n - 密码保护(管理密码 / 积分密码)与找回字符串\n\n## 使用方法\n\n### 1. 权限与解锁\n\n- 右上角会显示当前权限:管理权限 / 积分权限 / 只读\n- 若设置了密码,可通过右上角“输入密码”进行解锁\n - 管理密码:全功能(学生管理、理由管理、结算、数据管理等)\n - 积分密码:仅允许积分相关操作\n- 点击“锁定”可切回只读状态\n- 无密码时默认视为管理权限\n\n### 2. 学生管理(导入名单)\n\n入口:左侧菜单 → 学生管理 → 导入名单\n\n- 通过 xlsx 导入\n 1. 选择一个 `.xlsx` 文件(默认读取第一个工作表)\n 2. 在预览表格里点击表头,选择“姓名列”\n 3. 点击“导入”\n 4. 导入完成后会提示“新增 / 跳过”数量\n\n建议:姓名列尽量只包含纯姓名文本;同名学生会被视为重复并跳过。\n\n### 3. 积分管理(加分/扣分)\n\n入口:左侧菜单 → 积分管理\n\n1. 在“姓名”选择一个学生(支持搜索)\n2. 选择“加分/扣分”,并输入分值\n3. 在“理由内容”填写原因(可手动输入)\n4. 点击“确认提交”\n\n快捷理由:\n\n- 可在“快捷理由”下拉框中选择预设理由,一键填充理由内容/分值(优先尊重你当前是否已手动输入分值)\n\n撤销最近记录:\n\n- “最近记录”默认折叠,展开后可对记录点击“撤销”\n- 撤销会回滚该条记录对学生积分的影响\n\n### 4. 排行榜与导出\n\n入口:左侧菜单 → 排行榜\n\n- 可切换统计范围:今天 / 本周 / 本月\n- 点击“导出 XLSX”可导出当前排行榜\n- 点击某个学生的“查看”可打开该学生的操作记录\n\n### 5. 结算与数据备份\n\n入口:左侧菜单 → 系统设置 → 数据管理\n\n- 结算并重新开始\n - 会把当前未结算的积分记录归档为一个阶段\n - 会将所有学生当前积分清零\n - 学生名单不变;结算后的历史在“结算历史”查看\n- 导出 JSON(强烈建议定期备份)\n - 导入会覆盖现有学生/理由/积分记录/设置\n - 安全相关设置(密码等)不会随导入写入\n\n## 开发与运行(面向贡献者)\n\n## MCP 文档\n\n- [MCP-使用说明](./MCP-使用说明.md)\n\n### 环境要求\n\n- Node.js(建议使用 LTS 版本)\n- pnpm\n\n### 安装依赖\n\n```bash\npnpm install\n```\n\n### 开发模式\n\n```bash\npnpm dev\n```\n\n### 构建\n\n```bash\npnpm build\n```\n\n(可选)打包:\n\n```bash\npnpm build:win\npnpm build:unpack\n```\n\n### 常用检查\n\n```bash\npnpm lint\npnpm typecheck\n```\n" +} \ No newline at end of file diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 7b73cdf..de6b642 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -4221,6 +4221,7 @@ dependencies = [ "chrono", "dirs", "hex", + "libc", "once_cell", "parking_lot", "rand 0.8.5", @@ -4242,6 +4243,7 @@ dependencies = [ "tracing-subscriber", "urlencoding", "uuid", + "windows-sys 0.59.0", "winreg 0.52.0", ] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 4e6f8a0..8cb1dd1 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -44,6 +44,10 @@ urlencoding = "2.1" [target.'cfg(windows)'.dependencies] winreg = "0.52" +windows-sys = { version = "0.59", features = ["Win32_UI_Shell", "Win32_UI_WindowsAndMessaging"] } + +[target.'cfg(unix)'.dependencies] +libc = "0.2" [profile.release] panic = "abort" diff --git a/src-tauri/src/commands/app.rs b/src-tauri/src/commands/app.rs index 65311cd..57ae32c 100644 --- a/src-tauri/src/commands/app.rs +++ b/src-tauri/src/commands/app.rs @@ -13,13 +13,474 @@ pub struct RegisterUrlProtocolResult { pub registered: Option, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UrlProtocolStatus { + pub registered: bool, + pub protocol: String, + pub platform: String, + pub details: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ElevationStatus { + pub is_elevated: bool, + pub platform: String, + pub can_request_elevation: bool, +} + +#[tauri::command] +pub async fn check_url_protocol_status( + _state: tauri::State<'_, Arc>>, +) -> Result, String> { + let protocol = "secscore"; + eprintln!("[URL Protocol] Checking status for protocol: {}", protocol); + + #[cfg(target_os = "windows")] + { + use std::process::Command; + + let ps_check = format!( + r#" +$ErrorActionPreference = 'SilentlyContinue' +$key = Get-Item 'HKCU:\Software\Classes\{protocol}' +if (-not $key) {{ + Write-Output 'NOT_FOUND' + exit 0 +}} +$urlProtocol = Get-ItemProperty -Path 'HKCU:\Software\Classes\{protocol}' -Name 'URL Protocol' +$command = Get-ItemProperty -Path 'HKCU:\Software\Classes\{protocol}\shell\open\command' -Name '(Default)' +if ($urlProtocol -and $command) {{ + Write-Output 'REGISTERED' + Write-Output ("CMD:" + $command.'(Default)') +}} else {{ + Write-Output 'PARTIAL' + Write-Output ("URL_PROTOCOL:" + [string]($null -ne $urlProtocol)) + Write-Output ("COMMAND:" + [string]($null -ne $command)) +}} +"#, + protocol = protocol + ); + + let output = Command::new("powershell") + .args(["-NoProfile", "-NonInteractive", "-Command", &ps_check]) + .output(); + + let (registered, details) = match output { + Ok(o) => { + let stdout = String::from_utf8_lossy(&o.stdout).to_string(); + eprintln!("[URL Protocol] Windows PowerShell check output: {}", stdout.trim()); + + if stdout.contains("REGISTERED") { + (true, "URL protocol fully registered in Windows Registry".to_string()) + } else if stdout.contains("PARTIAL") { + (false, format!("Partial registration: {}", stdout.trim())) + } else { + (false, "Registry key HKCU\\Software\\Classes\\secscore not found".to_string()) + } + } + Err(e) => { + eprintln!("[URL Protocol] Windows PowerShell check error: {}", e); + (false, format!("Failed to check registry: {}", e)) + } + }; + + eprintln!("[URL Protocol] Windows check result: registered={}, {}", registered, details); + + Ok(IpcResponse::success(UrlProtocolStatus { + registered, + protocol: protocol.to_string(), + platform: "windows".to_string(), + details, + })) + } + + #[cfg(target_os = "macos")] + { + let mut registered = false; + let mut details = String::new(); + + let exe_path = std::env::current_exe().unwrap_or_default(); + let app_bundle = find_app_bundle(&exe_path); + + if app_bundle.is_some() { + let lsregister = "/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister"; + let output = std::process::Command::new(lsregister) + .args(["-dump"]) + .output(); + + match output { + Ok(o) => { + let stdout = String::from_utf8_lossy(&o.stdout); + if stdout.contains("secscore") { + registered = true; + details = "secscore:// registered via Launch Services (app bundle)".to_string(); + } else { + details = "secscore:// not found in Launch Services dump".to_string(); + } + } + Err(e) => { + details = format!("Failed to query Launch Services: {}", e); + } + } + } + + if !registered { + let home = dirs::home_dir().unwrap_or_default(); + let plist_path = home.join("Library/LaunchAgents/com.secscore.url-handler.plist"); + if plist_path.exists() { + registered = true; + details = "URL handler plist found at ~/Library/LaunchAgents".to_string(); + } else if details.is_empty() { + details = "No app bundle or plist found for URL protocol".to_string(); + } + } + + eprintln!("[URL Protocol] macOS check result: registered={}, {}", registered, details); + + Ok(IpcResponse::success(UrlProtocolStatus { + registered, + protocol: protocol.to_string(), + platform: "macos".to_string(), + details, + })) + } + + #[cfg(target_os = "linux")] + { + let home = dirs::home_dir().unwrap_or_default(); + let desktop_path = home.join(".local/share/applications/secscore.desktop"); + let mut registered = false; + let mut details = String::new(); + + if desktop_path.exists() { + match std::fs::read_to_string(&desktop_path) { + Ok(content) => { + if content.contains("x-scheme-handler/secscore") { + let xdg_output = std::process::Command::new("xdg-settings") + .args(["get", "default-url-scheme-handler", "secscore"]) + .output(); + + match xdg_output { + Ok(o) if o.status.success() => { + let handler = String::from_utf8_lossy(&o.stdout).trim().to_string(); + if handler == "secscore.desktop" { + registered = true; + details = "secscore.desktop is default URL scheme handler".to_string(); + } else { + registered = true; + details = format!( + "Desktop file exists with MimeType, but default handler is: {}", + handler + ); + } + } + _ => { + registered = true; + details = "Desktop file exists with MimeType (xdg-settings query failed)".to_string(); + } + } + } else { + details = "Desktop file exists but missing x-scheme-handler/secscore MimeType".to_string(); + } + } + Err(e) => { + details = format!("Failed to read desktop file: {}", e); + } + } + } else { + details = "secscore.desktop not found in ~/.local/share/applications".to_string(); + } + + eprintln!("[URL Protocol] Linux check result: registered={}, {}", registered, details); + + Ok(IpcResponse::success(UrlProtocolStatus { + registered, + protocol: protocol.to_string(), + platform: "linux".to_string(), + details, + })) + } + + #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))] + { + Ok(IpcResponse::success(UrlProtocolStatus { + registered: false, + protocol: "secscore".to_string(), + platform: "unknown".to_string(), + details: "URL protocol check not supported on this platform".to_string(), + })) + } +} + +#[tauri::command] +pub async fn unregister_url_protocol( + _state: tauri::State<'_, Arc>>, +) -> Result, String> { + let protocol = "secscore"; + eprintln!("[URL Protocol] Unregistering protocol: {}", protocol); + + #[cfg(target_os = "windows")] + { + use std::process::Command; + + let ps_script = format!( + r#" +$ErrorActionPreference = 'SilentlyContinue' +Remove-Item -Path 'HKCU:\Software\Classes\{protocol}' -Recurse -Force +if (-not (Test-Path 'HKCU:\Software\Classes\{protocol}')) {{ + Write-Output 'SUCCESS' +}} else {{ + Write-Output 'FAILED' +}} +"#, + protocol = protocol + ); + + let output = Command::new("powershell") + .args(["-NoProfile", "-NonInteractive", "-Command", &ps_script]) + .output(); + + let success = match output { + Ok(o) => { + let stdout = String::from_utf8_lossy(&o.stdout); + let ok = o.status.success() && stdout.contains("SUCCESS"); + eprintln!("[URL Protocol] Windows PowerShell unregister result: {}", ok); + ok + } + Err(e) => { + eprintln!("[URL Protocol] Windows PowerShell unregister error: {}", e); + false + } + }; + + return Ok(IpcResponse::success(RegisterUrlProtocolResult { + registered: Some(!success), + })); + } + + #[cfg(target_os = "macos")] + { + let exe_path = std::env::current_exe().unwrap_or_default(); + let mut unregistered = true; + + if let Some(bundle_path) = find_app_bundle(&exe_path) { + let lsregister = "/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister"; + let output = std::process::Command::new(lsregister) + .args(["-u", &bundle_path.to_string_lossy()]) + .output(); + + match output { + Ok(o) => { + eprintln!("[URL Protocol] macOS lsregister -u result: {}", o.status.success()); + } + Err(e) => { + eprintln!("[URL Protocol] macOS lsregister -u error: {}", e); + unregistered = false; + } + } + } + + let home = dirs::home_dir().unwrap_or_default(); + let plist_path = home.join("Library/LaunchAgents/com.secscore.url-handler.plist"); + if plist_path.exists() { + match std::fs::remove_file(&plist_path) { + Ok(_) => { + eprintln!("[URL Protocol] macOS plist removed: {:?}", plist_path); + } + Err(e) => { + eprintln!("[URL Protocol] macOS plist remove error: {}", e); + unregistered = false; + } + } + } + + return Ok(IpcResponse::success(RegisterUrlProtocolResult { + registered: Some(!unregistered), + })); + } + + #[cfg(target_os = "linux")] + { + use std::fs; + + let home = dirs::home_dir().unwrap_or_default(); + let desktop_path = home.join(".local/share/applications/secscore.desktop"); + let mut unregistered = true; + + if desktop_path.exists() { + match fs::remove_file(&desktop_path) { + Ok(_) => { + eprintln!("[URL Protocol] Linux desktop file removed: {:?}", desktop_path); + let apps_dir = home.join(".local/share/applications"); + let _ = std::process::Command::new("update-desktop-database") + .arg(apps_dir.to_string_lossy().to_string()) + .status(); + } + Err(e) => { + eprintln!("[URL Protocol] Linux desktop file remove error: {}", e); + unregistered = false; + } + } + } + + return Ok(IpcResponse::success(RegisterUrlProtocolResult { + registered: Some(!unregistered), + })); + } + + #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))] + { + Ok(IpcResponse::error( + "URL protocol unregistration is not supported on this platform", + )) + } +} + +#[tauri::command] +pub async fn check_elevation( + _state: tauri::State<'_, Arc>>, +) -> Result, String> { + #[cfg(target_os = "windows")] + { + let is_elevated = check_windows_elevation(); + return Ok(IpcResponse::success(ElevationStatus { + is_elevated, + platform: "windows".to_string(), + can_request_elevation: true, + })); + } + + #[cfg(target_os = "macos")] + { + let is_elevated = unsafe { libc::geteuid() == 0 }; + return Ok(IpcResponse::success(ElevationStatus { + is_elevated, + platform: "macos".to_string(), + can_request_elevation: true, + })); + } + + #[cfg(target_os = "linux")] + { + let is_elevated = unsafe { libc::geteuid() == 0 }; + return Ok(IpcResponse::success(ElevationStatus { + is_elevated, + platform: "linux".to_string(), + can_request_elevation: true, + })); + } + + #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))] + { + Ok(IpcResponse::success(ElevationStatus { + is_elevated: false, + platform: "unknown".to_string(), + can_request_elevation: false, + })) + } +} + +#[tauri::command] +pub async fn request_elevation( + app: AppHandle, + _state: tauri::State<'_, Arc>>, +) -> Result, String> { + #[cfg(target_os = "windows")] + { + let exe_path = + std::env::current_exe().map_err(|e| format!("Failed to get exe path: {}", e))?; + let exe_str = exe_path.to_string_lossy().to_string(); + + use std::ffi::OsStr; + use std::os::windows::ffi::OsStrExt; + use windows_sys::Win32::UI::Shell::ShellExecuteW; + use windows_sys::Win32::UI::WindowsAndMessaging::SW_SHOW; + + let verb: Vec = OsStr::new("runas\0").encode_wide().collect(); + let file: Vec = OsStr::new(&format!("{}\0", exe_str)).encode_wide().collect(); + let params: Vec = OsStr::new("\0").encode_wide().collect(); + + let result = unsafe { + ShellExecuteW( + 0 as _, + verb.as_ptr(), + file.as_ptr(), + params.as_ptr(), + std::ptr::null(), + SW_SHOW, + ) + }; + + if result as usize > 32 { + app.exit(0); + Ok(IpcResponse::success(())) + } else { + Err(format!( + "Failed to request elevation (ShellExecuteW returned {})", + result as usize + )) + } + } + + #[cfg(target_os = "macos")] + { + let exe_path = + std::env::current_exe().map_err(|e| format!("Failed to get exe path: {}", e))?; + let exe_str = exe_path.to_string_lossy().to_string(); + + let script = format!( + "do shell script \"\\\"{}\\\"\" with administrator privileges", + exe_str + ); + + let status = std::process::Command::new("osascript") + .args(["-e", &script]) + .status(); + + match status { + Ok(s) if s.success() => { + app.exit(0); + Ok(IpcResponse::success(())) + } + Ok(s) => Err(format!("Elevation cancelled or failed (exit code: {:?})", s.code())), + Err(e) => Err(format!("Failed to request elevation: {}", e)), + } + } + + #[cfg(target_os = "linux")] + { + let exe_path = + std::env::current_exe().map_err(|e| format!("Failed to get exe path: {}", e))?; + let exe_str = exe_path.to_string_lossy().to_string(); + + let status = std::process::Command::new("pkexec") + .arg(&exe_str) + .status(); + + match status { + Ok(s) if s.success() => { + app.exit(0); + Ok(IpcResponse::success(())) + } + Ok(s) => Err(format!("Elevation cancelled or failed (exit code: {:?})", s.code())), + Err(e) => Err(format!("Failed to request elevation: {}", e)), + } + } + + #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))] + { + let _ = app; + Err("Elevation is not supported on this platform".to_string()) + } +} + #[tauri::command] pub async fn register_url_protocol( app: AppHandle, _state: tauri::State<'_, Arc>>, ) -> Result, String> { - #[cfg(target_os = "windows")] let protocol = "secscore"; + eprintln!("[URL Protocol] Starting registration for protocol: {}", protocol); #[cfg(target_os = "windows")] { @@ -27,50 +488,222 @@ pub async fn register_url_protocol( let exe_path = std::env::current_exe().map_err(|e| format!("Failed to get executable path: {}", e))?; - let exe_path_str = exe_path.to_string_lossy(); - let reg_command = format!( - r#"reg add "HKCU\Software\Classes\{}" /ve /d "URL:SecScore Protocol" /f"#, - protocol + eprintln!("[URL Protocol] Windows: Exe path = {}", exe_path_str); + + let ps_script = format!( + r#" +$ErrorActionPreference = 'Stop' +try {{ + New-Item -Path 'HKCU:\Software\Classes\{protocol}' -Force | Out-Null + Set-ItemProperty -Path 'HKCU:\Software\Classes\{protocol}' -Name '(Default)' -Value 'URL:SecScore Protocol' -Force + Set-ItemProperty -Path 'HKCU:\Software\Classes\{protocol}' -Name 'URL Protocol' -Value '' -Force + New-Item -Path 'HKCU:\Software\Classes\{protocol}\DefaultIcon' -Force | Out-Null + Set-ItemProperty -Path 'HKCU:\Software\Classes\{protocol}\DefaultIcon' -Name '(Default)' -Value '{exe},1' -Force + New-Item -Path 'HKCU:\Software\Classes\{protocol}\shell' -Force | Out-Null + New-Item -Path 'HKCU:\Software\Classes\{protocol}\shell\open' -Force | Out-Null + New-Item -Path 'HKCU:\Software\Classes\{protocol}\shell\open\command' -Force | Out-Null + Set-ItemProperty -Path 'HKCU:\Software\Classes\{protocol}\shell\open\command' -Name '(Default)' -Value '"{exe}" "%1"' -Force + Write-Output 'SUCCESS' +}} catch {{ + Write-Error $_.Exception.Message + exit 1 +}} +"#, + protocol = protocol, + exe = exe_path_str ); - let reg_command2 = format!( - r#"reg add "HKCU\Software\Classes\{}\DefaultIcon" /ve /d "{},1" /f"#, - protocol, exe_path_str - ); + let output = Command::new("powershell") + .args(["-NoProfile", "-NonInteractive", "-Command", &ps_script]) + .output(); - let reg_command3 = format!( - r#"reg add "HKCU\Software\Classes\{}\shell\open\command" /ve /d "\"{}\" \"%%1\"" /f"#, - protocol, exe_path_str - ); + let mut registered = false; + match output { + Ok(o) => { + let stdout = String::from_utf8_lossy(&o.stdout); + let stderr = String::from_utf8_lossy(&o.stderr); + let success = o.status.success() && stdout.contains("SUCCESS"); + eprintln!("[URL Protocol] Windows PowerShell result: success={}, stdout={}, stderr={}", success, stdout.trim(), stderr.trim()); + if success { + let query_cmd = format!( + r#"reg query "HKCU\Software\Classes\{}\shell\open\command" /ve"#, + protocol + ); + let verify = Command::new("cmd").args(["/C", &query_cmd]).output(); + registered = verify.map(|v| v.status.success()).unwrap_or(false); + } + } + Err(e) => { + eprintln!("[URL Protocol] Windows PowerShell error: {}", e); + } + } - let _ = Command::new("cmd").args(["/C", ®_command]).output(); - - let _ = Command::new("cmd").args(["/C", ®_command2]).output(); - - let _ = Command::new("cmd").args(["/C", ®_command3]).output(); + eprintln!("[URL Protocol] Windows registration verification: {}", registered); let _ = app; - return Ok(IpcResponse::success(RegisterUrlProtocolResult { - registered: Some(true), + registered: Some(registered), })); } #[cfg(target_os = "macos")] { + use std::fs; + use std::process::Command; + + let exe_path = + std::env::current_exe().map_err(|e| format!("Failed to get executable path: {}", e))?; + + let app_bundle = find_app_bundle(&exe_path); + let mut registered = false; + + if let Some(bundle_path) = app_bundle { + eprintln!("[URL Protocol] macOS: Found app bundle at {:?}", bundle_path); + let output = Command::new( + "/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister", + ) + .args(["-f", &bundle_path.to_string_lossy()]) + .output(); + + match output { + Ok(o) if o.status.success() => { + eprintln!("[URL Protocol] macOS: lsregister -f succeeded"); + registered = true; + } + Ok(o) => { + eprintln!("[URL Protocol] macOS: lsregister -f failed (exit: {:?}), trying -R -f", o.status.code()); + let _ = Command::new("/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister") + .args(["-R", "-f", &bundle_path.to_string_lossy()]) + .output(); + registered = true; + } + Err(e) => { + eprintln!("[URL Protocol] macOS: lsregister error: {}", e); + } + } + } else { + eprintln!("[URL Protocol] macOS: No app bundle found, using plist fallback"); + let home = dirs::home_dir().unwrap_or_default(); + let plist_dir = home.join("Library/LaunchAgents"); + let _ = fs::create_dir_all(&plist_dir); + + let plist_path = plist_dir.join("com.secscore.url-handler.plist"); + let plist_content = format!( + r#" + + + + Label + com.secscore.url-handler + ProgramArguments + + {} + + Sockets + + secscore + + SockServiceName + secscore + + + +"#, + exe_path.to_string_lossy() + ); + + match fs::write(&plist_path, plist_content) { + Ok(_) => { + eprintln!("[URL Protocol] macOS: Plist written to {:?}", plist_path); + registered = plist_path.exists(); + } + Err(e) => { + eprintln!("[URL Protocol] macOS: Plist write error: {}", e); + } + } + } + let _ = app; return Ok(IpcResponse::success(RegisterUrlProtocolResult { - registered: Some(false), + registered: Some(registered), })); } #[cfg(target_os = "linux")] { + use std::fs; + + let exe_path = + std::env::current_exe().map_err(|e| format!("Failed to get executable path: {}", e))?; + let exe_str = exe_path.to_string_lossy(); + + let home = dirs::home_dir().unwrap_or_default(); + let apps_dir = home.join(".local/share/applications"); + let _ = fs::create_dir_all(&apps_dir); + + let desktop_path = apps_dir.join("secscore.desktop"); + let desktop_content = format!( + r#"[Desktop Entry] +Name=SecScore +Comment=Security Score Application +Exec={} %u +Icon=secscore +Type=Application +Terminal=false +Categories=Utility; +MimeType=x-scheme-handler/secscore; +"#, + exe_str + ); + + match fs::write(&desktop_path, &desktop_content) { + Ok(_) => { + eprintln!("[URL Protocol] Linux: Desktop file written to {:?}", desktop_path); + } + Err(e) => { + eprintln!("[URL Protocol] Linux: Desktop file write error: {}", e); + let _ = app; + return Ok(IpcResponse::success(RegisterUrlProtocolResult { + registered: Some(false), + })); + } + } + + let update_result = std::process::Command::new("update-desktop-database") + .arg(apps_dir.to_string_lossy().to_string()) + .status(); + eprintln!("[URL Protocol] Linux: update-desktop-database result: {:?}", update_result); + + let xdg_set_result = std::process::Command::new("xdg-settings") + .args([ + "set", + "default-url-scheme-handler", + "secscore", + "secscore.desktop", + ]) + .status(); + eprintln!("[URL Protocol] Linux: xdg-settings set result: {:?}", xdg_set_result); + + let xdg_mime_result = std::process::Command::new("xdg-mime") + .args([ + "default", + "secscore.desktop", + "x-scheme-handler/secscore", + ]) + .status(); + eprintln!("[URL Protocol] Linux: xdg-mime default result: {:?}", xdg_mime_result); + + let verify_exists = desktop_path.exists(); + let verify_content = fs::read_to_string(&desktop_path).unwrap_or_default(); + let actually_registered = verify_exists && verify_content.contains("x-scheme-handler/secscore"); + + eprintln!("[URL Protocol] Linux registration verification: {}", actually_registered); + let _ = app; return Ok(IpcResponse::success(RegisterUrlProtocolResult { - registered: Some(false), + registered: Some(actually_registered), })); } @@ -99,3 +732,31 @@ pub async fn app_restart( ) -> Result<(), String> { app.restart(); } + +#[cfg(target_os = "windows")] +fn check_windows_elevation() -> bool { + use std::process::Command; + + Command::new("net") + .args(["session"]) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) + .status() + .map(|s| s.success()) + .unwrap_or(false) +} + +#[cfg(target_os = "macos")] +fn find_app_bundle(exe_path: &std::path::Path) -> Option { + let mut current = exe_path.parent()?; + loop { + if current.extension().map_or(false, |e| e == "app") { + return Some(current.to_path_buf()); + } + if !current.parent().map_or(false, |p| p != current) { + break; + } + current = current.parent()?; + } + None +} diff --git a/src-tauri/src/commands/auth.rs b/src-tauri/src/commands/auth.rs index fddedb6..c00a1aa 100644 --- a/src-tauri/src/commands/auth.rs +++ b/src-tauri/src/commands/auth.rs @@ -108,7 +108,6 @@ pub struct SetPasswordsResponse { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct OAuthConfig { pub platform_id: String, - pub platform_secret: String, pub callback_url: String, } @@ -463,7 +462,6 @@ pub async fn oauth_get_authorization_url( pub async fn oauth_exchange_code( code: String, platform_id: String, - platform_secret: String, callback_url: String, code_verifier: String, state: State<'_, Arc>>, @@ -473,19 +471,16 @@ pub async fn oauth_exchange_code( code, platform_id, callback_url ); - // 获取设备 UUID 和 IP 地址 let device_uuid = get_or_create_device_uuid(); let ip_address = get_local_ip().await.unwrap_or_else(|_| "127.0.0.1".to_string()); - println!("[OAuth] 设备 UUID: {}, IP: {}", device_uuid, ip_address); - let state_guard = state.read(); let client = &state_guard.http_client; + let payload = serde_json::json!({ "grant_type": "authorization_code", "code": &code, "client_id": &platform_id, - "client_secret": &platform_secret, "redirect_uri": &callback_url, "code_verifier": &code_verifier, "device_uuid": &device_uuid, @@ -537,7 +532,6 @@ pub async fn oauth_revoke_token( token: String, token_type_hint: Option, platform_id: String, - platform_secret: String, state: State<'_, Arc>>, ) -> Result, String> { let state_guard = state.read(); @@ -546,7 +540,6 @@ pub async fn oauth_revoke_token( let mut payload = serde_json::json!({ "token": token, "client_id": platform_id, - "client_secret": platform_secret }); if let Some(hint) = token_type_hint { @@ -575,7 +568,6 @@ pub async fn oauth_revoke_token( pub async fn oauth_introspect_token( token: String, platform_id: String, - platform_secret: String, state: State<'_, Arc>>, ) -> Result, String> { let state_guard = state.read(); @@ -586,7 +578,6 @@ pub async fn oauth_introspect_token( .json(&serde_json::json!({ "token": token, "client_id": platform_id, - "client_secret": platform_secret })) .send() .await @@ -667,7 +658,6 @@ pub async fn oauth_get_user_info( pub async fn oauth_refresh_token( refresh_token: String, platform_id: String, - platform_secret: String, state: State<'_, Arc>>, ) -> Result, String> { let state_guard = state.read(); @@ -679,7 +669,6 @@ pub async fn oauth_refresh_token( "grant_type": "refresh_token", "refresh_token": refresh_token, "client_id": platform_id, - "client_secret": platform_secret })) .send() .await @@ -901,7 +890,6 @@ pub async fn oauth_clear_login_state() -> Result, String> { pub async fn oauth_refresh_access_token( refresh_token: String, platform_id: String, - platform_secret: String, state: State<'_, Arc>>, ) -> Result, String> { println!("[OAuth] 刷新 access_token"); @@ -916,7 +904,6 @@ pub async fn oauth_refresh_access_token( "grant_type": "refresh_token", "refresh_token": refresh_token, "client_id": platform_id, - "client_secret": platform_secret, "device_uuid": device_uuid, "ip_address": ip_address, }); diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index fd524a5..0856ff6 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -153,6 +153,10 @@ pub fn run() { mcp_server_stop, mcp_server_status, register_url_protocol, + check_url_protocol_status, + unregister_url_protocol, + check_elevation, + request_elevation, app_quit, app_restart, ]) diff --git a/src/App.tsx b/src/App.tsx index bb3d328..3bf223d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -36,6 +36,8 @@ import { ThemeProvider, useTheme } from "./contexts/ThemeContext" import { MOBILE_NAV_ITEMS, MobileNavKey, sanitizeMobileNavKeys } from "./shared/mobileNavigation" import { resolveStoredFontFamily } from "./shared/fontFamily" import { getPluginRuntime } from "./plugins/runtime" +import { sectlCloudSync } from "./services/sectlCloudSync" +import { sectlAuth } from "./services/sectlAuth" const DEFAULT_MOBILE_BOTTOM_NAV_ITEMS: MobileNavKey[] = MOBILE_NAV_ITEMS.map((item) => item.key) const DEFAULT_MOBILE_BOTTOM_PRIMARY_KEYS: MobileNavKey[] = DEFAULT_MOBILE_BOTTOM_NAV_ITEMS.slice( @@ -389,6 +391,27 @@ function MainContent(): React.JSX.Element { if (permission !== "admin") return try { syncCheckingRef.current = true + + const settingsRes = await api.getAllSettings() + const syncMethod = settingsRes?.success ? settingsRes.data?.sync_method : null + + if (syncMethod === "sectl_cloud") { + if (!sectlAuth.isAuthenticated()) return + const status = sectlCloudSync.getStatus() + if (!status.is_configured || status.is_syncing) return + + const now = Date.now() + const recentLocal = now - lastLocalMutationAtRef.current < 15000 + const direction = recentLocal ? "push" : "bidirectional" + const result = await sectlCloudSync.fullSync(direction) + if (result.success) { + window.dispatchEvent( + new CustomEvent("ss:data-updated", { detail: { category: "all", source: "sync" } }) + ) + } + return + } + const statusRes = await api.dbGetStatus() if ( !statusRes?.success || diff --git a/src/components/AutoScore/AutoScoreUtils.ts b/src/components/AutoScore/AutoScoreUtils.ts index 50e932b..fcc6e0c 100644 --- a/src/components/AutoScore/AutoScoreUtils.ts +++ b/src/components/AutoScore/AutoScoreUtils.ts @@ -630,9 +630,9 @@ export const normalizeActionDrafts = (drafts: ActionDraft[] | null | undefined): ? stringifyRewardActionValue(parsedValue.rewardId, parsedValue.rewardName) || "" : "" })() - : draft.event === "settle_score" - ? "" - : toStringValue(Array.isArray(draft.value) ? draft.value[0] : draft.value), + : draft.event === "settle_score" + ? "" + : toStringValue(Array.isArray(draft.value) ? draft.value[0] : draft.value), } satisfies ActionDraft }) .filter((item): item is ActionDraft => Boolean(item)) @@ -664,9 +664,9 @@ export const actionsToDrafts = (actions: AutoScoreAction[]): ActionDraft[] => { ? stringifyRewardActionValue(parsedValue.rewardId, parsedValue.rewardName) || "" : "" })() - : action.event === "settle_score" - ? "" - : toStringValue(action.value), + : action.event === "settle_score" + ? "" + : toStringValue(action.value), } satisfies ActionDraft }) .filter((item): item is ActionDraft => Boolean(item)) diff --git a/src/components/ContentArea.tsx b/src/components/ContentArea.tsx index 04329c5..2ae599b 100644 --- a/src/components/ContentArea.tsx +++ b/src/components/ContentArea.tsx @@ -686,4 +686,3 @@ export function ContentArea({ ) } - diff --git a/src/components/OAuth/OAuthLogin.tsx b/src/components/OAuth/OAuthLogin.tsx index 0a558c7..15bd9dc 100644 --- a/src/components/OAuth/OAuthLogin.tsx +++ b/src/components/OAuth/OAuthLogin.tsx @@ -17,7 +17,6 @@ interface OAuthLoginProps { interface OAuthConfig { platform_id: string - platform_secret: string } interface OAuthCallbackResult { @@ -56,15 +55,13 @@ export function OAuthLogin({ visible, onClose, onSuccess }: OAuthLoginProps) { if (!api) return null const platformId = import.meta.env.VITE_OAUTH_PLATFORM_ID - const platformSecret = import.meta.env.VITE_OAUTH_PLATFORM_SECRET - if (!platformId || !platformSecret) { + if (!platformId) { return null } return { platform_id: platformId, - platform_secret: platformSecret, } } @@ -138,7 +135,6 @@ export function OAuthLogin({ visible, onClose, onSuccess }: OAuthLoginProps) { const tokenRes = await api.oauthExchangeCode( result.code, config.platform_id, - config.platform_secret, callbackUrl, codeVerifier ) diff --git a/src/components/SectlCloudSyncPanel.tsx b/src/components/SectlCloudSyncPanel.tsx new file mode 100644 index 0000000..a982dca --- /dev/null +++ b/src/components/SectlCloudSyncPanel.tsx @@ -0,0 +1,402 @@ +import React, { useState, useEffect, useCallback } from "react" +import { + Card, + Button, + Space, + Tag, + Table, + Progress, + message, + Spin, + Tooltip, + Modal, + Radio, + Divider, + Alert, +} from "antd" +import { + CloudSyncOutlined, + CloudUploadOutlined, + CloudDownloadOutlined, + ReloadOutlined, + CheckCircleOutlined, + ExclamationCircleOutlined, + InfoCircleOutlined, +} from "@ant-design/icons" +import { useTranslation } from "react-i18next" +import { useSectl } from "../contexts/SectlContext" +import { sectlCloudSync, CloudSyncResult, CloudSyncStatus } from "../services/sectlCloudSync" +import { sectlAuth } from "../services/sectlAuth" + +const TABLE_LABELS: Record = { + students: "学生", + reasons: "理由", + score_events: "积分事件", + tags: "标签", + student_tags: "学生标签", + reward_settings: "奖励设置", + reward_redemptions: "奖励兑换", +} + +interface StorageStats { + used: number + total: number + percentage: number + table_stats: Array<{ table: string; count: number; size: number }> +} + +export const SectlCloudSyncPanel: React.FC = () => { + const { t } = useTranslation() + const { isAuthenticated, isLoading: authLoading } = useSectl() + const [syncStatus, setSyncStatus] = useState(sectlCloudSync.getStatus()) + const [storageStats, setStorageStats] = useState(null) + const [syncDirection, setSyncDirection] = useState<"push" | "pull" | "bidirectional">( + "bidirectional" + ) + const [lastResult, setLastResult] = useState(null) + const [loadingStats, setLoadingStats] = useState(false) + const [messageApi, contextHolder] = message.useMessage() + + const refreshStatus = useCallback(() => { + setSyncStatus(sectlCloudSync.getStatus()) + }, []) + + useEffect(() => { + refreshStatus() + const interval = setInterval(refreshStatus, 10_000) + return () => clearInterval(interval) + }, [refreshStatus]) + + const loadStorageStats = useCallback(async () => { + if (!isAuthenticated) return + setLoadingStats(true) + try { + const stats = await sectlCloudSync.getCloudStorageUsage() + setStorageStats(stats) + } catch { + // ignore + } finally { + setLoadingStats(false) + } + }, [isAuthenticated]) + + useEffect(() => { + if (isAuthenticated) loadStorageStats() + }, [isAuthenticated, loadStorageStats]) + + const handleSync = async (direction: "push" | "pull" | "bidirectional") => { + if (!sectlAuth.isAuthenticated()) { + messageApi.warning(t("settings.cloudSync.loginRequired")) + return + } + + refreshStatus() + if (syncStatus.is_syncing) { + messageApi.warning(t("settings.cloudSync.syncInProgress")) + return + } + + const directionLabel = + direction === "push" + ? t("settings.cloudSync.uploadToCloud") + : direction === "pull" + ? t("settings.cloudSync.downloadFromCloud") + : t("settings.cloudSync.fullSync") + + Modal.confirm({ + title: directionLabel, + content: t("settings.cloudSync.syncConfirm", { direction: directionLabel }), + okText: t("common.confirm"), + cancelText: t("common.cancel"), + onOk: async () => { + try { + let result: CloudSyncResult + if (direction === "push") { + result = await sectlCloudSync.syncToCloud() + } else if (direction === "pull") { + result = await sectlCloudSync.syncFromCloud() + } else { + result = await sectlCloudSync.fullSync("bidirectional") + } + + setLastResult(result) + refreshStatus() + await loadStorageStats() + + if (result.success) { + messageApi.success(result.message) + window.dispatchEvent( + new CustomEvent("ss:data-updated", { detail: { category: "all" } }) + ) + } else { + messageApi.error(result.message) + } + } catch (err: unknown) { + const msg = err instanceof Error ? err.message : String(err) + messageApi.error(`同步失败:${msg}`) + } + }, + }) + } + + const formatBytes = (bytes: number): string => { + if (bytes === 0) return "0 B" + const k = 1024 + const sizes = ["B", "KB", "MB"] + const i = Math.floor(Math.log(bytes) / Math.log(k)) + return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}` + } + + const formatTime = (iso: string | null): string => { + if (!iso) return t("settings.cloudSync.never") + try { + return new Date(iso).toLocaleString() + } catch { + return iso + } + } + + if (authLoading) { + return ( + +
+ +
+
+ ) + } + + if (!isAuthenticated) { + return ( + + } + message={t("settings.cloudSync.loginRequired")} + description={t("settings.cloudSync.loginHint")} + /> + + ) + } + + return ( +
+ {contextHolder} + + + + {t("settings.cloudSync.statusTitle")} + + } + style={{ backgroundColor: "var(--ss-card-bg)", color: "var(--ss-text-main)" }} + > +
+ + + {syncStatus.is_configured + ? t("settings.cloudSync.configured") + : t("settings.cloudSync.notConfigured")} + + {syncStatus.is_syncing && ( + + {t("settings.cloudSync.syncing")} + + )} + + + {t("settings.cloudSync.lastSync")}: {formatTime(syncStatus.last_sync_at)} + +
+ + {lastResult && ( + : } + message={lastResult.message} + description={ + lastResult.tables.length > 0 && ( +
+ {lastResult.tables + .filter((tr) => tr.uploaded > 0 || tr.downloaded > 0 || tr.errors.length > 0) + .map((tr) => ( + 0 ? "error" : "success"} + style={{ marginBottom: 4 }} + > + {TABLE_LABELS[tr.table] || tr.table} + {tr.uploaded > 0 && ` ↑${tr.uploaded}`} + {tr.downloaded > 0 && ` ↓${tr.downloaded}`} + {tr.errors.length > 0 && ` ⚠${tr.errors.length}`} + + ))} +
+ ) + } + style={{ marginBottom: 16 }} + closable + onClose={() => setLastResult(null)} + /> + )} +
+ + +
+
+ {t("settings.cloudSync.syncDirection")} +
+ setSyncDirection(e.target.value)} + style={{ width: "100%" }} + > + + {t("settings.cloudSync.bidirectional")} + {t("settings.cloudSync.pushOnly")} + {t("settings.cloudSync.pullOnly")} + + +
+ + + + + + + + + + + +
+ + + {t("settings.cloudSync.storageUsage")} + +