From 15f199258168c03cbd2171c086a09ba67d81245a Mon Sep 17 00:00:00 2001 From: HermanPlay Date: Wed, 12 Aug 2026 01:02:32 +0200 Subject: [PATCH] feat: proper page titles with identification pattern; move admin monitoring route MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - buildDocumentTitle helper: '{Name} - {Type} · Mini-Maxit' pattern - resource titles: task detail (task name), contest (contest name), contest task, results, profile (user name), submission (task title) - list pages: '{Page} · Mini-Maxit' via dashboard layout page-title map - fix: /dashboard/tasks dropped the Mini-Maxit suffix (removed bare svelte:head override); admin languages title now maps correctly - move worker monitoring from /dashboard/admin to /dashboard/admin/worker-monitoring; rename title to Worker Monitoring - closes #165 --- messages/en.json | 20 +++++++++++++++---- messages/pl.json | 20 +++++++++++++++---- .../dashboard/DashboardSidebar.svelte | 2 +- src/lib/components/dashboard/utils.ts | 4 +++- src/lib/routes.ts | 1 + src/lib/title.ts | 18 +++++++++++++++++ src/routes/+layout.svelte | 5 +++++ src/routes/dashboard/+layout.svelte | 6 +++++- .../{ => worker-monitoring}/+page.svelte | 0 src/routes/dashboard/tasks/+page.svelte | 4 ---- .../dashboard/tasks/[taskId]/+page.svelte | 11 ++++++++++ .../user/contests/[contestId]/+page.svelte | 20 +++++++++++++++++++ .../contests/[contestId]/results/+page.svelte | 11 ++++++++++ .../[contestId]/tasks/[taskId]/+page.svelte | 11 ++++++++++ .../dashboard/user/profile/+page.svelte | 10 ++++++++++ .../submissions/[submissionId]/+page.svelte | 11 ++++++++++ 16 files changed, 139 insertions(+), 15 deletions(-) create mode 100644 src/lib/title.ts rename src/routes/dashboard/admin/{ => worker-monitoring}/+page.svelte (100%) diff --git a/messages/en.json b/messages/en.json index c9251bf..29cae16 100644 --- a/messages/en.json +++ b/messages/en.json @@ -281,7 +281,7 @@ "relative_time_years_ago_plural": "{count} years ago", "sidebar_admin": "Admin", "sidebar_teacher": "Teacher", - "sidebar_admin_monitoring": "System Monitoring", + "sidebar_admin_monitoring": "Worker Monitoring", "sidebar_admin_contests": "Contests", "sidebar_admin_tasks": "Tasks", "sidebar_admin_users": "Users", @@ -506,7 +506,7 @@ "admin_contest_submissions_placeholder_user": "Search by username, name...", "admin_contest_submissions_placeholder_task": "Search by task name or ID...", "admin_contests_card_view_submissions": "View Submissions", - "admin_dashboard_title": "Admin Dashboard", + "admin_dashboard_title": "Worker Monitoring", "admin_dashboard_worker_status": "Worker Status", "admin_dashboard_total_workers": "Total Workers", "admin_dashboard_busy_workers": "Busy Workers", @@ -515,7 +515,7 @@ "admin_dashboard_worker_details": "Worker Details", "admin_dashboard_worker_status_error": "Failed to load worker status", "admin_dashboard_loading": "Loading...", - "admin_dashboard_system_monitoring": "System Monitoring", + "admin_dashboard_system_monitoring": "Worker Status", "admin_dashboard_total_workers_description": "All registered workers", "admin_dashboard_busy_workers_description": "Currently processing", "admin_dashboard_available_workers_description": "Ready for tasks", @@ -895,5 +895,17 @@ "admin_languages_status_disabled": "Disabled", "admin_languages_action_toggle": "Toggle", "admin_languages_toggle_error": "Failed to update language", - "admin_languages_toggle_success": "Language updated successfully" + "admin_languages_toggle_success": "Language updated successfully", + "app_name": "Mini-Maxit", + "title_type_task": "Task", + "title_type_contest": "Contest", + "title_type_profile": "Profile", + "title_type_submission": "Submission", + "title_type_results": "Results", + "title_type_group": "Group", + "title_type_groups": "Groups", + "title_type_language": "Languages", + "title_type_user": "Users", + "title_type_dashboard": "Dashboard", + "title_type_admin": "Admin" } diff --git a/messages/pl.json b/messages/pl.json index 76a4345..0322d83 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -281,7 +281,7 @@ "relative_time_years_ago_plural": "{count} lat temu", "sidebar_admin": "Admin", "sidebar_teacher": "Nauczyciel", - "sidebar_admin_monitoring": "Monitoring Systemu", + "sidebar_admin_monitoring": "Monitoring Pracowników", "sidebar_admin_contests": "Konkursy", "sidebar_admin_tasks": "Zadania", "sidebar_admin_users": "Użytkownicy", @@ -506,7 +506,7 @@ "admin_contest_submissions_placeholder_user": "Szukaj według nazwy użytkownika, imienia...", "admin_contest_submissions_placeholder_task": "Szukaj według nazwy zadania lub ID...", "admin_contests_card_view_submissions": "Zobacz Zgłoszenia", - "admin_dashboard_title": "Panel Administracyjny", + "admin_dashboard_title": "Monitoring Pracowników", "admin_dashboard_worker_status": "Status Workerów", "admin_dashboard_total_workers": "Wszystkie Workery", "admin_dashboard_busy_workers": "Zajęte Workery", @@ -515,7 +515,7 @@ "admin_dashboard_worker_details": "Szczegóły Workerów", "admin_dashboard_worker_status_error": "Nie udało się załadować statusu workerów", "admin_dashboard_loading": "Ładowanie...", - "admin_dashboard_system_monitoring": "Monitoring Systemu", + "admin_dashboard_system_monitoring": "Status Pracowników", "admin_dashboard_total_workers_description": "Wszystkie zarejestrowane workery", "admin_dashboard_busy_workers_description": "Obecnie przetwarzane", "admin_dashboard_available_workers_description": "Gotowe do zadań", @@ -895,5 +895,17 @@ "admin_languages_status_disabled": "Wyłączony", "admin_languages_action_toggle": "Przełącz", "admin_languages_toggle_error": "Nie udało się zaktualizować języka", - "admin_languages_toggle_success": "Język zaktualizowany pomyślnie" + "admin_languages_toggle_success": "Język zaktualizowany pomyślnie", + "app_name": "Mini-Maxit", + "title_type_task": "Zadanie", + "title_type_contest": "Konkurs", + "title_type_profile": "Profil", + "title_type_submission": "Zgłoszenie", + "title_type_results": "Wyniki", + "title_type_group": "Grupa", + "title_type_groups": "Grupy", + "title_type_language": "Języki", + "title_type_user": "Użytkownicy", + "title_type_dashboard": "Panel", + "title_type_admin": "Administracja" } diff --git a/src/lib/components/dashboard/DashboardSidebar.svelte b/src/lib/components/dashboard/DashboardSidebar.svelte index a2f9d8d..741bfae 100644 --- a/src/lib/components/dashboard/DashboardSidebar.svelte +++ b/src/lib/components/dashboard/DashboardSidebar.svelte @@ -87,7 +87,7 @@ const adminMenuItems = [ { title: () => m.sidebar_admin_monitoring(), - href: localizeHref(AppRoutes.Admin), + href: localizeHref(AppRoutes.AdminWorkerMonitoring), icon: Activity }, { diff --git a/src/lib/components/dashboard/utils.ts b/src/lib/components/dashboard/utils.ts index 04d35c3..1e51f25 100644 --- a/src/lib/components/dashboard/utils.ts +++ b/src/lib/components/dashboard/utils.ts @@ -18,7 +18,9 @@ export function getDashboardTitleTranslationFromPathname(pathname: string): stri [AppRoutes.TeacherContests]: () => m.sidebar_admin_contests(), [AppRoutes.TeacherGroups]: () => m.groups_management_title(), [AppRoutes.TeacherTasks]: () => m.sidebar_admin_tasks(), - [AppRoutes.AdminUsers]: () => m.admin_users_title() + [AppRoutes.AdminUsers]: () => m.admin_users_title(), + [AppRoutes.AdminLanguages]: () => m.admin_languages_title(), + [AppRoutes.AdminWorkerMonitoring]: () => m.sidebar_admin_monitoring() }; // Check for dynamic routes (e.g., /dashboard/tasks/[taskId]) diff --git a/src/lib/routes.ts b/src/lib/routes.ts index 5c591f8..39b0272 100644 --- a/src/lib/routes.ts +++ b/src/lib/routes.ts @@ -30,6 +30,7 @@ export enum AppRoutes { Admin = `${AppRoutes.Dashboard}/admin`, AdminUsers = `${AppRoutes.Admin}/users`, AdminLanguages = `${AppRoutes.Admin}/languages`, + AdminWorkerMonitoring = `${AppRoutes.Admin}/worker-monitoring`, Error = '/error' } diff --git a/src/lib/title.ts b/src/lib/title.ts new file mode 100644 index 0000000..cf5202a --- /dev/null +++ b/src/lib/title.ts @@ -0,0 +1,18 @@ +import * as m from '$lib/paraglide/messages'; + +export const APP_NAME = 'Mini-Maxit'; + +/** + * Builds a browser tab title with a consistent identification pattern. + * e.g. `Test Contest - Contest · Mini-Maxit` + * + * @param name - the resource name (task title, contest name, user name...) + * @param type - the resource type label (translated, e.g. "Contest") + */ +export function buildDocumentTitle(name?: string, type?: string): string { + const parts: string[] = []; + if (name) parts.push(name); + if (type) parts.push(type); + if (parts.length === 0) return m.app_name(); + return `${parts.join(' - ')} · ${APP_NAME}`; +} diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index d71e84f..09769fd 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -6,6 +6,7 @@ import { page } from '$app/state'; import { getApiInstance } from '$lib/services'; import { isProtectedRoute } from '$lib/routes'; + import { buildDocumentTitle } from '$lib/title'; let { children } = $props(); @@ -26,6 +27,10 @@ }); + + {buildDocumentTitle()} + + {@render children?.()} diff --git a/src/routes/dashboard/+layout.svelte b/src/routes/dashboard/+layout.svelte index 1b49eae..0303727 100644 --- a/src/routes/dashboard/+layout.svelte +++ b/src/routes/dashboard/+layout.svelte @@ -12,12 +12,12 @@ import { getUserInstance, getApiInstance } from '$lib/services'; import { LoadingSpinner } from '$lib/components/common'; import Footer from '$lib/components/Footer.svelte'; + import { buildDocumentTitle } from '$lib/title'; let { children } = $props(); const pageTitle = $derived(getDashboardTitleTranslationFromPathname(page.url.pathname)); const isLoading = $derived(userStore.isLoading()); - // Client-side authentication guard and user fetch onMount(async () => { if (!browser) return; @@ -58,6 +58,10 @@ }); + + {buildDocumentTitle(pageTitle)} + + {#if isLoading || !userStore.tryGetUser()}
diff --git a/src/routes/dashboard/admin/+page.svelte b/src/routes/dashboard/admin/worker-monitoring/+page.svelte similarity index 100% rename from src/routes/dashboard/admin/+page.svelte rename to src/routes/dashboard/admin/worker-monitoring/+page.svelte diff --git a/src/routes/dashboard/tasks/+page.svelte b/src/routes/dashboard/tasks/+page.svelte index 181fbb8..432cbf8 100644 --- a/src/routes/dashboard/tasks/+page.svelte +++ b/src/routes/dashboard/tasks/+page.svelte @@ -18,10 +18,6 @@ }); - - {m.tasks_page_title()} - -

