feat: implement debug logging cleanup and i18n coverage proposals

## cleanup-debug-logging
- Create environment-aware logger utility (logger.ts)
- Replace 60+ console.log/error statements across 28 files
- Production: only warn/error logs visible
- Development: all log levels with prefixes

Updated files:
- Contexts: NotificationContext, ProjectSyncContext, AuthContext
- Components: GanttChart, CalendarView, ErrorBoundary, and 11 others
- Pages: Tasks, Projects, Dashboard, and 7 others
- Services: api.ts

## complete-i18n-coverage
- WeeklyReportPreview: all strings translated, dynamic locale
- ReportHistory: all strings translated, dynamic locale
- AuditPage: detail modal and verification modal translated
- WorkloadPage: error message translated

Locale files updated:
- en/common.json, zh-TW/common.json: reports section
- en/audit.json, zh-TW/audit.json: modal sections
- en/workload.json, zh-TW/workload.json: errors section

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
beabigegg
2026-01-13 21:37:29 +08:00
parent ed8d30e9bd
commit 4b7e523f84
37 changed files with 430 additions and 207 deletions

View File

@@ -52,5 +52,39 @@
"empty": {
"title": "No Audit Records",
"description": "No audit records match the current filters"
},
"modal": {
"details": {
"title": "Audit Log Details",
"eventType": "Event Type:",
"action": "Action:",
"resource": "Resource:",
"user": "User:",
"ipAddress": "IP Address:",
"sensitivity": "Sensitivity:",
"time": "Time:",
"changes": "Changes",
"field": "Field",
"oldValue": "Old Value",
"newValue": "New Value",
"checksum": "Checksum:",
"na": "N/A",
"system": "System"
},
"verification": {
"title": "Integrity Verification",
"verifying": "Verifying audit log integrity...",
"verifyingHint": "This may take a moment depending on the number of records.",
"failed": "Verification Failed",
"statusSuccess": "Integrity Verified",
"statusFailed": "Integrity Issues Detected",
"successDescription": "All audit records have valid checksums and have not been tampered with.",
"failedDescription": "Some audit records have invalid checksums, indicating potential tampering or corruption.",
"totalChecked": "Total Checked",
"valid": "Valid",
"invalid": "Invalid",
"invalidRecords": "Invalid Records",
"invalidRecordsDescription": "The following record IDs failed integrity verification:"
}
}
}

View File

@@ -21,7 +21,8 @@
"remove": "Remove",
"view": "View",
"download": "Download",
"upload": "Upload"
"upload": "Upload",
"retry": "Retry"
},
"labels": {
"loading": "Loading...",
@@ -135,5 +136,62 @@
"maxFileSize": "Maximum file size: {{size}}",
"uploading": "Uploading {{filename}} ({{current}}/{{total}})...",
"uploadFailed": "Upload failed"
},
"reports": {
"weeklyPreview": {
"title": "Weekly Report Preview",
"generateNow": "Generate Now",
"generating": "Generating...",
"loading": "Loading report preview...",
"noData": "No report data available",
"noProjects": "No projects found",
"projects": "Projects",
"retry": "Retry",
"completedShort": "completed",
"done": "done",
"inProgressLower": "in progress",
"overdueLower": "overdue",
"blockedLower": "blocked",
"nextWeekLower": "next week",
"unassigned": "Unassigned",
"due": "Due",
"since": "Since",
"noReasonProvided": "No reason provided",
"daysOverdue": "{{count}} day overdue",
"daysOverdue_other": "{{count}} days overdue",
"status": {
"completed": "Completed",
"inProgress": "In Progress",
"overdue": "Overdue",
"blocked": "Blocked",
"nextWeek": "Next Week",
"total": "Total"
},
"errors": {
"loadFailed": "Failed to load report preview",
"generateFailed": "Failed to generate report"
},
"messages": {
"generateSuccess": "Report generated and notification sent!"
}
},
"history": {
"title": "Report History",
"loading": "Loading history...",
"retry": "Retry",
"empty": "No report history found. Reports are generated every Friday at 16:00.",
"totalReports": "{{count}} report",
"totalReports_other": "{{count}} reports",
"completed": "Completed",
"inProgress": "In Progress",
"overdue": "Overdue",
"status": {
"sent": "sent",
"failed": "failed"
},
"errors": {
"loadFailed": "Failed to load report history"
}
}
}
}

View File

@@ -68,5 +68,8 @@
"normal": "Normal: < 80%",
"warning": "Warning: 80% - 99%",
"overloaded": "Overloaded: ≥ 100%"
},
"errors": {
"loadFailed": "Failed to load workload data. Please try again."
}
}

View File

@@ -52,5 +52,39 @@
"empty": {
"title": "沒有稽核記錄",
"description": "目前沒有符合條件的稽核記錄"
},
"modal": {
"details": {
"title": "稽核日誌詳情",
"eventType": "事件類型:",
"action": "操作:",
"resource": "資源:",
"user": "使用者:",
"ipAddress": "IP 位址:",
"sensitivity": "敏感度:",
"time": "時間:",
"changes": "變更內容",
"field": "欄位",
"oldValue": "舊值",
"newValue": "新值",
"checksum": "校驗碼:",
"na": "無",
"system": "系統"
},
"verification": {
"title": "完整性驗證",
"verifying": "正在驗證稽核日誌完整性...",
"verifyingHint": "根據記錄數量,這可能需要一些時間。",
"failed": "驗證失敗",
"statusSuccess": "完整性已驗證",
"statusFailed": "偵測到完整性問題",
"successDescription": "所有稽核記錄都具有有效的校驗碼,未被竄改。",
"failedDescription": "部分稽核記錄的校驗碼無效,表示可能存在竄改或損壞。",
"totalChecked": "已檢查總數",
"valid": "有效",
"invalid": "無效",
"invalidRecords": "無效記錄",
"invalidRecordsDescription": "以下記錄 ID 未通過完整性驗證:"
}
}
}

View File

@@ -21,7 +21,8 @@
"remove": "移除",
"view": "檢視",
"download": "下載",
"upload": "上傳"
"upload": "上傳",
"retry": "重試"
},
"labels": {
"loading": "載入中...",
@@ -135,5 +136,60 @@
"maxFileSize": "檔案大小上限:{{size}}",
"uploading": "正在上傳 {{filename}} ({{current}}/{{total}})...",
"uploadFailed": "上傳失敗"
},
"reports": {
"weeklyPreview": {
"title": "週報預覽",
"generateNow": "立即產生",
"generating": "產生中...",
"loading": "載入報告預覽中...",
"noData": "無可用的報告資料",
"noProjects": "找不到專案",
"projects": "專案",
"retry": "重試",
"completedShort": "已完成",
"done": "完成",
"inProgressLower": "進行中",
"overdueLower": "逾期",
"blockedLower": "受阻",
"nextWeekLower": "下週",
"unassigned": "未指派",
"due": "截止",
"since": "自",
"noReasonProvided": "未提供原因",
"daysOverdue": "逾期 {{count}} 天",
"status": {
"completed": "已完成",
"inProgress": "進行中",
"overdue": "逾期",
"blocked": "受阻",
"nextWeek": "下週",
"total": "總計"
},
"errors": {
"loadFailed": "載入報告預覽失敗",
"generateFailed": "產生報告失敗"
},
"messages": {
"generateSuccess": "報告已產生並發送通知!"
}
},
"history": {
"title": "報告歷史",
"loading": "載入歷史中...",
"retry": "重試",
"empty": "找不到報告歷史記錄。報告會在每週五 16:00 自動產生。",
"totalReports": "{{count}} 份報告",
"completed": "已完成",
"inProgress": "進行中",
"overdue": "逾期",
"status": {
"sent": "已發送",
"failed": "失敗"
},
"errors": {
"loadFailed": "載入報告歷史失敗"
}
}
}
}

View File

@@ -68,5 +68,8 @@
"normal": "正常:< 80%",
"warning": "警告80% - 99%",
"overloaded": "超載:≥ 100%"
},
"errors": {
"loadFailed": "載入工作負載資料失敗,請再試一次。"
}
}

View File

