fix(dashboard): calculate usability percentage from real evaluator data#1854
Conversation
…sionAnalyticsDialog, Answers.vue)
Signed-off-by: Rakshit Yadav <yadavrakshit60@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
… verification redirects. Prevents authenticated users from seeing signin page after refresh. Signed-off-by: Rakshit Yadav <yadavrakshit60@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Refactor router logic to allow access to public pages for logged-in users with unverified emails.
…n should directly redirect to opeen room Signed-off-by: Rakshit Yadav <yadavrakshit60@gmail.com>
Signed-off-by: Rakshit Yadav <yadavrakshit60@gmail.com>
Signed-off-by: Rakshit Yadav <yadavrakshit60@gmail.com>
Signed-off-by: Rakshit Yadav <yadavrakshit60@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Rakshit Yadav <yadavrakshit60@gmail.com>
Signed-off-by: Rakshit Yadav <yadavrakshit60@gmail.com>
Signed-off-by: Rakshit Yadav <yadavrakshit60@gmail.com>
Signed-off-by: Rakshit Yadav <yadavrakshit60@gmail.com>
… resolve N^2 complexity
…existent test.answers The four Beta manager dashboard components (StudyOverview, ParticipantsInfo, TasksInfo, StorageInfo) read answer data from props.test.answers, but the Study model does not define an answers property — it only stores answersDocId. Answer data lives in the Answer Vuex store module at store.getters.testAnswerDocument.taskAnswers. Changes: - Switch all four components to read from the Answer store module - Fix task iteration to use Object.values() for Firestore maps - Remove task.attempted gate that undercounted metrics - Fix notStartedParticipants going negative when admin takes test
… align metrics with GeneralAnalytics - Add allAnswersList getter to Answer.js to centralize duplicated answer-filtering logic - Refactor StudyOverview, ParticipantsInfo, TasksInfo, StorageInfo to use the new getter - Align overallCompletionRate with calculateEffectiveness (completed/all tasks, no attempted gate) - Align taskSuccessRate with getTasksPerformance (task.completed && task.success !== false) - Align averageTaskDuration and averageCompletionTime with averageTimePerTask (count all tasks, no gates)
…table sorting via stable keys and custom-key-sort - Use locale.value from useI18n() instead of hardcoded 'es' for date formatting - Move id out of baseFile, generate stable unique IDs per file using testId_answerIdx_taskIdx_fileType - Store raw timestamp in date field for sorting, display formatted string via template slot - Add custom-key-sort with natural string sort for names and numeric sort for date/size - Add custom-filter for search on displayed date strings and study names - Add item-value='id' to v-data-table for stable row key tracking
- Replaces underscores with hyphens in locale string - Prevents Intl.DateTimeFormat from throwing on invalid BCP 47 tag
…ing-locale # Conflicts: # src/shared/utils/dateUtils.js
…re-silent-errors fix(store): add error handling to Answer.js actions that silently swa…
…ardize-cloud-functions-logging refactor(functions): replace raw console.log with project logger utility
fix: Submenu text space fixed
…n-refresh Fix: Admin redirected to test steps on refresh and after step completion in moderated sessions
…usertestview chore: remove debug console logs from attachMediaToTasks
…el-i18n Fix/moderator panel i18n
…ialog-ui fix: dialog header spacing in heuristic edit dialogs
…n/develop/axios-1.13.6 chore(deps): bump axios from 1.13.5 to 1.13.6
…wer-errors-all-languages feat(locales): add missing error translation keys to all languages
Replace hardcoded 75% return value with actual calculation using the existing statistics() utility, averaging all evaluators' scores. Closes ruxailab#1852 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
There was a problem hiding this comment.
Pull request overview
This PR fixes the heuristic manager dashboard’s usability percentage to be computed from real evaluator data, and also introduces a broad set of additional UX, analytics, auth/email-verification, routing, i18n tooling, and Firebase Functions updates across the app.
Changes:
- Replace hardcoded heuristic dashboard usability % with a computed average from evaluator statistics.
- Add time-spent aggregation/formatting (heuristic + user tests) and refactor heuristic answer/statistics UI into new components.
- Introduce an email verification flow (new view/route, auth-store enforcement, email template/function changes) plus various UserTest/manager UI refinements and i18n tooling updates.
Reviewed changes
Copilot reviewed 66 out of 70 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| weight_function/README.md | Markdown reformatting/normalization. |
| tests/unit/EmailController.spec.js | Refactors axios mocking/env setup for EmailController tests. |
| src/ux/accessibility/view/automatic/Answers.vue | Uses managed listener helper for iframe event handling + cleanup. |
| src/ux/UserTest/views/UserTestView.vue | Adds pre/post test optionality and stepper/index logic updates; removes debug logs. |
| src/ux/UserTest/views/ModeratedTestView.vue | Adjusts step progression and video call display behavior. |
| src/ux/UserTest/components/steps/WelcomeStep.vue | Stepper display updated to reflect optional pre/post test steps. |
| src/ux/UserTest/components/steps/PreTestStep.vue | Redesigns pre-test UI into paged questions with progress and navigation. |
| src/ux/UserTest/components/steps/PreTasksStep.vue | Formatting/cleanup for script/template. |
| src/ux/UserTest/components/steps/PostTestStep.vue | Redesigns post-test UI into paged questions with progress and navigation. |
| src/ux/UserTest/components/manager/TasksInfo.vue | Switches manager analytics to centralized Answer store getter and recalculates rates/times. |
| src/ux/UserTest/components/manager/StudyOverview.vue | Switches participant metrics to Answer store + accepted cooperators logic. |
| src/ux/UserTest/components/manager/StorageInfo.vue | Switches to Answer store list getter for storage calculations. |
| src/ux/UserTest/components/manager/ParticipantsInfo.vue | Recomputes participant counts from Answer store + accepted cooperators. |
| src/ux/UserTest/components/dialogs/SessionAnalyticsDialog.vue | Uses managed listener helper + cleanup for video events/RAF. |
| src/ux/UserTest/components/VideoCall.vue | Moves hardcoded UI strings to i18n keys; adds i18n usage in script. |
| src/ux/Heuristic/views/HeuristicTestView.vue | Adds heuristic shuffling for fresh runs, deterministic order for resumed runs. |
| src/ux/Heuristic/views/EditTest.vue | Makes WeightTable emit change events to mark unsaved changes. |
| src/ux/Heuristic/utils/statistics.js | Adds time parsing/formatting helpers; attaches timeSpentMs into heuristic stats. |
| src/ux/Heuristic/store/Heuristic.js | Simplifies getter signature (removes unused params). |
| src/ux/Heuristic/components/weights_evaluation/WeightTable.vue | Switches to immediate persistence via watcher; adjusts v-model handling and initialization. |
| src/ux/Heuristic/components/statistics/StatisticsSummaryCard.vue | New extracted statistics summary card component. |
| src/ux/Heuristic/components/statistics/HeuristicsDataCard.vue | New extracted heuristics data card with tabs (incl. time-by-heuristics). |
| src/ux/Heuristic/components/statistics/EvaluatorsAndGraphicsCard.vue | New extracted evaluators table/graph card component. |
| src/ux/Heuristic/components/manager/UsabilityResults.vue | Replaces hardcoded 75% with computed average from evaluator statistics. |
| src/ux/Heuristic/components/HeuristicsTestAnswer.vue | Refactors into new subcomponents; adds time-by-heuristics table. |
| src/ux/Heuristic/components/HeuristicsTable.vue | Minor dialog style adjustments. |
| src/shared/views/ReportView.vue | Adds “Total Time” column/section; computes totals for heuristic and user test reports. |
| src/shared/utils/dateUtils.js | Normalizes locale strings (e.g. pt_BR → pt-BR) before Intl formatting. |
| src/shared/store/Answer.js | Adds allAnswersList getter; improves error handling + adds total time to evaluator stats. |
| src/shared/constants/methodDefinitions.js | Adds study type normalization for legacy values and uses it in method resolution. |
| src/shared/composables/useManagedListeners.js | New managed event-listener composable/factory with cleanup support. |
| src/router/modules/public.js | Adds /verify-email public route. |
| src/features/ux_creation/StudyDetailsForm.vue | Normalizes test type when instantiating/redirecting from template creation. |
| src/features/navigation/components/navbarSections/StorageSection.vue | Adds custom sort/filter, stable IDs, and locale-aware date formatting in storage table. |
| src/features/navigation/components/DashboardSidebar.vue | Adjusts subsection indentation/icon spacing styles. |
| src/features/dashboard/components/StatsCards.vue | Formats storage MB value with fixed precision. |
| src/features/auth/views/VerifyEmailView.vue | New email verification page (check status, resend, continue, sign out). |
| src/features/auth/views/SignUpView.vue | Redirects to verify-email after signup. |
| src/features/auth/views/SignInView.vue | Redirects to verify-email when EMAIL_NOT_VERIFIED; adds mobile logo styling. |
| src/features/auth/store/Auth.js | Enforces email verification on sign-in; sends verification email on signup; adds action to resend verification. |
| src/features/auth/controllers/AuthController.js | Adds sendVerificationEmail using EmailController. |
| src/controllers/StudyController.js | Fixes notification removal call placement when removing cooperators. |
| src/app/router/index.js | Adds email-verification gate/redirect logic in global beforeEach; removes template access checks. |
| src/app/plugins/locales/zh.json | Adds auth/video-call related keys and some error keys. |
| src/app/plugins/locales/ru.json | Adds auth/video-call related keys and some error keys. |
| src/app/plugins/locales/pt_br.json | Adds auth/video-call related keys and some error keys. |
| src/app/plugins/locales/ja.json | Adds auth/video-call related keys and some error keys. |
| src/app/plugins/locales/hi.json | Adds auth/video-call related keys and some error keys. |
| src/app/plugins/locales/fr.json | Adds auth/video-call related keys and some error keys. |
| src/app/plugins/locales/es.json | Adds auth/video-call related keys and some error keys. |
| src/app/plugins/locales/en.json | Adds auth/video-call related keys and some error keys. |
| src/app/plugins/locales/de.json | Adds auth/video-call related keys and some error keys. |
| src/app/plugins/locales/ar.json | Adds auth/video-call related keys and some error keys. |
| package.json | Dependency bumps (axios, vue, vuetify, eslint ecosystem, etc.). |
| i18n-diff-guard.mjs | Adds a diff-based i18n key validator script. |
| functions/src/templates/mails/invitations.html | Updates logo asset URL. |
| functions/src/templates/mails/emailVerification.html | Adds new email verification HTML template. |
| functions/src/scheduled/cleanupGhostSessions.js | Refactors scheduled cleanup with structured logging. |
| functions/src/https/eyeTracking.js | Refactors formatting and makes CORS origins configurable via env. |
| functions/src/https/email.js | Extends email function to support email verification template and callable payload shape. |
| functions/src/helpers/addSubTypeInUser.js | Refactors helper with structured logging + formatting. |
| functions/src/f.firebase.js | Makes region getter dynamic; loads dotenv config. |
| functions/.gitignore | Ignores functions env files, keeps example. |
| functions/.env.example | Adds EYE_LAB_CORS_ORIGINS to example env. |
| README.md | Updates support/contact links and repository link. |
| CONTRIBUTING.md | Expands contributor guidance (labels, hooks, formatting/linting notes). |
| .husky/pre-commit | Switches i18n guard to .mjs. |
| .github/workflows/i18n-diff-guard.yml | Switches i18n guard to .mjs. |
| .firebaserc | Formatting-only change. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| beforeEach(() => { | ||
| jest.clearAllMocks() | ||
| emailController = new EmailController() | ||
| consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}) | ||
| process.env.VUE_APP_CLOUD_FUNCTIONS_URL = 'http://localhost' | ||
| }) | ||
|
|
||
| afterEach(() => { | ||
| consoleErrorSpy.mockRestore() | ||
| }) |
There was a problem hiding this comment.
This test mutates process.env.VUE_APP_CLOUD_FUNCTIONS_URL in beforeEach but no longer restores the original environment in afterEach, which can leak state into other tests. Please snapshot/restore process.env (or at least this key) in afterEach like the previous version did.
| <script setup> | ||
| import { computed } from 'vue' | ||
| import { useRouter } from 'vue-router' | ||
| import { useI18n } from 'vue-i18n' | ||
| import { statistics } from '@/ux/Heuristic/utils/statistics' | ||
|
|
||
| const props = defineProps({ | ||
| test: { | ||
| type: Object, | ||
| required: true, | ||
| }, | ||
| }) | ||
|
|
||
| const router = useRouter() | ||
| const { t } = useI18n() | ||
|
|
||
| // Navigate to answers section | ||
| const navigateToAnswers = () => { | ||
| if (props.test?.id) { | ||
| router.push(`/heuristic/answer/${props.test.id}`) | ||
| } | ||
| } | ||
|
|
||
| // Computed properties | ||
| const usabilityPercentage = computed(() => { | ||
| // Por ahora devolvemos 75% como solicitado | ||
| // En el futuro esto se calculará basado en las respuestas reales | ||
| return 75 | ||
| const resultEvaluator = statistics() | ||
| if (!resultEvaluator.length) return 0 | ||
| const total = resultEvaluator.reduce( | ||
| (sum, ev) => sum + parseFloat(ev.result), | ||
| 0, | ||
| ) | ||
| return Math.round(total / resultEvaluator.length) | ||
| }) |
There was a problem hiding this comment.
The PR description/title focus on fixing the hardcoded dashboard usability percentage, but this PR also introduces substantial unrelated changes (email verification flow, router guard changes, new i18n tooling, multiple UX/UserTest refactors). Please either narrow this PR to the usability calculation or update the PR description (and ideally split into separate PRs) so review and rollback risk are manageable.
| const authUser = await store.dispatch('autoSignIn') | ||
| user = store.state.Auth.user | ||
| // If user is logged in but email not verified, redirect to verify-email | ||
| if (authUser && authUser.emailVerified === false && !publicPages.includes(to.path)) { | ||
| return next('/verify-email') | ||
| } | ||
| } |
There was a problem hiding this comment.
The new email-verification redirect block has unbalanced braces/indentation, which will make the router guard invalid JS (the if (authUser && ...) block is not properly closed, and if (!user) is not closed cleanly). Please fix the braces so the guard compiles and executes as intended.
| router.beforeEach(async (to, from, next) => { | ||
| const { authorize = [] } = to.meta || {} | ||
| let user = store.state.Auth.user | ||
|
|
||
| // Special handling for accessibility preview routes - allow complete public access | ||
| const isAccessibilityPreview = | ||
| to.path.includes('/accessibility/') && to.path.includes('/preview/') | ||
|
|
||
| if (isAccessibilityPreview) { | ||
| return next() // Allow immediate access without any checks | ||
| } | ||
|
|
||
| // Allow access to public pages even if user is logged in but email not verified | ||
| const publicPages = ['/signin', '/signup', '/verify-email', '/forgot-password'] | ||
| if (publicPages.includes(to.path)) { | ||
| return next() | ||
| } | ||
|
|
||
| if (!user) { | ||
| await store.dispatch('autoSignIn') | ||
| const authUser = await store.dispatch('autoSignIn') | ||
| user = store.state.Auth.user | ||
| // If user is logged in but email not verified, redirect to verify-email | ||
| if (authUser && authUser.emailVerified === false && !publicPages.includes(to.path)) { | ||
| return next('/verify-email') | ||
| } | ||
| } | ||
|
|
||
| if (to.path === '/') return next(redirect()) | ||
|
|
||
| if (authorize.length && to.path !== '/signin' && !to.params.token) { | ||
| if (!user || !authorize.includes(user.accessLevel)) { | ||
| return next(redirect()) | ||
| } | ||
| } | ||
|
|
||
| if (to.meta?.templateAccess && to.params?.id) { | ||
| const template = await store.dispatch('getTemplateById', to.params.id) | ||
|
|
||
| if (!template) { | ||
| store.commit('SET_TOAST', { | ||
| type: 'error', | ||
| message: 'Template not found.', | ||
| }) | ||
| return next('/admin') | ||
| } | ||
|
|
||
| const ownerId = template.header?.templateAuthor?.userDocId | ||
| const isOwner = ownerId && user?.id === ownerId | ||
| const isPublic = Boolean(template.header?.isTemplatePublic) | ||
|
|
||
| if (!isOwner && !isPublic) { | ||
| store.commit('SET_TOAST', { | ||
| type: 'error', | ||
| message: 'You do not have permission to access this template.', | ||
| }) | ||
| return next('/admin') | ||
| } | ||
|
|
||
| const canonicalPath = getTemplateManagerPath(template) | ||
| if (canonicalPath && canonicalPath !== to.path) { | ||
| if (to.meta?.templateSection === 'manager') { | ||
| return next({ path: canonicalPath, query: to.query, replace: true }) | ||
| } | ||
| } | ||
|
|
||
| if (to.meta?.templateOwnerOnly && !isOwner) { | ||
| store.commit('SET_TOAST', { | ||
| type: 'error', | ||
| message: 'Only template owner can access configuration.', | ||
| }) | ||
| return next(canonicalPath || '/admin') | ||
| } | ||
| } | ||
|
|
||
| if (user && ['/signin', '/signup'].includes(to.path)) { | ||
| return next(redirect()) | ||
| } | ||
|
|
||
| next() |
There was a problem hiding this comment.
Routes in src/router/modules/admin.js still set meta.templateAccess / meta.templateOwnerOnly, but the corresponding access-control logic was removed from this global beforeEach guard. This looks like a security/authorization regression (template pages may become accessible without the intended checks). Please reintroduce the templateAccess handling (or move it into per-route guards) so those meta flags are enforced again.
| async logout() { | ||
| try { | ||
| await this.logout() | ||
| this.$router.push('/signin') | ||
| } catch {} | ||
| }, |
There was a problem hiding this comment.
methods maps the Vuex logout action and also defines a logout() method that calls this.logout(), which will recurse into itself and never dispatch the action. Rename either the mapped action (e.g., mapActions({ doLogout: 'logout' })) or the method, and call the action explicitly before routing.
| import EmailController from '@/shared/controllers/EmailController' | ||
| import axios from 'axios' | ||
|
|
||
| jest.mock('axios', () => ({ | ||
| post: jest.fn() | ||
| })) | ||
| jest.mock('axios') | ||
|
|
||
| describe('EmailController', () => { | ||
| let emailController | ||
| let consoleErrorSpy | ||
| const originalEnv = process.env | ||
|
|
||
| beforeEach(() => { | ||
| jest.clearAllMocks() | ||
| emailController = new EmailController() | ||
| process.env = { | ||
| ...originalEnv, | ||
| VUE_APP_CLOUD_FUNCTIONS_URL: 'https://cloud-functions.example.com' | ||
| } | ||
| consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}) | ||
| let emailController | ||
| let consoleErrorSpy | ||
|
|
||
| beforeEach(() => { | ||
| jest.clearAllMocks() | ||
| emailController = new EmailController() | ||
| consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}) | ||
| process.env.VUE_APP_CLOUD_FUNCTIONS_URL = 'http://localhost' | ||
| }) | ||
|
|
||
| afterEach(() => { | ||
| consoleErrorSpy.mockRestore() | ||
| }) | ||
|
|
||
| describe('Structure', () => { | ||
| it('should have send method', () => { | ||
| expect(typeof emailController.send).toBe('function') | ||
| }) | ||
| }) | ||
|
|
||
| afterEach(() => { | ||
| process.env = originalEnv | ||
| consoleErrorSpy.mockRestore() | ||
| describe('send', () => { | ||
| it('should call axios.post with correct url and payload', async () => { | ||
| axios.post.mockResolvedValue({ data: {} }) | ||
|
|
There was a problem hiding this comment.
jest.mock('axios') does not define axios.post unless you have a manual Jest mock for axios (none found in the repo). These tests call axios.post.mockResolvedValue(...), which will fail when post is undefined. Use an explicit factory mock (like in AuthController.spec.js) or set axios.post = jest.fn() in the test setup.



Summary
return 75inUsabilityResults.vuewith a real calculation using the existingstatistics()utility fromstatistics.jsresultvalues, returning0when no evaluators exist (prevents NaN)usabilityPercentageCloses #1852
Before vs After
Test plan