{m.tasks_title()}

diff --git a/src/routes/dashboard/tasks/[taskId]/+page.svelte b/src/routes/dashboard/tasks/[taskId]/+page.svelte index 892396d..1cf4cf1 100644 --- a/src/routes/dashboard/tasks/[taskId]/+page.svelte +++ b/src/routes/dashboard/tasks/[taskId]/+page.svelte @@ -8,6 +8,7 @@ import FilePreview from '$lib/components/dashboard/tasks/task-page/tasks/FilePreview.svelte'; import { LoadingSpinner, ErrorCard } from '$lib/components/common'; import * as m from '$lib/paraglide/messages'; + import { buildDocumentTitle } from '$lib/title'; const taskService = getTaskInstance(); const submissionService = getSubmissionInstance(); @@ -32,8 +33,18 @@ }); let fileContent = $state(''); + + const documentTitle = $derived( + buildDocumentTitle(taskQuery.current?.title, m.title_type_task()) + ); + + {#if taskQuery.current?.title} + {documentTitle} + {/if} + +
{#if taskQuery.error} contestId, + async (id) => { + if (!contestService) throw new Error('Service unavailable'); + if (!Number.isFinite(id)) throw new Error('Invalid contest ID'); + return await contestService.getContest(id); + } + ); + const tasksQuery = createParameterizedQuery( () => contestId, async (id) => { @@ -25,8 +35,18 @@ return result.data!; } ); + + const documentTitle = $derived( + buildDocumentTitle(contestQuery.current?.name, m.title_type_contest()) + ); + + {#if contestQuery.current?.name} + {documentTitle} + {/if} + +

diff --git a/src/routes/dashboard/user/contests/[contestId]/results/+page.svelte b/src/routes/dashboard/user/contests/[contestId]/results/+page.svelte index 532b255..3662ef2 100644 --- a/src/routes/dashboard/user/contests/[contestId]/results/+page.svelte +++ b/src/routes/dashboard/user/contests/[contestId]/results/+page.svelte @@ -11,6 +11,7 @@ import { format } from 'date-fns'; import * as m from '$lib/paraglide/messages'; import type { TaskResult } from '$lib/dto/contest'; + import { buildDocumentTitle } from '$lib/title'; const contestService = getContestInstance(); const contestId = $derived(Number(page.params.contestId)); @@ -44,8 +45,18 @@ const date = new Date(dateString); return format(date, 'MMM dd, yyyy'); } + + const documentTitle = $derived( + buildDocumentTitle(resultsQuery.current?.contest?.name, m.title_type_results()) + ); + + {#if resultsQuery.current?.contest?.name} + {documentTitle} + {/if} + +

{m.contest_results_title()}

diff --git a/src/routes/dashboard/user/contests/[contestId]/tasks/[taskId]/+page.svelte b/src/routes/dashboard/user/contests/[contestId]/tasks/[taskId]/+page.svelte index 0c04591..7274380 100644 --- a/src/routes/dashboard/user/contests/[contestId]/tasks/[taskId]/+page.svelte +++ b/src/routes/dashboard/user/contests/[contestId]/tasks/[taskId]/+page.svelte @@ -8,6 +8,7 @@ import { LoadingSpinner, ErrorCard } from '$lib/components/common'; import * as m from '$lib/paraglide/messages'; import TaskHeader from '$lib/components/dashboard/tasks/task-page/tasks/TaskHeader.svelte'; + import { buildDocumentTitle } from '$lib/title'; const taskService = getTaskInstance(); const submissionService = getSubmissionInstance(); @@ -34,8 +35,18 @@ }); let fileContent = $state(''); + + const documentTitle = $derived( + buildDocumentTitle(taskQuery.current?.title, m.title_type_task()) + ); + + {#if taskQuery.current?.title} + {documentTitle} + {/if} + +
{#if taskQuery.error} + + {#if user} + {documentTitle} + {/if} + +
diff --git a/src/routes/dashboard/user/submissions/[submissionId]/+page.svelte b/src/routes/dashboard/user/submissions/[submissionId]/+page.svelte index f7c3e57..65da3c6 100644 --- a/src/routes/dashboard/user/submissions/[submissionId]/+page.svelte +++ b/src/routes/dashboard/user/submissions/[submissionId]/+page.svelte @@ -6,6 +6,7 @@ import * as Card from '$lib/components/ui/card'; import * as m from '$lib/paraglide/messages'; import { formatDate } from '$lib/utils'; + import { buildDocumentTitle } from '$lib/title'; import TestCaseResult from '$lib/components/dashboard/submissions/TestCaseResult.svelte'; import { SubmissionStatus, SubmissionResultCode, type TestResult } from '$lib/dto/submission'; import CheckCircle from '@lucide/svelte/icons/check-circle'; @@ -143,8 +144,18 @@ const total = submissionQuery.current.result.testResults.length; return `${passed}/${total}`; }; + + const documentTitle = $derived( + buildDocumentTitle(submissionQuery.current?.task?.title, m.title_type_submission()) + ); + + {#if submissionQuery.current?.task?.title} + {documentTitle} + {/if} + +