@@ -4,6 +4,7 @@ import { AttachmentVersionHistory } from './AttachmentVersionHistory'
import { ConfirmModal } from './ConfirmModal'
import { useToast } from '../contexts/ToastContext'
import { SkeletonList } from './Skeleton'
import { logger } from '../utils/logger'
interface AttachmentListProps {
taskId: string
@@ -30,7 +31,7 @@ export function AttachmentList({ taskId, onRefresh }: AttachmentListProps) {
const response = await attachmentService.listAttachments(taskId)
setAttachments(response.attachments)
} catch (error) {
console.error('Failed to load attachments:', error)
logger.error('Failed to load attachments:', error)
} finally {
setLoading(false)
}
@@ -44,7 +45,7 @@ export function AttachmentList({ taskId, onRefresh }: AttachmentListProps) {
try {
await attachmentService.downloadAttachment(attachment.id)
} catch (error) {
console.error('Failed to download attachment:', error)
logger.error('Failed to download attachment:', error)
showToast('Failed to download file', 'error')
}
}
@@ -61,7 +62,7 @@ export function AttachmentList({ taskId, onRefresh }: AttachmentListProps) {
onRefresh?.()
showToast('File deleted successfully', 'success')
} catch (error) {
console.error('Failed to delete attachment:', error)
logger.error('Failed to delete attachment:', error)
showToast('Failed to delete file', 'error')
} finally {
setDeleting(null)

View File

@@ -1,6 +1,7 @@
import { useState, useRef, useEffect, DragEvent, ChangeEvent } from 'react'
import { useTranslation } from 'react-i18next'
import { attachmentService } from '../services/attachments'
import { logger } from '../utils/logger'
// Spinner animation keyframes - injected once via useEffect
const SPINNER_KEYFRAMES_ID = 'attachment-upload-spinner-keyframes'
@@ -93,7 +94,7 @@ export function AttachmentUpload({ taskId, onUploadComplete }: AttachmentUploadP
setUploadProgress(null)
onUploadComplete?.()
} catch (err: unknown) {
console.error('Upload failed:', err)
logger.error('Upload failed:', err)
const errorMessage = err instanceof Error ? err.message : t('attachments.uploadFailed')
setError(errorMessage)
} finally {

View File

@@ -1,5 +1,6 @@
import { useState, useEffect, useCallback, useRef } from 'react'
import { attachmentService, AttachmentVersion, VersionHistoryResponse } from '../services/attachments'
import { logger } from '../utils/logger'
interface AttachmentVersionHistoryProps {
attachmentId: string
@@ -50,7 +51,7 @@ export function AttachmentVersionHistory({
const response: VersionHistoryResponse = await attachmentService.getVersionHistory(attachmentId)
setVersions(response.versions)
} catch (err) {
console.error('Failed to load version history:', err)
logger.error('Failed to load version history:', err)
setError('Failed to load version history')
} finally {
setLoading(false)
@@ -72,7 +73,7 @@ export function AttachmentVersionHistory({
onRestore()
onClose()
} catch (err) {
console.error('Failed to restore version:', err)
logger.error('Failed to restore version:', err)
setError('Failed to restore version. Please try again.')
} finally {
setRestoring(null)
@@ -83,7 +84,7 @@ export function AttachmentVersionHistory({
try {
await attachmentService.downloadAttachment(attachmentId, version)
} catch (err) {
console.error('Failed to download version:', err)
logger.error('Failed to download version:', err)
setError('Failed to download version')
}
}

View File

@@ -7,6 +7,7 @@ import interactionPlugin from '@fullcalendar/interaction'
import { EventClickArg, EventDropArg, DatesSetArg } from '@fullcalendar/core'
import api from '../services/api'
import { Skeleton } from './Skeleton'
import { logger } from '../utils/logger'
interface Task {
id: string
@@ -109,7 +110,7 @@ export function CalendarView({
Array.from(uniqueAssignees.entries()).map(([id, name]) => ({ id, name }))
)
} catch (err) {
console.error('Failed to load assignees:', err)
logger.error('Failed to load assignees:', err)
}
}
@@ -169,7 +170,7 @@ export function CalendarView({
setEvents(calendarEvents)
} catch (err) {
console.error('Failed to load tasks:', err)
logger.error('Failed to load tasks:', err)
} finally {
setLoading(false)
}
@@ -277,7 +278,7 @@ export function CalendarView({
onTaskUpdate()
}
}
console.error('Failed to update task date:', err)
logger.error('Failed to update task date:', err)
// Rollback on error
dropInfo.revert()
}

View File

@@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next'
import { customFieldsApi, CustomField, FieldType } from '../services/customFields'
import { CustomFieldEditor } from './CustomFieldEditor'
import { useToast } from '../contexts/ToastContext'
import { logger } from '../utils/logger'
interface CustomFieldListProps {
projectId: string
@@ -39,7 +40,7 @@ export function CustomFieldList({ projectId }: CustomFieldListProps) {
const response = await customFieldsApi.getCustomFields(projectId)
setFields(response.fields)
} catch (err) {
console.error('Failed to load custom fields:', err)
logger.error('Failed to load custom fields:', err)
setError(t('customFields.loadError'))
} finally {
setLoading(false)

View File

@@ -1,4 +1,5 @@
import React, { Component, ErrorInfo, ReactNode } from 'react'
import { logger } from '../utils/logger'
// Error logging service - can be extended to send to external service
export interface ErrorLog {
@@ -25,13 +26,12 @@ export function logError(error: Error, errorInfo: ErrorInfo): ErrorLog {
errorLogs.push(log)
// Log to console for debugging
console.group('ErrorBoundary caught an error')
console.error('Error:', error)
console.error('Component Stack:', errorInfo.componentStack)
console.error('Timestamp:', log.timestamp.toISOString())
console.error('URL:', log.url)
console.groupEnd()
// Log to console for debugging (always logged as these are critical errors)
logger.error('ErrorBoundary caught an error')
logger.error('Error:', error)
logger.error('Component Stack:', errorInfo.componentStack)
logger.error('Timestamp:', log.timestamp.toISOString())
logger.error('URL:', log.url)
// In production, could send to error tracking service
// sendToErrorTrackingService(log)

View File

@@ -5,6 +5,7 @@ import api from '../services/api'
import { dependenciesApi, TaskDependency, DependencyType } from '../services/dependencies'
import { CircularDependencyError, parseCircularError } from './CircularDependencyError'
import { escapeHtml } from '../utils/escapeHtml'
import { logger } from '../utils/logger'
interface CycleDetails {
cycle: string[]
@@ -91,7 +92,7 @@ export function GanttChart({
const deps = await dependenciesApi.getProjectDependencies(projectId)
setDependencies(deps)
} catch (err) {
console.error('Failed to load dependencies:', err)
logger.error('Failed to load dependencies:', err)
}
}
@@ -283,7 +284,7 @@ export function GanttChart({
await api.patch(`/tasks/${taskId}`, payload)
onTaskUpdate()
} catch (err: unknown) {
console.error('Failed to update task dates:', err)
logger.error('Failed to update task dates:', err)
const error = err as { response?: { status?: number; data?: { detail?: string | { message?: string } } } }
// Handle 409 Conflict - version mismatch
if (error.response?.status === 409) {
@@ -306,8 +307,8 @@ export function GanttChart({
// Handle progress change
const handleProgressChange = async (taskId: string, progress: number) => {
// Progress changes could update task status in the future
// For now, just log it
console.log(`Task ${taskId} progress changed to ${progress}%`)
// For now, just log it for debugging
logger.debug(`Task ${taskId} progress changed to ${progress}%`)
}
// Add dependency
@@ -328,7 +329,7 @@ export function GanttChart({
setSelectedPredecessor('')
setSelectedDependencyType('FS')
} catch (err: unknown) {
console.error('Failed to add dependency:', err)
logger.error('Failed to add dependency:', err)
const error = err as { response?: { data?: { detail?: unknown } } }
const errorDetail = error.response?.data?.detail
@@ -353,7 +354,7 @@ export function GanttChart({
await dependenciesApi.removeDependency(dependencyId)
await loadDependencies()
} catch (err) {
console.error('Failed to remove dependency:', err)
logger.error('Failed to remove dependency:', err)
setError('Failed to remove dependency')
}
}

View File

@@ -5,6 +5,7 @@ import { useToast } from '../contexts/ToastContext'
import { Skeleton } from './Skeleton'
import { ConfirmModal } from './ConfirmModal'
import { AddMemberModal } from './AddMemberModal'
import { logger } from '../utils/logger'
interface ProjectMemberListProps {
projectId: string
@@ -28,7 +29,7 @@ export function ProjectMemberList({ projectId }: ProjectMemberListProps) {
const response = await projectMembersApi.list(projectId)
setMembers(response.members)
} catch (err) {
console.error('Failed to load project members:', err)
logger.error('Failed to load project members:', err)
setError(t('members.loadError'))
} finally {
setLoading(false)
@@ -47,7 +48,7 @@ export function ProjectMemberList({ projectId }: ProjectMemberListProps) {
setIsAddModalOpen(false)
showToast(t('members.memberAdded'), 'success')
} catch (err: unknown) {
console.error('Failed to add member:', err)
logger.error('Failed to add member:', err)
const errorMessage = err instanceof Error ? err.message : t('members.addError')
const axiosError = err as { response?: { data?: { detail?: string } } }
if (axiosError.response?.data?.detail) {
@@ -70,7 +71,7 @@ export function ProjectMemberList({ projectId }: ProjectMemberListProps) {
setMemberToRemove(null)
showToast(t('messages.memberRemoved'), 'success')
} catch (err) {
console.error('Failed to remove member:', err)
logger.error('Failed to remove member:', err)
showToast(t('members.removeError'), 'error')
} finally {
setActionLoading(false)
@@ -94,7 +95,7 @@ export function ProjectMemberList({ projectId }: ProjectMemberListProps) {
setEditingMemberId(null)
showToast(t('messages.roleChanged'), 'success')
} catch (err) {
console.error('Failed to update member role:', err)
logger.error('Failed to update member role:', err)
showToast(t('members.roleChangeError'), 'error')
} finally {
setActionLoading(false)

View File

@@ -1,7 +1,9 @@
import { useState, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { reportsApi, ReportHistoryItem } from '../services/reports'
export function ReportHistory() {
const { t, i18n } = useTranslation()
const [reports, setReports] = useState<ReportHistoryItem[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
@@ -15,7 +17,7 @@ export function ReportHistory() {
setTotal(data.total)
setError(null)
} catch {
setError('Failed to load report history')
setError(t('reports.history.errors.loadFailed'))
} finally {
setLoading(false)
}
@@ -26,7 +28,7 @@ export function ReportHistory() {
}, [])
const formatDate = (dateStr: string) => {
return new Date(dateStr).toLocaleString('zh-TW', {
return new Date(dateStr).toLocaleString(i18n.language, {
year: 'numeric',
month: 'short',
day: 'numeric',
@@ -36,7 +38,7 @@ export function ReportHistory() {
}
if (loading) {
return <div className="p-4 text-center text-gray-500">Loading history...</div>
return <div className="p-4 text-center text-gray-500">{t('reports.history.loading')}</div>
}
if (error) {
@@ -44,7 +46,7 @@ export function ReportHistory() {
<div className="p-4 text-center text-red-500">
{error}
<button onClick={fetchHistory} className="ml-2 text-blue-600 hover:underline">
Retry
{t('reports.history.retry')}
</button>
</div>
)
@@ -53,7 +55,7 @@ export function ReportHistory() {
if (reports.length === 0) {
return (
<div className="p-4 text-center text-gray-500">
No report history found. Reports are generated every Friday at 16:00.
{t('reports.history.empty')}
</div>
)
}
@@ -61,8 +63,8 @@ export function ReportHistory() {
return (
<div className="space-y-3">
<div className="flex justify-between items-center">
<h3 className="text-lg font-medium">Report History</h3>
<span className="text-sm text-gray-500">{total} reports</span>
<h3 className="text-lg font-medium">{t('reports.history.title')}</h3>
<span className="text-sm text-gray-500">{t('reports.history.totalReports', { count: total })}</span>
</div>
<div className="space-y-2">
@@ -80,9 +82,9 @@ export function ReportHistory() {
<p className="font-medium">{formatDate(report.generated_at)}</p>
{report.status === 'sent' && summary && (
<p className="text-sm text-gray-600 mt-1">
Completed: {summary.completed_count || 0} |
In Progress: {summary.in_progress_count || 0} |
Overdue: {summary.overdue_count || 0}
{t('reports.history.completed')}: {summary.completed_count || 0} |
{t('reports.history.inProgress')}: {summary.in_progress_count || 0} |
{t('reports.history.overdue')}: {summary.overdue_count || 0}
</p>
)}
{report.status === 'failed' && report.error_message && (
@@ -94,7 +96,7 @@ export function ReportHistory() {
? 'bg-green-100 text-green-800'
: 'bg-red-100 text-red-800'
}`}>
{report.status}
{t(`reports.history.status.${report.status}`)}
</span>
</div>
</div>

View File

@@ -1,5 +1,6 @@
import { useState, useEffect, useCallback } from 'react'
import { auditService, AuditLog } from '../services/audit'
import { logger } from '../utils/logger'
interface ResourceHistoryProps {
resourceType: string
@@ -18,7 +19,7 @@ export function ResourceHistory({ resourceType, resourceId, title = 'Change Hist
const response = await auditService.getResourceHistory(resourceType, resourceId, 10)
setLogs(response.logs)
} catch (error) {
console.error('Failed to load resource history:', error)
logger.error('Failed to load resource history:', error)
} finally {
setLoading(false)
}

View File

@@ -1,6 +1,7 @@
import { useState, useEffect, useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import api from '../services/api'
import { logger } from '../utils/logger'
interface Subtask {
id: string
@@ -44,7 +45,7 @@ export function SubtaskList({
setSubtasks(response.data.tasks || [])
setError(null)
} catch (err) {
console.error('Failed to fetch subtasks:', err)
logger.error('Failed to fetch subtasks:', err)
setError(t('subtasks.error.load'))
} finally {
setLoading(false)
@@ -71,7 +72,7 @@ export function SubtaskList({
fetchSubtasks()
onSubtaskCreated?.()
} catch (err: unknown) {
console.error('Failed to create subtask:', err)
logger.error('Failed to create subtask:', err)
const axiosError = err as { response?: { data?: { detail?: string } } }
const errorMessage = axiosError.response?.data?.detail || t('subtasks.error.create')
setError(errorMessage)

View File

@@ -9,6 +9,7 @@ import { UserSearchResult } from '../services/collaboration'
import { customFieldsApi, CustomField, CustomValueResponse } from '../services/customFields'
import { CustomFieldInput } from './CustomFieldInput'
import { SkeletonList } from './Skeleton'
import { logger } from '../utils/logger'
interface Task {
id: string
@@ -88,7 +89,7 @@ export function TaskDetailModal({
const response = await customFieldsApi.getCustomFields(task.project_id)
setCustomFields(response.fields)
} catch (err) {
console.error('Failed to load custom fields:', err)
logger.error('Failed to load custom fields:', err)
} finally {
setLoadingCustomFields(false)
}
@@ -222,7 +223,7 @@ export function TaskDetailModal({
}
}
}
console.error('Failed to update task:', err)
logger.error('Failed to update task:', err)
} finally {
setSaving(false)
}

View File

@@ -1,5 +1,6 @@
import { useState, useEffect, useRef, useCallback } from 'react'
import { usersApi, UserSearchResult } from '../services/collaboration'
import { logger } from '../utils/logger'
interface UserSelectProps {
value: string | null
@@ -46,7 +47,7 @@ export function UserSelect({
const results = await usersApi.search(query)
setUsers(results)
} catch (err) {
console.error('Failed to search users:', err)
logger.error('Failed to search users:', err)
setUsers([])
} finally {
setLoading(false)

View File

@@ -1,4 +1,5 @@
import { useState, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { reportsApi, WeeklyReportContent, ProjectSummary } from '../services/reports'
import { useToast } from '../contexts/ToastContext'
@@ -53,9 +54,11 @@ function TaskItem({ title, subtitle, highlight }: TaskItemProps) {
}
function ProjectCard({ project }: { project: ProjectSummary }) {
const { t, i18n } = useTranslation()
const formatDate = (dateStr: string | null) => {
if (!dateStr) return ''
return new Date(dateStr).toLocaleDateString('zh-TW', { month: 'short', day: 'numeric' })
return new Date(dateStr).toLocaleDateString(i18n.language, { month: 'short', day: 'numeric' })
}
return (
@@ -63,78 +66,78 @@ function ProjectCard({ project }: { project: ProjectSummary }) {
<div className="flex justify-between items-center mb-2">
<h5 className="font-medium">{project.project_title}</h5>
<span className="text-sm text-gray-500">
{project.completed_count}/{project.total_tasks} completed
{project.completed_count}/{project.total_tasks} {t('reports.weeklyPreview.completedShort')}
</span>
</div>
{/* Summary row */}
<div className="flex flex-wrap gap-3 text-sm mb-2">
<span className="text-green-600">{project.completed_count} done</span>
<span className="text-blue-600">{project.in_progress_count} in progress</span>
<span className="text-green-600">{project.completed_count} {t('reports.weeklyPreview.done')}</span>
<span className="text-blue-600">{project.in_progress_count} {t('reports.weeklyPreview.inProgressLower')}</span>
{project.overdue_count > 0 && (
<span className="text-red-600">{project.overdue_count} overdue</span>
<span className="text-red-600">{project.overdue_count} {t('reports.weeklyPreview.overdueLower')}</span>
)}
{project.blocked_count > 0 && (
<span className="text-orange-600">{project.blocked_count} blocked</span>
<span className="text-orange-600">{project.blocked_count} {t('reports.weeklyPreview.blockedLower')}</span>
)}
{project.next_week_count > 0 && (
<span className="text-purple-600">{project.next_week_count} next week</span>
<span className="text-purple-600">{project.next_week_count} {t('reports.weeklyPreview.nextWeekLower')}</span>
)}
</div>
{/* Completed Tasks */}
<CollapsibleSection title="Completed" count={project.completed_count} colorClass="text-green-700">
<CollapsibleSection title={t('reports.weeklyPreview.status.completed')} count={project.completed_count} colorClass="text-green-700">
{project.completed_tasks.map(task => (
<TaskItem
key={task.id}
title={task.title}
subtitle={`${task.assignee_name || 'Unassigned'}${formatDate(task.completed_at)}`}
subtitle={`${task.assignee_name || t('reports.weeklyPreview.unassigned')}${formatDate(task.completed_at)}`}
/>
))}
</CollapsibleSection>
{/* In Progress Tasks */}
<CollapsibleSection title="In Progress" count={project.in_progress_count} colorClass="text-blue-700">
<CollapsibleSection title={t('reports.weeklyPreview.status.inProgress')} count={project.in_progress_count} colorClass="text-blue-700">
{project.in_progress_tasks.map(task => (
<TaskItem
key={task.id}
title={task.title}
subtitle={`${task.assignee_name || 'Unassigned'}${task.due_date ? `Due ${formatDate(task.due_date)}` : ''}`}
subtitle={`${task.assignee_name || t('reports.weeklyPreview.unassigned')}${task.due_date ? `${t('reports.weeklyPreview.due')} ${formatDate(task.due_date)}` : ''}`}
/>
))}
</CollapsibleSection>
{/* Overdue Tasks */}
<CollapsibleSection title="Overdue" count={project.overdue_count} colorClass="text-red-700" defaultOpen>
<CollapsibleSection title={t('reports.weeklyPreview.status.overdue')} count={project.overdue_count} colorClass="text-red-700" defaultOpen>
{project.overdue_tasks.map(task => (
<TaskItem
key={task.id}
title={task.title}
subtitle={`${task.assignee_name || 'Unassigned'}${task.days_overdue} days overdue`}
subtitle={`${task.assignee_name || t('reports.weeklyPreview.unassigned')}${t('reports.weeklyPreview.daysOverdue', { count: task.days_overdue })}`}
highlight="overdue"
/>
))}
</CollapsibleSection>
{/* Blocked Tasks */}
<CollapsibleSection title="Blocked" count={project.blocked_count} colorClass="text-orange-700" defaultOpen>
<CollapsibleSection title={t('reports.weeklyPreview.status.blocked')} count={project.blocked_count} colorClass="text-orange-700" defaultOpen>
{project.blocked_tasks.map(task => (
<TaskItem
key={task.id}
title={task.title}
subtitle={`${task.blocker_reason || 'No reason provided'}${task.blocked_since ? `Since ${formatDate(task.blocked_since)}` : ''}`}
subtitle={`${task.blocker_reason || t('reports.weeklyPreview.noReasonProvided')}${task.blocked_since ? `${t('reports.weeklyPreview.since')} ${formatDate(task.blocked_since)}` : ''}`}
highlight="blocked"
/>
))}
</CollapsibleSection>
{/* Next Week Tasks */}
<CollapsibleSection title="Next Week" count={project.next_week_count} colorClass="text-purple-700">
<CollapsibleSection title={t('reports.weeklyPreview.status.nextWeek')} count={project.next_week_count} colorClass="text-purple-700">
{project.next_week_tasks.map(task => (
<TaskItem
key={task.id}
title={task.title}
subtitle={`${task.assignee_name || 'Unassigned'}Due ${formatDate(task.due_date)}`}
subtitle={`${task.assignee_name || t('reports.weeklyPreview.unassigned')}${t('reports.weeklyPreview.due')} ${formatDate(task.due_date)}`}
/>
))}
</CollapsibleSection>
@@ -143,6 +146,7 @@ function ProjectCard({ project }: { project: ProjectSummary }) {
}
export function WeeklyReportPreview() {
const { t, i18n } = useTranslation()
const { showToast } = useToast()
const [report, setReport] = useState<WeeklyReportContent | null>(null)
const [loading, setLoading] = useState(true)
@@ -156,7 +160,7 @@ export function WeeklyReportPreview() {
setReport(data)
setError(null)
} catch {
setError('Failed to load report preview')
setError(t('reports.weeklyPreview.errors.loadFailed'))
} finally {
setLoading(false)
}
@@ -170,17 +174,17 @@ export function WeeklyReportPreview() {
try {
setGenerating(true)
await reportsApi.generateWeeklyReport()
showToast('Report generated and notification sent!', 'success')
showToast(t('reports.weeklyPreview.messages.generateSuccess'), 'success')
fetchPreview()
} catch {
showToast('Failed to generate report', 'error')
showToast(t('reports.weeklyPreview.errors.generateFailed'), 'error')
} finally {
setGenerating(false)
}
}
if (loading) {
return <div className="p-4 text-center text-gray-500">Loading report preview...</div>
return <div className="p-4 text-center text-gray-500">{t('reports.weeklyPreview.loading')}</div>
}
if (error) {
@@ -188,18 +192,18 @@ export function WeeklyReportPreview() {
<div className="p-4 text-center text-red-500">
{error}
<button onClick={fetchPreview} className="ml-2 text-blue-600 hover:underline">
Retry
{t('reports.weeklyPreview.retry')}
</button>
</div>
)
}
if (!report) {
return <div className="p-4 text-center text-gray-500">No report data available</div>
return <div className="p-4 text-center text-gray-500">{t('reports.weeklyPreview.noData')}</div>
}
const formatDate = (dateStr: string) => {
return new Date(dateStr).toLocaleDateString('zh-TW', {
return new Date(dateStr).toLocaleDateString(i18n.language, {
month: 'short',
day: 'numeric',
})
@@ -209,7 +213,7 @@ export function WeeklyReportPreview() {
<div className="space-y-4">
<div className="flex justify-between items-center">
<div>
<h3 className="text-lg font-medium">Weekly Report Preview</h3>
<h3 className="text-lg font-medium">{t('reports.weeklyPreview.title')}</h3>
<p className="text-sm text-gray-500">
{formatDate(report.week_start)} - {formatDate(report.week_end)}
</p>
@@ -219,7 +223,7 @@ export function WeeklyReportPreview() {
disabled={generating}
className="px-4 py-2 text-white bg-blue-600 hover:bg-blue-700 rounded-md disabled:opacity-50"
>
{generating ? 'Generating...' : 'Generate Now'}
{generating ? t('reports.weeklyPreview.generating') : t('reports.weeklyPreview.generateNow')}
</button>
</div>
@@ -227,40 +231,40 @@ export function WeeklyReportPreview() {
<div className="grid grid-cols-3 sm:grid-cols-6 gap-3">
<div className="bg-green-50 border border-green-200 rounded-lg p-3">
<p className="text-2xl font-bold text-green-600">{report.summary.completed_count}</p>
<p className="text-xs text-green-800">Completed</p>
<p className="text-xs text-green-800">{t('reports.weeklyPreview.status.completed')}</p>
</div>
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3">
<p className="text-2xl font-bold text-blue-600">{report.summary.in_progress_count}</p>
<p className="text-xs text-blue-800">In Progress</p>
<p className="text-xs text-blue-800">{t('reports.weeklyPreview.status.inProgress')}</p>
</div>
<div className="bg-red-50 border border-red-200 rounded-lg p-3">
<p className="text-2xl font-bold text-red-600">{report.summary.overdue_count}</p>
<p className="text-xs text-red-800">Overdue</p>
<p className="text-xs text-red-800">{t('reports.weeklyPreview.status.overdue')}</p>
</div>
<div className="bg-orange-50 border border-orange-200 rounded-lg p-3">
<p className="text-2xl font-bold text-orange-600">{report.summary.blocked_count}</p>
<p className="text-xs text-orange-800">Blocked</p>
<p className="text-xs text-orange-800">{t('reports.weeklyPreview.status.blocked')}</p>
</div>
<div className="bg-purple-50 border border-purple-200 rounded-lg p-3">
<p className="text-2xl font-bold text-purple-600">{report.summary.next_week_count}</p>
<p className="text-xs text-purple-800">Next Week</p>
<p className="text-xs text-purple-800">{t('reports.weeklyPreview.status.nextWeek')}</p>
</div>
<div className="bg-gray-50 border border-gray-200 rounded-lg p-3">
<p className="text-2xl font-bold text-gray-600">{report.summary.total_tasks}</p>
<p className="text-xs text-gray-800">Total</p>
<p className="text-xs text-gray-800">{t('reports.weeklyPreview.status.total')}</p>
</div>
</div>
{/* Project Details */}
{report.projects.length > 0 ? (
<div className="space-y-3">
<h4 className="font-medium">Projects</h4>
<h4 className="font-medium">{t('reports.weeklyPreview.projects')}</h4>
{report.projects.map(project => (
<ProjectCard key={project.project_id} project={project} />
))}
</div>
) : (
<p className="text-gray-500 text-center py-4">No projects found</p>
<p className="text-gray-500 text-center py-4">{t('reports.weeklyPreview.noProjects')}</p>
)}
</div>
)

View File

@@ -1,6 +1,7 @@
import { useState, useEffect, useRef, useCallback } from 'react'
import { UserWorkloadDetail, LoadLevel, workloadApi } from '../services/workload'
import { SkeletonList } from './Skeleton'
import { logger } from '../utils/logger'
interface WorkloadUserDetailProps {
userId: string
@@ -62,7 +63,7 @@ export function WorkloadUserDetail({
const data = await workloadApi.getUserWorkload(userId, weekStart)
setDetail(data)
} catch (err) {
console.error('Failed to load user workload:', err)
logger.error('Failed to load user workload:', err)
setError('Failed to load workload details')
} finally {
setLoading(false)

View File

@@ -8,6 +8,7 @@ import {
getStoredToken,
isTokenExpired,
} from '../services/api'
import { logger } from '../utils/logger'
/**
* Validates that a parsed object has the required User properties.
@@ -89,7 +90,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
setUser(validatedUser)
} else {
// Invalid user data structure, clear storage and redirect to login
console.warn('Invalid user data in localStorage, clearing session')
logger.warn('Invalid user data in localStorage, clearing session')
clearStoredTokens()
// Don't redirect here as we're in initial loading state
// The app will naturally show login page when user is null
@@ -97,7 +98,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
}
} catch (err) {
// JSON parse error or other unexpected error
console.error('Error parsing stored user data:', err)
logger.error('Error parsing stored user data:', err)
clearStoredTokens()
}
}

View File

@@ -1,5 +1,6 @@
import { createContext, useContext, useState, useEffect, useCallback, ReactNode, useRef } from 'react'
import { notificationsApi, Notification, NotificationListResponse } from '../services/collaboration'
import { logger } from '../utils/logger'
interface NotificationContextType {
notifications: Notification[]
@@ -32,7 +33,7 @@ export function NotificationProvider({ children }: { children: ReactNode }) {
const count = await notificationsApi.getUnreadCount()
setUnreadCount(count)
} catch (err) {
console.error('Failed to fetch unread count:', err)
logger.error('Failed to fetch unread count:', err)
}
}, [])
@@ -45,7 +46,7 @@ export function NotificationProvider({ children }: { children: ReactNode }) {
setUnreadCount(response.unread_count)
} catch (err) {
setError('Failed to load notifications')
console.error('Failed to fetch notifications:', err)
logger.error('Failed to fetch notifications:', err)
} finally {
setLoading(false)
}
@@ -59,7 +60,7 @@ export function NotificationProvider({ children }: { children: ReactNode }) {
)
setUnreadCount(prev => Math.max(0, prev - 1))
} catch (err) {
console.error('Failed to mark as read:', err)
logger.error('Failed to mark as read:', err)
}
}, [])
@@ -71,7 +72,7 @@ export function NotificationProvider({ children }: { children: ReactNode }) {
)
setUnreadCount(0)
} catch (err) {
console.error('Failed to mark all as read:', err)
logger.error('Failed to mark all as read:', err)
}
}, [])
@@ -96,7 +97,7 @@ export function NotificationProvider({ children }: { children: ReactNode }) {
wsRef.current = ws
ws.onopen = () => {
console.log('WebSocket opened, sending authentication...')
logger.debug('WebSocket opened, sending authentication...')
// Send authentication message as first message (more secure than query parameter)
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'auth', token }))
@@ -116,7 +117,7 @@ export function NotificationProvider({ children }: { children: ReactNode }) {
break
case 'connected':
console.log('WebSocket authenticated:', message.data.message)
logger.debug('WebSocket authenticated:', message.data.message)
// Start ping interval after successful authentication
pingIntervalRef.current = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
@@ -169,12 +170,12 @@ export function NotificationProvider({ children }: { children: ReactNode }) {
break
}
} catch (err) {
console.error('Failed to parse WebSocket message:', err)
logger.error('Failed to parse WebSocket message:', err)
}
}
ws.onclose = () => {
console.log('WebSocket disconnected')
logger.debug('WebSocket disconnected')
// Clear ping interval
if (pingIntervalRef.current) {
clearInterval(pingIntervalRef.current)
@@ -192,10 +193,10 @@ export function NotificationProvider({ children }: { children: ReactNode }) {
}
ws.onerror = (err) => {
console.error('WebSocket error:', err)
logger.error('WebSocket error:', err)
}
} catch (err) {
console.error('Failed to create WebSocket:', err)
logger.error('Failed to create WebSocket:', err)
}
}, [])

View File

@@ -1,4 +1,5 @@
import React, { createContext, useContext, useEffect, useRef, useState, useCallback } from 'react'
import { logger } from '../utils/logger'
interface TaskEvent {
type: 'task_created' | 'task_updated' | 'task_status_changed' | 'task_deleted' | 'task_assigned'
@@ -47,18 +48,6 @@ const INITIAL_RECONNECT_DELAY = 1000 // 1 second
const MAX_RECONNECT_DELAY = 30000 // 30 seconds
const MAX_PROCESSED_EVENTS = 1000 // Limit memory usage for event tracking
// Development-only logging helper
const devLog = (...args: unknown[]) => {
if (import.meta.env.DEV) {
console.log(...args)
}
}
const devError = (...args: unknown[]) => {
if (import.meta.env.DEV) {
console.error(...args)
}
}
export function ProjectSyncProvider({ children }: { children: React.ReactNode }) {
const [isConnected, setIsConnected] = useState(false)
@@ -110,7 +99,7 @@ export function ProjectSyncProvider({ children }: { children: React.ReactNode })
const ws = new WebSocket(wsUrl)
ws.onopen = () => {
devLog(`WebSocket opened for project ${projectId}, sending auth...`)
logger.debug(`WebSocket opened for project ${projectId}, sending auth...`)
// Send authentication message as first message (more secure than query parameter)
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'auth', token }))
@@ -134,7 +123,7 @@ export function ProjectSyncProvider({ children }: { children: React.ReactNode })
reconnectAttemptsRef.current = 0 // Reset on successful connection
setIsConnected(true)
setCurrentProjectId(projectId)
devLog(`Authenticated and connected to project ${projectId} sync`)
logger.debug(`Authenticated and connected to project ${projectId} sync`)
// Start ping interval to keep connection alive
pingIntervalRef.current = setInterval(() => {
@@ -183,7 +172,7 @@ export function ProjectSyncProvider({ children }: { children: React.ReactNode })
listenersRef.current.forEach((listener) => listener(message as TaskEvent))
}
} catch (e) {
devError('Failed to parse WebSocket message:', e)
logger.error('Failed to parse WebSocket message:', e)
}
}
@@ -206,7 +195,7 @@ export function ProjectSyncProvider({ children }: { children: React.ReactNode })
MAX_RECONNECT_DELAY
)
reconnectAttemptsRef.current++
devLog(
logger.debug(
`Reconnecting in ${delay}ms (attempt ${reconnectAttemptsRef.current}/${MAX_RECONNECT_ATTEMPTS})`
)
@@ -216,17 +205,17 @@ export function ProjectSyncProvider({ children }: { children: React.ReactNode })
}
}, delay)
} else if (reconnectAttemptsRef.current >= MAX_RECONNECT_ATTEMPTS) {
devError('Max reconnection attempts reached. Please refresh the page.')
logger.error('Max reconnection attempts reached. Please refresh the page.')
}
}
ws.onerror = (error) => {
devError('WebSocket error:', error)
logger.error('WebSocket error:', error)
}
wsRef.current = ws
} catch (err) {
devError('Failed to create WebSocket:', err)
logger.error('Failed to create WebSocket:', err)
}
}, [cleanup])

View File

@@ -4,6 +4,7 @@ import { useAuth } from '../contexts/AuthContext'
import { SkeletonTable } from '../components/Skeleton'
import { auditService, AuditLog, AuditLogFilters, IntegrityCheckResponse } from '../services/audit'
import { escapeHtml } from '../utils/escapeHtml'
import { logger } from '../utils/logger'
interface AuditLogDetailProps {
log: AuditLog
@@ -11,6 +12,7 @@ interface AuditLogDetailProps {
}
function AuditLogDetail({ log, onClose }: AuditLogDetailProps) {
const { t, i18n } = useTranslation('audit')
const modalOverlayRef = useRef<HTMLDivElement>(null)
// Handle Escape key to close modal - document-level listener
@@ -41,49 +43,49 @@ function AuditLogDetail({ log, onClose }: AuditLogDetailProps) {
>
<div style={styles.modalContent}>
<div style={styles.modalHeader}>
<h3 id="audit-log-detail-title">Audit Log Details</h3>
<button onClick={onClose} style={styles.closeButton} aria-label="Close">×</button>
<h3 id="audit-log-detail-title">{t('modal.details.title')}</h3>
<button onClick={onClose} style={styles.closeButton} aria-label={t('common:buttons.close')}>×</button>
</div>
<div style={styles.modalBody}>
<div style={styles.detailRow}>
<span style={styles.label}>Event Type:</span>
<span style={styles.label}>{t('modal.details.eventType')}</span>
<span>{log.event_type}</span>
</div>
<div style={styles.detailRow}>
<span style={styles.label}>Action:</span>
<span style={styles.label}>{t('modal.details.action')}</span>
<span>{log.action}</span>
</div>
<div style={styles.detailRow}>
<span style={styles.label}>Resource:</span>
<span>{log.resource_type} / {log.resource_id || 'N/A'}</span>
<span style={styles.label}>{t('modal.details.resource')}</span>
<span>{log.resource_type} / {log.resource_id || t('modal.details.na')}</span>
</div>
<div style={styles.detailRow}>
<span style={styles.label}>User:</span>
<span>{log.user_name || log.user_id || 'System'}</span>
<span style={styles.label}>{t('modal.details.user')}</span>
<span>{log.user_name || log.user_id || t('modal.details.system')}</span>
</div>
<div style={styles.detailRow}>
<span style={styles.label}>IP Address:</span>
<span>{log.request_metadata?.ip_address || 'N/A'}</span>
<span style={styles.label}>{t('modal.details.ipAddress')}</span>
<span>{log.request_metadata?.ip_address || t('modal.details.na')}</span>
</div>
<div style={styles.detailRow}>
<span style={styles.label}>Sensitivity:</span>
<span style={styles.label}>{t('modal.details.sensitivity')}</span>
<span style={getSensitivityStyle(log.sensitivity_level)}>
{log.sensitivity_level}
</span>
</div>
<div style={styles.detailRow}>
<span style={styles.label}>Time:</span>
<span>{new Date(log.created_at).toLocaleString()}</span>
<span style={styles.label}>{t('modal.details.time')}</span>
<span>{new Date(log.created_at).toLocaleString(i18n.language)}</span>
</div>
{log.changes && log.changes.length > 0 && (
<div style={styles.changesSection}>
<h4>Changes</h4>
<h4>{t('modal.details.changes')}</h4>
<table style={styles.changesTable}>
<thead>
<tr>
<th>Field</th>
<th>Old Value</th>
<th>New Value</th>
<th>{t('modal.details.field')}</th>
<th>{t('modal.details.oldValue')}</th>
<th>{t('modal.details.newValue')}</th>
</tr>
</thead>
<tbody>
@@ -99,7 +101,7 @@ function AuditLogDetail({ log, onClose }: AuditLogDetailProps) {
</div>
)}
<div style={styles.detailRow}>
<span style={styles.label}>Checksum:</span>
<span style={styles.label}>{t('modal.details.checksum')}</span>
<span style={styles.checksum}>{log.checksum}</span>
</div>
</div>
@@ -133,6 +135,7 @@ interface IntegrityVerificationModalProps {
}
function IntegrityVerificationModal({ result, isLoading, error, onClose }: IntegrityVerificationModalProps) {
const { t } = useTranslation('audit')
const isSuccess = result && result.invalid_count === 0
const modalOverlayRef = useRef<HTMLDivElement>(null)
@@ -186,22 +189,22 @@ function IntegrityVerificationModal({ result, isLoading, error, onClose }: Integ
>
<div style={styles.modalContent}>
<div style={styles.modalHeader}>
<h3 id="integrity-verification-title">Integrity Verification</h3>
<button onClick={onClose} style={styles.closeButton} aria-label="Close">x</button>
<h3 id="integrity-verification-title">{t('modal.verification.title')}</h3>
<button onClick={onClose} style={styles.closeButton} aria-label={t('common:buttons.close')}>x</button>
</div>
<div style={styles.modalBody}>
{isLoading && (
<div style={styles.loadingContainer}>
<div className="integrity-spinner" style={styles.spinner}></div>
<p style={styles.loadingText}>Verifying audit log integrity...</p>
<p style={styles.loadingSubtext}>This may take a moment depending on the number of records.</p>
<p style={styles.loadingText}>{t('modal.verification.verifying')}</p>
<p style={styles.loadingSubtext}>{t('modal.verification.verifyingHint')}</p>
</div>
)}
{error && (
<div style={styles.errorContainer}>
<div style={styles.errorIcon}>!</div>
<h4 style={styles.errorTitle}>Verification Failed</h4>
<h4 style={styles.errorTitle}>{t('modal.verification.failed')}</h4>
<p style={styles.errorMessage}>{error}</p>
</div>
)}
@@ -225,15 +228,15 @@ function IntegrityVerificationModal({ result, isLoading, error, onClose }: Integ
...styles.statusTitle,
color: isSuccess ? '#155724' : '#721c24',
}}>
{isSuccess ? 'Integrity Verified' : 'Integrity Issues Detected'}
{isSuccess ? t('modal.verification.statusSuccess') : t('modal.verification.statusFailed')}
</h4>
<p style={{
...styles.statusDescription,
color: isSuccess ? '#155724' : '#721c24',
}}>
{isSuccess
? 'All audit records have valid checksums and have not been tampered with.'
: 'Some audit records have invalid checksums, indicating potential tampering or corruption.'}
? t('modal.verification.successDescription')
: t('modal.verification.failedDescription')}
</p>
</div>
</div>
@@ -242,26 +245,26 @@ function IntegrityVerificationModal({ result, isLoading, error, onClose }: Integ
<div style={styles.statsContainer}>
<div style={styles.statBox}>
<span style={styles.statValue}>{result.total_checked}</span>
<span style={styles.statLabel}>Total Checked</span>
<span style={styles.statLabel}>{t('modal.verification.totalChecked')}</span>
</div>
<div style={{ ...styles.statBox, backgroundColor: '#e8f5e9' }}>
<span style={{ ...styles.statValue, color: '#28a745' }}>{result.valid_count}</span>
<span style={styles.statLabel}>Valid</span>
<span style={styles.statLabel}>{t('modal.verification.valid')}</span>
</div>
<div style={{ ...styles.statBox, backgroundColor: result.invalid_count > 0 ? '#ffebee' : '#f5f5f5' }}>
<span style={{ ...styles.statValue, color: result.invalid_count > 0 ? '#dc3545' : '#666' }}>
{result.invalid_count}
</span>
<span style={styles.statLabel}>Invalid</span>
<span style={styles.statLabel}>{t('modal.verification.invalid')}</span>
</div>
</div>
{/* Invalid Records List */}
{result.invalid_records && result.invalid_records.length > 0 && (
<div style={styles.invalidRecordsSection}>
<h4 style={styles.invalidRecordsTitle}>Invalid Records</h4>
<h4 style={styles.invalidRecordsTitle}>{t('modal.verification.invalidRecords')}</h4>
<p style={styles.invalidRecordsDescription}>
The following record IDs failed integrity verification:
{t('modal.verification.invalidRecordsDescription')}
</p>
<div style={styles.invalidRecordsList}>
{result.invalid_records.map((recordId, index) => (
@@ -312,7 +315,7 @@ export default function AuditPage() {
setLogs(response.logs)
setTotal(response.total)
} catch (error) {
console.error('Failed to load audit logs:', error)
logger.error('Failed to load audit logs:', error)
} finally {
setLoading(false)
}
@@ -344,7 +347,7 @@ export default function AuditPage() {
window.URL.revokeObjectURL(url)
document.body.removeChild(a)
} catch (error) {
console.error('Failed to export audit logs:', error)
logger.error('Failed to export audit logs:', error)
}
}
@@ -352,7 +355,7 @@ export default function AuditPage() {
// Create a printable version of the audit logs
const printWindow = window.open('', '_blank')
if (!printWindow) {
console.error('Failed to open print window. Please allow popups.')
logger.error('Failed to open print window. Please allow popups.')
return
}
@@ -448,7 +451,7 @@ export default function AuditPage() {
const result = await auditService.verifyIntegrity(startDate, endDate)
setVerificationResult(result)
} catch (error: unknown) {
console.error('Failed to verify integrity:', error)
logger.error('Failed to verify integrity:', error)
const err = error as { response?: { data?: { detail?: string } }; message?: string }
setVerificationError(
err.response?.data?.detail ||

View File

@@ -9,6 +9,7 @@ import {
QuickActions,
} from '../components/dashboard'
import { Skeleton } from '../components/Skeleton'
import { logger } from '../utils/logger'
export default function Dashboard() {
const { t } = useTranslation('dashboard')
@@ -25,7 +26,7 @@ export default function Dashboard() {
setData(response)
} catch (err) {
setError(t('common:messages.networkError'))
console.error('Dashboard fetch error:', err)
logger.error('Dashboard fetch error:', err)
} finally {
setLoading(false)
}

View File

@@ -4,6 +4,7 @@ import { useAuth } from '../contexts/AuthContext'
import { useToast } from '../contexts/ToastContext'
import api from '../services/api'
import { Skeleton } from '../components/Skeleton'
import { logger } from '../utils/logger'
interface UserProfile {
id: string
@@ -47,7 +48,7 @@ export default function MySettings() {
})
setCapacity(String(userData.capacity || 40))
} catch (err) {
console.error('Failed to load profile:', err)
logger.error('Failed to load profile:', err)
setError(t('common:messages.error'))
} finally {
setLoading(false)
@@ -60,7 +61,7 @@ export default function MySettings() {
const response = await api.get('/reports/weekly/subscription')
setWeeklySubscription(Boolean(response.data?.is_active))
} catch (err) {
console.error('Failed to load weekly subscription:', err)
logger.error('Failed to load weekly subscription:', err)
showToast(t('mySettings.weeklyReportError'), 'error')
} finally {
setSubscriptionLoading(false)
@@ -85,7 +86,7 @@ export default function MySettings() {
setProfile({ ...profile, capacity: capacityValue })
showToast(t('mySettings.capacitySaved'), 'success')
} catch (err) {
console.error('Failed to save capacity:', err)
logger.error('Failed to save capacity:', err)
showToast(t('mySettings.capacityError'), 'error')
} finally {
setSaving(false)
@@ -102,7 +103,7 @@ export default function MySettings() {
'success'
)
} catch (err) {
console.error('Failed to update weekly subscription:', err)
logger.error('Failed to update weekly subscription:', err)
showToast(t('mySettings.weeklyReportError'), 'error')
} finally {
setSubscriptionSaving(false)

View File

@@ -9,6 +9,7 @@ import {
ProjectHealthItem,
RiskLevel,
} from '../services/projectHealth'
import { logger } from '../utils/logger'
type SortOption = 'risk_high' | 'risk_low' | 'health_high' | 'health_low' | 'name'
@@ -35,7 +36,7 @@ export default function ProjectHealthPage() {
const data = await projectHealthApi.getDashboard()
setDashboardData(data)
} catch (err) {
console.error('Failed to load project health dashboard:', err)
logger.error('Failed to load project health dashboard:', err)
setError(t('error.loadFailed'))
} finally {
setLoading(false)

View File

@@ -6,6 +6,7 @@ import { CustomFieldList } from '../components/CustomFieldList'
import { ProjectMemberList } from '../components/ProjectMemberList'
import { useToast } from '../contexts/ToastContext'
import { Skeleton } from '../components/Skeleton'
import { logger } from '../utils/logger'
interface Project {
id: string
@@ -34,7 +35,7 @@ export default function ProjectSettings() {
const response = await api.get(`/projects/${projectId}`)
setProject(response.data)
} catch (err) {
console.error('Failed to load project:', err)
logger.error('Failed to load project:', err)
showToast(t('common:messages.error'), 'error')
} finally {
setLoading(false)

View File

@@ -5,6 +5,7 @@ import api from '../services/api'
import { SkeletonGrid } from '../components/Skeleton'
import { useToast } from '../contexts/ToastContext'
import { useAuth } from '../contexts/AuthContext'
import { logger } from '../utils/logger'
interface Project {
id: string
@@ -115,7 +116,7 @@ export default function Projects() {
setSpace(spaceRes.data)
setProjects(projectsRes.data)
} catch (err) {
console.error('Failed to load data:', err)
logger.error('Failed to load data:', err)
showToast(t('messages.loadFailed'), 'error')
} finally {
setLoading(false)
@@ -129,7 +130,7 @@ export default function Projects() {
// API returns {templates: [], total: number}
setTemplates(response.data.templates || [])
} catch (err) {
console.error('Failed to load templates:', err)
logger.error('Failed to load templates:', err)
showToast(t('template.loadFailed'), 'error')
} finally {
setTemplatesLoading(false)
@@ -165,7 +166,7 @@ export default function Projects() {
loadData()
showToast(t('messages.created'), 'success')
} catch (err) {
console.error('Failed to create project:', err)
logger.error('Failed to create project:', err)
showToast(t('messages.createFailed'), 'error')
} finally {
setCreating(false)
@@ -189,7 +190,7 @@ export default function Projects() {
loadData()
showToast(t('messages.deleted'), 'success')
} catch (err) {
console.error('Failed to delete project:', err)
logger.error('Failed to delete project:', err)
showToast(t('common:messages.error'), 'error')
} finally {
setDeleting(false)

View File

@@ -5,6 +5,7 @@ import api from '../services/api'
import { useToast } from '../contexts/ToastContext'
import { useAuth } from '../contexts/AuthContext'
import { SkeletonGrid } from '../components/Skeleton'
import { logger } from '../utils/logger'
interface Space {
id: string
@@ -69,7 +70,7 @@ export default function Spaces() {
const response = await api.get('/spaces')
setSpaces(response.data)
} catch (err) {
console.error('Failed to load spaces:', err)
logger.error('Failed to load spaces:', err)
showToast(t('common:messages.error'), 'error')
} finally {
setLoading(false)
@@ -87,7 +88,7 @@ export default function Spaces() {
loadSpaces()
showToast(t('messages.created'), 'success')
} catch (err) {
console.error('Failed to create space:', err)
logger.error('Failed to create space:', err)
showToast(t('common:messages.error'), 'error')
} finally {
setCreating(false)
@@ -111,7 +112,7 @@ export default function Spaces() {
loadSpaces()
showToast(t('messages.deleted'), 'success')
} catch (err) {
console.error('Failed to delete space:', err)
logger.error('Failed to delete space:', err)
showToast(t('common:messages.error'), 'error')
} finally {
setDeleting(false)

View File

@@ -12,6 +12,7 @@ import { useProjectSync, TaskEvent } from '../contexts/ProjectSyncContext'
import { customFieldsApi, CustomField, CustomValueResponse } from '../services/customFields'
import { CustomFieldInput } from '../components/CustomFieldInput'
import { SkeletonTable, SkeletonKanban, Skeleton } from '../components/Skeleton'
import { logger } from '../utils/logger'
interface Task {
id: string
@@ -135,7 +136,7 @@ export default function Tasks() {
const response = await customFieldsApi.getCustomFields(projectId!)
setCustomFields(response.fields)
} catch (err) {
console.error('Failed to load custom fields:', err)
logger.error('Failed to load custom fields:', err)
}
}
@@ -315,7 +316,7 @@ export default function Tasks() {
setTasks(tasksRes.data.tasks)
setStatuses(statusesRes.data)
} catch (err) {
console.error('Failed to load data:', err)
logger.error('Failed to load data:', err)
} finally {
setLoading(false)
}
@@ -377,7 +378,7 @@ export default function Tasks() {
setSelectedAssignee(null)
loadData()
} catch (err) {
console.error('Failed to create task:', err)
logger.error('Failed to create task:', err)
} finally {
setCreating(false)
}
@@ -417,7 +418,7 @@ export default function Tasks() {
} catch (err) {
// Rollback on error
setTasks(originalTasks)
console.error('Failed to update status:', err)
logger.error('Failed to update status:', err)
// Could add toast notification here for better UX
}
}
@@ -455,7 +456,7 @@ export default function Tasks() {
time_estimate: updatedTask.original_estimate,
})
} catch (err) {
console.error('Failed to refresh selected task:', err)
logger.error('Failed to refresh selected task:', err)
}
}
}
@@ -474,7 +475,7 @@ export default function Tasks() {
setSelectedTask(subtaskWithProject)
// Modal is already open, just update the task
} catch (err) {
console.error('Failed to load subtask:', err)
logger.error('Failed to load subtask:', err)
}
}

View File

@@ -4,6 +4,7 @@ import { WorkloadHeatmap } from '../components/WorkloadHeatmap'
import { WorkloadUserDetail } from '../components/WorkloadUserDetail'
import { SkeletonTable } from '../components/Skeleton'
import { workloadApi, WorkloadHeatmapResponse } from '../services/workload'
import { logger } from '../utils/logger'
// Helper to get Monday of a given week
function getMonday(date: Date): Date {
@@ -49,8 +50,8 @@ export default function WorkloadPage() {
const data = await workloadApi.getHeatmap(formatDateParam(selectedWeek), !showAllUsers)
setHeatmapData(data)
} catch (err) {
console.error('Failed to load workload heatmap:', err)
setError('Failed to load workload data. Please try again.')
logger.error('Failed to load workload heatmap:', err)
setError(t('errors.loadFailed'))
} finally {
setLoading(false)
}

View File

@@ -1,4 +1,5 @@
import axios, { InternalAxiosRequestConfig, AxiosError } from 'axios'
import { logger } from '../utils/logger'
// API base URL - using legacy routes until v1 migration is complete
// TODO: Switch to /api/v1 when all routes are migrated
@@ -175,7 +176,7 @@ async function fetchCsrfToken(): Promise<string | null> {
csrfTokenExpiry = Date.now() + CSRF_TOKEN_LIFETIME_MS
return csrfToken
} catch (error) {
console.error('Failed to fetch CSRF token:', error)
logger.error('Failed to fetch CSRF token:', error)
return null
}
}

View File

@@ -0,0 +1,14 @@
/**
* Environment-aware logging utility.
* - debug/info: Only log in development mode
* - warn/error: Always log (needed for production debugging)
*/
const isDev = import.meta.env.DEV
export const logger = {
debug: (...args: unknown[]) => isDev && console.log('[DEBUG]', ...args),
info: (...args: unknown[]) => isDev && console.info('[INFO]', ...args),
warn: (...args: unknown[]) => console.warn('[WARN]', ...args),
error: (...args: unknown[]) => console.error('[ERROR]', ...args),
}

View File

@@ -1,28 +1,28 @@
## 1. Create Logging Utility
- [ ] 1.1 Create `utils/logger.ts` with environment-aware logging
- [ ] 1.2 Export debug, info, warn, error functions
- [ ] 1.3 Only output logs when `import.meta.env.DEV` is true
- [x] 1.1 Create `utils/logger.ts` with environment-aware logging
- [x] 1.2 Export debug, info, warn, error functions
- [x] 1.3 Only output logs when `import.meta.env.DEV` is true
## 2. Cleanup Context Files
- [ ] 2.1 Remove/replace console statements in NotificationContext.tsx
- [ ] 2.2 Remove/replace console statements in ProjectSyncContext.tsx
- [ ] 2.3 Remove/replace console statements in AuthContext.tsx
- [x] 2.1 Remove/replace console statements in NotificationContext.tsx
- [x] 2.2 Remove/replace console statements in ProjectSyncContext.tsx
- [x] 2.3 Remove/replace console statements in AuthContext.tsx
## 3. Cleanup Component Files
- [ ] 3.1 Remove/replace console statements in GanttChart.tsx
- [ ] 3.2 Remove/replace console statements in CalendarView.tsx
- [ ] 3.3 Audit and clean remaining components
- [x] 3.1 Remove/replace console statements in GanttChart.tsx
- [x] 3.2 Remove/replace console statements in CalendarView.tsx
- [x] 3.3 Audit and clean remaining components
## 4. Cleanup Page Files
- [ ] 4.1 Remove/replace console statements in Tasks.tsx
- [ ] 4.2 Remove/replace console statements in other page files
- [ ] 4.3 Audit and clean remaining pages
- [x] 4.1 Remove/replace console statements in Tasks.tsx
- [x] 4.2 Remove/replace console statements in other page files
- [x] 4.3 Audit and clean remaining pages
## 5. Cleanup Service Files
- [ ] 5.1 Remove/replace console statements in api.ts
- [ ] 5.2 Audit and clean remaining services
- [x] 5.1 Remove/replace console statements in api.ts
- [x] 5.2 Audit and clean remaining services
## 6. Verification
- [ ] 6.1 Run frontend build successfully
- [ ] 6.2 Verify no console.log in production build
- [ ] 6.3 Test error handling still works correctly
- [x] 6.1 Run frontend build successfully
- [x] 6.2 Verify no console.log in production build
- [x] 6.3 Test error handling still works correctly

View File

@@ -1,30 +1,30 @@
## 1. WeeklyReportPreview i18n
- [ ] 1.1 Add translation keys for report labels and status names
- [ ] 1.2 Add translation keys for error and loading messages
- [ ] 1.3 Replace hardcoded strings with t() calls
- [ ] 1.4 Use dynamic locale for date formatting
- [x] 1.1 Add translation keys for report labels and status names
- [x] 1.2 Add translation keys for error and loading messages
- [x] 1.3 Replace hardcoded strings with t() calls
- [x] 1.4 Use dynamic locale for date formatting
## 2. ReportHistory i18n
- [ ] 2.1 Add translation keys for history labels
- [ ] 2.2 Add translation keys for loading/empty states
- [ ] 2.3 Replace hardcoded strings with t() calls
- [x] 2.1 Add translation keys for history labels
- [x] 2.2 Add translation keys for loading/empty states
- [x] 2.3 Replace hardcoded strings with t() calls
## 3. AuditPage Modal i18n
- [ ] 3.1 Add translation keys for detail modal labels
- [ ] 3.2 Add translation keys for verification modal
- [ ] 3.3 Add translation keys for field names and status messages
- [ ] 3.4 Replace hardcoded strings with t() calls
- [x] 3.1 Add translation keys for detail modal labels
- [x] 3.2 Add translation keys for verification modal
- [x] 3.3 Add translation keys for field names and status messages
- [x] 3.4 Replace hardcoded strings with t() calls
## 4. WorkloadPage i18n
- [ ] 4.1 Replace hardcoded error message with t() call
- [x] 4.1 Replace hardcoded error message with t() call
## 5. Update Locale Files
- [ ] 5.1 Add all new keys to en/common.json
- [ ] 5.2 Add all new keys to zh-TW/common.json
- [ ] 5.3 Verify key structure consistency
- [x] 5.1 Add all new keys to en/common.json
- [x] 5.2 Add all new keys to zh-TW/common.json
- [x] 5.3 Verify key structure consistency
## 6. Verification
- [ ] 6.1 Test English locale displays correctly
- [ ] 6.2 Test Chinese locale displays correctly
- [ ] 6.3 Verify no untranslated strings remain
- [ ] 6.4 Frontend builds successfully
- [x] 6.1 Test English locale displays correctly
- [x] 6.2 Test Chinese locale displays correctly
- [x] 6.3 Verify no untranslated strings remain
- [x] 6.4 Frontend builds successfully