Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ Examora is a monorepo with three logical layers:

## Judge Flow (planned, not yet implemented)

1. `POST /api/client/programming/submissions` → inserts `submissions` + `judge_tasks` rows, enqueues to Redis Stream
1. `POST /api/v1/submissions` → inserts `submissions` + `judge_tasks` rows, enqueues to Redis Stream
2. `judge-worker` consumes from Redis Stream → reads submission + test cases from PostgreSQL → calls `sandbox-runner` → writes `judge_case_results` → updates `submissions.status`
3. Candidate polls status until terminal state

Expand Down
2 changes: 1 addition & 1 deletion apps/admin/src/access.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export default function access(
) {
const { currentUser, forbidden } = initialState ?? {};

// If the user is forbidden (403 from /api/auth/me), deny access
// If the user is forbidden (403 from /api/v1/auth/me), deny access
if (forbidden) {
return { canAdmin: false };
}
Expand Down
14 changes: 8 additions & 6 deletions apps/admin/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import {
type Settings as LayoutSettings,
SettingDrawer,
} from '@ant-design/pro-components';
import { API_PATHS } from '@examora/types';
import type { RunTimeLayoutConfig } from '@umijs/max';
import { history, Link } from '@umijs/max';
import { history, Link, useIntl } from '@umijs/max';
import { Avatar, Button, ConfigProvider, Result } from 'antd';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
Expand Down Expand Up @@ -86,7 +87,7 @@ export async function getInitialState(): Promise<{
if (!token) return null;

try {
const response = await fetch('/api/auth/me', {
const response = await fetch(API_PATHS.auth.me, {
headers: { Authorization: `Bearer ${token}` },
});
const data = await response.json();
Expand Down Expand Up @@ -255,13 +256,14 @@ export const request: any = {

// 无权限页面组件
const ForbiddenPage: React.FC = () => {
const intl = useIntl();
const [loading, setLoading] = React.useState(false);

const handleLogout = async () => {
setLoading(true);
try {
const token = getAccessToken();
await fetch('/api/auth/logout', {
await fetch(API_PATHS.auth.logout, {
method: 'POST',
headers: { Authorization: `Bearer ${token}` },
});
Expand All @@ -275,11 +277,11 @@ const ForbiddenPage: React.FC = () => {
return (
<Result
status="403"
title="无权访问后台"
subTitle="您的账户尚未激活或没有后台访问权限,请联系管理员开通权限。"
title={intl.formatMessage({ id: 'pages.forbidden.title' })}
subTitle={intl.formatMessage({ id: 'pages.forbidden.subTitle' })}
extra={
<Button type="primary" loading={loading} onClick={handleLogout}>
重新登录
{intl.formatMessage({ id: 'pages.forbidden.relogin' })}
</Button>
}
/>
Expand Down
3 changes: 2 additions & 1 deletion apps/admin/src/auth/config.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { API_PATHS } from '@examora/types';
import { request } from '@umijs/max';
import type { AuthConfig } from './token';

export const fetchAuthConfig = async (): Promise<AuthConfig> => {
const response = await request<AuthConfig>('/api/auth/config', {
const response = await request<AuthConfig>(API_PATHS.auth.config, {
method: 'GET',
});
return response;
Expand Down
3 changes: 2 additions & 1 deletion apps/admin/src/components/RightContent/AvatarDropdown.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { LogoutOutlined, SettingOutlined } from '@ant-design/icons';
import { API_PATHS } from '@examora/types';
import { history, request, useIntl, useModel } from '@umijs/max';
import type { MenuProps } from 'antd';
import { Spin } from 'antd';
Expand Down Expand Up @@ -48,7 +49,7 @@ export const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({
const intl = useIntl();
const loginOut = async () => {
try {
await request('/api/auth/logout', { method: 'POST' });
await request(API_PATHS.auth.logout, { method: 'POST' });
} catch (_) {
// ignore logout errors
}
Expand Down
1 change: 1 addition & 0 deletions apps/admin/src/components/RightContent/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const useStyles = createStyles(() => ({
fontSize: 18,
color: 'inherit',
transition: 'background 0.2s',
borderRadius: 8,
'&:hover': { background: 'rgba(0,0,0,0.04)' },
},
}));
Expand Down
8 changes: 5 additions & 3 deletions apps/admin/src/components/TagSelect/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { DownOutlined, UpOutlined } from '@ant-design/icons';
import { useMergedState } from '@rc-component/util';
import { useIntl } from '@umijs/max';
import { Tag } from 'antd';
import { clsx } from 'clsx';
import React, { type FC, useMemo, useState } from 'react';
Expand Down Expand Up @@ -51,6 +52,7 @@ export interface TagSelectProps {
const TagSelect: FC<TagSelectProps> & {
Option: typeof TagSelectOption;
} = (props) => {
const intl = useIntl();
const { styles } = useStyles();
const {
children,
Expand Down Expand Up @@ -103,9 +105,9 @@ const TagSelect: FC<TagSelectProps> & {
};
const checkedAll = allTags.length === value?.length && allTags.length > 0;
const {
expandText = '展开',
collapseText = '收起',
selectAllText = '全部',
expandText = intl.formatMessage({ id: 'common.expand' }),
collapseText = intl.formatMessage({ id: 'common.collapse' }),
selectAllText = intl.formatMessage({ id: 'common.selectAll' }),
} = actionsText;
const cls = clsx(styles.tagSelect, className, {
[styles.hasExpandTag]: expandable,
Expand Down
128 changes: 128 additions & 0 deletions apps/admin/src/locales/en-US/pages.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
export default {
'common.actions': 'Actions',
'common.expand': 'Expand',
'common.collapse': 'Collapse',
'common.selectAll': 'All',
'pages.404.subTitle': 'Sorry, the page you visited does not exist.',
'pages.404.buttonText': 'Back to Home',
'pages.admin.subPage.title': 'Admin-only page',
Expand Down Expand Up @@ -442,6 +445,131 @@ export default {
'To-do tasks will be notified via in-site messages',
'pages.account.settings.switch.on': 'On',
'pages.account.settings.switch.off': 'Off',
// Dashboard
'pages.dashboard.week.mon': 'Mon',
'pages.dashboard.week.tue': 'Tue',
'pages.dashboard.week.wed': 'Wed',
'pages.dashboard.week.thu': 'Thu',
'pages.dashboard.week.fri': 'Fri',
'pages.dashboard.week.sat': 'Sat',
'pages.dashboard.week.sun': 'Sun',
'pages.dashboard.hero.title': 'Exam Operations Dashboard',
'pages.dashboard.hero.kicker': 'Live Operations',
'pages.dashboard.hero.subtitle':
"Aggregate exam progress, proctoring events, question assets, and judge queues to help admins identify today's key risks quickly.",
'pages.dashboard.hero.online': 'Online Candidates',
'pages.dashboard.hero.risk': 'Risk Alerts',
'pages.dashboard.hero.queue': 'Pending Judges',
'pages.dashboard.actions.createExam': 'New Exam',
'pages.dashboard.actions.viewMonitoring': 'View Monitoring',
'pages.dashboard.stats.candidates.label': 'Registered Candidates',
'pages.dashboard.stats.candidates.caption': '146 new candidates this week',
'pages.dashboard.stats.activeExams.label': 'Active Exams',
'pages.dashboard.stats.activeExams.trend': '45 online',
'pages.dashboard.stats.activeExams.caption': 'Across 6 rooms',
'pages.dashboard.stats.papers.label': 'Total Papers',
'pages.dashboard.stats.papers.caption': 'Published this month',
'pages.dashboard.stats.questions.label': 'Total Questions',
'pages.dashboard.stats.questions.caption': 'Question reuse rate',
'pages.dashboard.chart.weekAria': '{label} 7-day trend',
'pages.dashboard.chart.empty': 'No trend data',
'pages.dashboard.series.online': 'Online Candidates',
'pages.dashboard.series.queue': 'Judge Queue',
'pages.dashboard.series.risk': 'Risk Events',
'pages.dashboard.load.title': 'Exam Load Trend',
'pages.dashboard.load.range': 'Last 12 hours',
'pages.dashboard.load.peakOnline': 'Peak Online',
'pages.dashboard.load.peakOnlineCaption': 'Peaked at 16:00',
'pages.dashboard.load.peakQueue': 'Queue Peak',
'pages.dashboard.load.peakQueueCaption': 'Average 42 seconds',
'pages.dashboard.load.riskEvents': 'Risk Events',
'pages.dashboard.load.riskEventsCaption': 'Down 18% from yesterday',
'pages.dashboard.load.aria':
'Online candidates, judge queue, and risk event trend in the last 12 hours',
'pages.dashboard.progress.title': 'Exam Progress',
'pages.dashboard.progress.allExams': 'All Exams',
'pages.dashboard.progress.activeCount': '{count} active',
'pages.dashboard.progress.duration': '{count} min',
'pages.dashboard.exams.math': '2026 Spring Advanced Mathematics Final',
'pages.dashboard.exams.python': 'Python Programming Skills Test',
'pages.dashboard.exams.physics': 'College Physics Mock Test',
'pages.dashboard.status.running': 'Running',
'pages.dashboard.status.closed': 'Closed',
'pages.dashboard.risk.low': 'Low Risk',
'pages.dashboard.risk.watch': 'Watch',
'pages.dashboard.risk.normal': 'Normal',
'pages.dashboard.risk.title': 'Proctoring Risk',
'pages.dashboard.risk.watchCount': '9 need attention',
'pages.dashboard.risk.healthLabel': 'Exam Health Today',
'pages.dashboard.risk.healthTitle': 'Stable',
'pages.dashboard.risk.healthCopy':
'Auto-submit, off-screen, and network fluctuation events are under control.',
'pages.dashboard.risk.offscreen': 'Off-screen Events',
'pages.dashboard.risk.network': 'Network Fluctuations',
'pages.dashboard.risk.review': 'Manual Reviews',
'pages.dashboard.queue.title': 'Judge Queue',
'pages.dashboard.queue.waiting': 'Waiting',
'pages.dashboard.queue.running': 'Running',
'pages.dashboard.queue.failed': 'Needs Review',
'pages.dashboard.activities.title': 'Exam Activity',
'pages.dashboard.activities.a1':
'Li Na submitted the Python Programming Skills Test',
'pages.dashboard.activities.a2': "Wang Lei's exam was auto-submitted",
'pages.dashboard.activities.a3':
'Zhang Wei started the 2026 Spring Advanced Mathematics exam',
'pages.dashboard.activities.a4':
'Chen Jing submitted the Data Structures and Algorithms test',
'pages.dashboard.activities.5m': '5 minutes ago',
'pages.dashboard.activities.8m': '8 minutes ago',
'pages.dashboard.activities.12m': '12 minutes ago',
'pages.dashboard.activities.15m': '15 minutes ago',
'pages.dashboard.shortcuts.title': 'Quick Access',
'pages.dashboard.shortcuts.users': 'User Management',
'pages.dashboard.shortcuts.exams': 'Exam Management',
'pages.dashboard.shortcuts.papers': 'Paper Management',
'pages.dashboard.shortcuts.questions': 'Question Bank',
'pages.dashboard.shortcuts.programming': 'Programming Bank',
'pages.dashboard.shortcuts.submissions': 'Submissions',
// Coming soon
'pages.comingSoon.title': 'Module Under Construction',
'pages.comingSoon.description':
'This admin module is reserved in the navigation and will be connected in a future iteration.',
'pages.comingSoon.shortTitle': 'Coming Soon',
'pages.comingSoon.shortDescription':
'This feature is under development. Please check back later.',
'pages.comingSoon.backDashboard': 'Back to Dashboard',
'pages.comingSoon.viewExams': 'View Exam Management',
'pages.comingSoon.note':
'This entry keeps the admin information architecture stable while business pages are added.',
'pages.comingSoon.questions.title': 'Question Bank',
'pages.comingSoon.questions.description':
'Question lists, type settings, answers, and explanations will be connected here.',
'pages.comingSoon.programming.title': 'Programming Questions and Test Cases',
'pages.comingSoon.programming.description':
'Programming templates, sample cases, hidden cases, and execution limits will be managed here.',
'pages.comingSoon.papers.title': 'Paper Management',
'pages.comingSoon.papers.description':
'Paper composition, question ordering, score settings, and status flow will be connected here.',
'pages.comingSoon.examCreate.title': 'Create Exam',
'pages.comingSoon.examCreate.description':
'Exam details, paper binding, and pre-publish configuration will be connected here.',
'pages.comingSoon.candidates.title': 'Candidate Management',
'pages.comingSoon.candidates.description':
'Candidate accounts, exam authorization, groups, and import/export will be connected here.',
'pages.comingSoon.events.title': 'Proctoring Audit',
'pages.comingSoon.events.description':
'Desktop events, device binding, and abnormal behavior records will be viewed here.',
'pages.comingSoon.submissions.title': 'Submission Records',
'pages.comingSoon.submissions.description':
'Candidate papers, programming submissions, and scoring status will be summarized here.',
'pages.comingSoon.judgeTasks.title': 'Judge Tasks',
'pages.comingSoon.judgeTasks.description':
'Async judge tasks, retry status, and sandbox results will be tracked here.',
// Forbidden
'pages.forbidden.title': 'Admin Access Denied',
'pages.forbidden.subTitle':
'Your account is inactive or does not have admin access. Contact an administrator to enable access.',
'pages.forbidden.relogin': 'Sign In Again',
'navbar.settings': 'Settings',
'navbar.logout': 'Logout',
};
Loading
Loading