@@ -272,7 +301,19 @@ export default function AnalysisContent({ showHeader = false }:{ showHeader?: bo
diff --git a/front/app/boss/analysis/AnalysisContent.tsx b/front/app/boss/analysis/AnalysisContent.tsx
index 36af561..c43e851 100644
--- a/front/app/boss/analysis/AnalysisContent.tsx
+++ b/front/app/boss/analysis/AnalysisContent.tsx
@@ -102,6 +102,8 @@ export default function AnalysisContent({
handleConfirmBatch,
handleConfirmAiRecommendedBatch,
handleConfirmManualBatch,
+ handleReconcileJob,
+ handleRetryJob,
handleSkipJob,
clearAnalysisData,
} = useBossDeliveryActions({
@@ -287,6 +289,8 @@ export default function AnalysisContent({
selectedManualJobIds={selectedManualJobIds}
onOpenText={openTextDialog}
onConfirmJob={handleConfirmJob}
+ onReconcileJob={handleReconcileJob}
+ onRetryJob={handleRetryJob}
onSkipJob={handleSkipJob}
onLoadList={loadList}
onInputPageChange={setInputPage}
diff --git a/front/app/boss/analysis/components/BossJobTable.tsx b/front/app/boss/analysis/components/BossJobTable.tsx
index 3034f05..1aabd23 100644
--- a/front/app/boss/analysis/components/BossJobTable.tsx
+++ b/front/app/boss/analysis/components/BossJobTable.tsx
@@ -23,6 +23,8 @@ export function BossJobTable({
selectedManualJobIds,
onOpenText,
onConfirmJob,
+ onReconcileJob,
+ onRetryJob,
onSkipJob,
onLoadList,
onInputPageChange,
@@ -44,6 +46,8 @@ export function BossJobTable({
selectedManualJobIds: ReadonlySet
onOpenText: (title: string, content?: string) => void
onConfirmJob: (job: BossJob) => void
+ onReconcileJob: (job: BossJob) => void
+ onRetryJob: (job: BossJob) => void
onSkipJob: (job: BossJob) => void
onLoadList: (page: number, size: number) => void
onInputPageChange: (value: number | string) => void
@@ -190,15 +194,30 @@ export function BossJobTable({
{(page - 1) * size + idx + 1}
- {job.deliveryStatus === "待确认" ? (
+ {job.deliveryStatus === "待确认" || job.deliveryStatus === "投递确认中" ? (
-
+ ) : job.deliveryStatus === "投递结果待确认" ? (
+
+ onReconcileJob(job)} className="h-7 w-full rounded px-2 text-xs leading-none">
+ 对账
+
+ onRetryJob(job)} className="h-7 w-full rounded px-2 text-xs leading-none">
+ 重试
+ ) : job.deliveryStatus === "投递失败" ? (
+ onRetryJob(job)} className="h-7 w-full rounded px-2 text-xs leading-none">
+ 重试
+
) : (job.deliveryStatus || "").includes("已投递") ? (
diff --git a/front/app/boss/analysis/hooks/useBossDeliveryActions.ts b/front/app/boss/analysis/hooks/useBossDeliveryActions.ts
index a695fea..6442b00 100644
--- a/front/app/boss/analysis/hooks/useBossDeliveryActions.ts
+++ b/front/app/boss/analysis/hooks/useBossDeliveryActions.ts
@@ -6,6 +6,72 @@ import { API_BASE } from "@/lib/api"
import { sendChromeBridgeMessage } from "@/lib/chromeBridge"
import type { BossJob, FilterState } from "../types"
+type ReservedTask = { id?: number; requestKey?: string }
+
+async function postJsonWithRetry(url: string, body?: unknown) {
+ let lastError: unknown
+ for (let attempt = 0; attempt < 2; attempt += 1) {
+ try {
+ const response = await fetch(url, {
+ method: "POST",
+ headers: body === undefined ? undefined : { "Content-Type": "application/json" },
+ body: body === undefined ? undefined : JSON.stringify(body),
+ })
+ return await response.json()
+ } catch (error) {
+ lastError = error
+ }
+ }
+ throw lastError instanceof Error ? lastError : new Error("投递请求未收到响应")
+}
+
+function unresolvedReservations(tasks: ReservedTask[], result: Record) {
+ const rows = Array.isArray(result.results) ? result.results : []
+ if (rows.length === 0) return tasks
+ const persistedKeys = new Set(rows.map((row) => {
+ if (!row || typeof row !== "object") return ""
+ const item = row as { requestKey?: unknown; persisted?: unknown }
+ return item.persisted === true ? String(item.requestKey || "") : ""
+ }))
+ return tasks.filter((task) => !task.requestKey || !persistedKeys.has(task.requestKey))
+}
+
+function formatBatchDeliveryResult(result: Record) {
+ const summary = String(result.message || "批量投递任务已结束。")
+ const rows = Array.isArray(result.results) ? result.results : []
+ if (rows.length === 0) return summary
+ const details = rows.slice(0, 50).map((row, index) => {
+ const item = row && typeof row === "object"
+ ? row as { id?: unknown; requestKey?: unknown; outcome?: unknown; evidence?: unknown; persisted?: unknown; message?: unknown }
+ : {}
+ const persisted = item.persisted === true ? "已落库" : "待补偿"
+ return `${index + 1}. 岗位 ${String(item.id || "-")} · ${String(item.outcome || "UNKNOWN")} · ${persisted} · ${String(item.evidence || "-")}\n${String(item.message || "")}`
+ })
+ return `${summary}\n\n逐条结果:\n${details.join("\n")}`
+}
+
+async function markUnknownReservations(tasks: ReservedTask[], reason: string) {
+ const results = await Promise.allSettled(tasks.map(async (task) => {
+ if (!task.id || !task.requestKey) return
+ const response = await fetch(`${API_BASE}/api/boss/jobs/${task.id}/delivery-result`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ requestKey: task.requestKey,
+ outcome: "UNKNOWN",
+ evidence: "NO_CONFIRMATION",
+ message: reason,
+ }),
+ })
+ const data = await response.json().catch(() => ({}))
+ if (!response.ok || data.success === false) {
+ throw new Error(data.message || "Boss UNKNOWN 状态回写失败")
+ }
+ }))
+ const failed = results.filter((result) => result.status === "rejected")
+ if (failed.length > 0) console.error("Boss UNKNOWN 状态回写失败", failed)
+}
+
export function useBossDeliveryActions({
filters,
activeScanRunId,
@@ -57,31 +123,88 @@ export function useBossDeliveryActions({
}, [openTextDialog])
const handleConfirmJob = useCallback(async (job: BossJob) => {
+ let reservedTasks: ReservedTask[] = []
try {
setActingJobId(job.id)
- const res = await fetch(`${API_BASE}/api/boss/jobs/${job.id}/confirm`, { method: "POST" })
- const data = await res.json()
+ const ok = window.confirm(`将通过 Chrome 真实联系 Boss HR:${job.companyName || ""} / ${job.jobName || ""}。确认继续?`)
+ if (!ok) return
+ const data = await postJsonWithRetry(`${API_BASE}/api/boss/jobs/${job.id}/confirm`)
if (!data.success) {
openTextDialog("确认投递", data.message || "该岗位暂不能投递。")
return
}
- const ok = window.confirm(`将通过 Chrome 真实联系 Boss HR:${job.companyName || ""} / ${job.jobName || ""}。确认继续?`)
- if (!ok) return
+ reservedTasks = [data.task]
const result = await sendChromeBridgeMessage({
type: "BOSS_DELIVER_ONE",
platform: "boss",
task: data.task,
}, 120000)
+ if (result.persisted !== true) {
+ await markUnknownReservations(reservedTasks, result.message || "Chrome Bridge 未返回岗位结果")
+ }
openTextDialog("确认投递", result.message || (result.success ? "已发送投递请求。" : "Chrome投递失败。"))
await loadList(page, size)
await refreshStats()
} catch {
+ await markUnknownReservations(reservedTasks, "前端未收到 Chrome 投递执行结果")
openTextDialog("待确认发送", "确认失败:网络或服务异常。")
} finally {
setActingJobId(null)
}
}, [loadList, openTextDialog, page, refreshStats, size])
+ const handleReconcileJob = useCallback(async (job: BossJob) => {
+ const answer = window.prompt(
+ "请先在 Boss 平台核对该岗位。输入“已投递”确认成功,输入“未投递”确认失败;其他内容不会修改状态。",
+ )?.trim()
+ if (answer !== "已投递" && answer !== "未投递") return
+ try {
+ setActingJobId(job.id)
+ const data = await postJsonWithRetry(`${API_BASE}/api/boss/jobs/${job.id}/delivery-reconcile`, {
+ outcome: answer === "已投递" ? "CONFIRMED" : "FAILED",
+ message: `用户在 Boss 平台人工核对:${answer}`,
+ })
+ openTextDialog("人工对账", data.message || (data.success ? "人工对账已保存。" : "人工对账失败。"))
+ await loadList(page, size)
+ await refreshStats()
+ } catch {
+ openTextDialog("人工对账", "人工对账失败:网络或服务异常。")
+ } finally {
+ setActingJobId(null)
+ }
+ }, [loadList, openTextDialog, page, refreshStats, size])
+
+ const handleRetryJob = useCallback(async (job: BossJob) => {
+ let reservedTasks: ReservedTask[] = []
+ const ok = window.confirm("这会创建新的投递 attempt,并可能再次联系该 Boss HR。确认显式重试?")
+ if (!ok) return
+ try {
+ setActingJobId(job.id)
+ const data = await postJsonWithRetry(`${API_BASE}/api/boss/jobs/${job.id}/delivery-retry`)
+ if (!data.success || !data.task) {
+ openTextDialog("重试投递", data.message || "当前岗位不能重试。")
+ return
+ }
+ reservedTasks = [data.task]
+ const result = await sendChromeBridgeMessage({
+ type: "BOSS_DELIVER_ONE",
+ platform: "boss",
+ task: data.task,
+ }, 120000)
+ if (result.persisted !== true) {
+ await markUnknownReservations(reservedTasks, result.message || "Chrome 重试结果未确认写入")
+ }
+ openTextDialog("重试投递", result.message || "重试任务已结束。")
+ await loadList(page, size)
+ await refreshStats()
+ } catch {
+ await markUnknownReservations(reservedTasks, "前端未收到 Chrome 重试执行结果")
+ openTextDialog("重试投递", "重试失败:网络或服务异常,已保守标记待对账。")
+ } finally {
+ setActingJobId(null)
+ }
+ }, [loadList, openTextDialog, page, refreshStats, size])
+
const currentBatchFilters = useCallback(() => ({
location: filters.location || undefined,
experience: filters.experience || undefined,
@@ -95,30 +218,32 @@ export function useBossDeliveryActions({
}), [activeScanRunId, filters])
const handleConfirmBatch = useCallback(async () => {
+ let reservedTasks: ReservedTask[] = []
try {
setActingBatch(true)
- const res = await fetch(`${API_BASE}/api/boss/jobs/confirm-batch`, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify(currentBatchFilters()),
- })
- const data = await res.json()
+ const ok = window.confirm("将通过 Chrome 真实联系当前筛选范围内的 Boss 待确认岗位。确认继续?")
+ if (!ok) return
+ const data = await postJsonWithRetry(`${API_BASE}/api/boss/jobs/confirm-batch`, currentBatchFilters())
const tasks = data.tasks || []
+ reservedTasks = tasks
if (!data.success || tasks.length === 0) {
openTextDialog("批量投递", data.message || "当前筛选条件下没有待确认岗位。")
return
}
- const ok = window.confirm(`将通过 Chrome 真实联系 ${tasks.length} 个 Boss 待确认岗位。确认继续?`)
- if (!ok) return
const result = await sendChromeBridgeMessage({
type: "BOSS_DELIVER_BATCH",
platform: "boss",
tasks,
}, Math.max(120000, tasks.length * 30000))
- openTextDialog("批量投递", result.message || "批量投递任务已结束。")
+ const unresolved = unresolvedReservations(reservedTasks, result)
+ if (unresolved.length > 0) {
+ await markUnknownReservations(unresolved, result.message || "Chrome Bridge 未确认写入完整批量结果")
+ }
+ openTextDialog("批量投递", formatBatchDeliveryResult(result))
await loadList(page, size)
await refreshStats()
} catch {
+ await markUnknownReservations(reservedTasks, "前端未收到 Chrome 批量投递执行结果")
openTextDialog("批量投递", "批量投递失败:网络或服务异常。")
} finally {
setActingBatch(false)
@@ -126,30 +251,35 @@ export function useBossDeliveryActions({
}, [currentBatchFilters, loadList, openTextDialog, page, refreshStats, size])
const handleConfirmAiRecommendedBatch = useCallback(async () => {
+ let reservedTasks: ReservedTask[] = []
try {
setActingAiBatch(true)
- const res = await fetch(`${API_BASE}/api/boss/jobs/confirm-batch`, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ aiRecommendedOnly: true, scanRunId: activeScanRunId || undefined }),
+ const ok = window.confirm("将通过 Chrome 真实联系当前批次中 AI 推荐的 Boss 待确认岗位。确认继续?")
+ if (!ok) return
+ const data = await postJsonWithRetry(`${API_BASE}/api/boss/jobs/confirm-batch`, {
+ aiRecommendedOnly: true,
+ scanRunId: activeScanRunId || undefined,
})
- const data = await res.json()
const tasks = data.tasks || []
+ reservedTasks = tasks
if (!data.success || tasks.length === 0) {
openTextDialog("AI推荐一键投递", data.message || "当前没有 AI 推荐的待确认岗位。")
return
}
- const ok = window.confirm(`将通过 Chrome 真实联系 ${tasks.length} 个 Boss AI推荐待确认岗位。确认继续?`)
- if (!ok) return
const result = await sendChromeBridgeMessage({
type: "BOSS_DELIVER_BATCH",
platform: "boss",
tasks,
}, Math.max(120000, tasks.length * 30000))
- openTextDialog("AI推荐一键投递", result.message || "AI推荐批量投递任务已结束。")
+ const unresolved = unresolvedReservations(reservedTasks, result)
+ if (unresolved.length > 0) {
+ await markUnknownReservations(unresolved, result.message || "Chrome Bridge 未确认写入完整批量结果")
+ }
+ openTextDialog("AI推荐一键投递", formatBatchDeliveryResult(result))
await loadList(page, size)
await refreshStats()
} catch {
+ await markUnknownReservations(reservedTasks, "前端未收到 Chrome AI 推荐批量投递结果")
openTextDialog("AI推荐一键投递", "AI推荐批量投递失败:网络或服务异常。")
} finally {
setActingAiBatch(false)
@@ -163,39 +293,40 @@ export function useBossDeliveryActions({
return false
}
+ let reservedTasks: ReservedTask[] = []
try {
setActingManualBatch(true)
- const res = await fetch(`${API_BASE}/api/boss/jobs/confirm-batch`, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({
- ids: uniqueIds,
- manualOverrideAiNotMatch: true,
- }),
+ const ok = window.confirm(
+ `AI 已将这些岗位判定为不匹配。你正在按人工判断强制投递 ${uniqueIds.length} 个岗位,`
+ + "将通过 Chrome 真实联系 Boss HR。确认继续?",
+ )
+ if (!ok) return false
+ const data = await postJsonWithRetry(`${API_BASE}/api/boss/jobs/confirm-batch`, {
+ ids: uniqueIds,
+ manualOverrideAiNotMatch: true,
})
- const data = await res.json()
const tasks = data.tasks || []
+ reservedTasks = tasks
if (!data.success || tasks.length === 0) {
openTextDialog("人工投递", data.message || "所选岗位中没有可人工投递的AI不匹配岗位。")
return false
}
- const ok = window.confirm(
- `AI 已将这些岗位判定为不匹配。你正在按人工判断强制投递 ${tasks.length} 个岗位,`
- + `将通过 Chrome 真实联系 Boss HR。确认继续?${data.message ? `\n\n${data.message}` : ""}`,
- )
- if (!ok) return false
-
const result = await sendChromeBridgeMessage({
type: "BOSS_DELIVER_BATCH",
platform: "boss",
tasks,
}, Math.max(120000, tasks.length * 30000))
- openTextDialog("人工投递", result.message || "人工批量投递任务已结束。")
+ const unresolved = unresolvedReservations(reservedTasks, result)
+ if (unresolved.length > 0) {
+ await markUnknownReservations(unresolved, result.message || "Chrome Bridge 未确认写入完整批量结果")
+ }
+ openTextDialog("人工投递", formatBatchDeliveryResult(result))
await loadList(page, size)
await refreshStats()
return true
} catch {
+ await markUnknownReservations(reservedTasks, "前端未收到 Chrome 人工批量投递结果")
openTextDialog("人工投递", "人工批量投递失败:网络或服务异常。")
return false
} finally {
@@ -254,6 +385,8 @@ export function useBossDeliveryActions({
handleConfirmBatch,
handleConfirmAiRecommendedBatch,
handleConfirmManualBatch,
+ handleReconcileJob,
+ handleRetryJob,
handleSkipJob,
clearAnalysisData,
}
diff --git a/front/app/boss/analysis/types.ts b/front/app/boss/analysis/types.ts
index 96b067f..c444f2d 100644
--- a/front/app/boss/analysis/types.ts
+++ b/front/app/boss/analysis/types.ts
@@ -93,7 +93,7 @@ export type FilterState = {
filterHeadhunter: boolean
}
-export const DELIVERY_STATUS_OPTIONS = ["待确认", "LIST_COLLECTED", "AI分析中", "已投递", "未投递", "AI不匹配", "AI分析失败", "采集信息不足", "已过滤", "已跳过", "投递失败"]
+export const DELIVERY_STATUS_OPTIONS = ["待确认", "投递确认中", "投递结果待确认", "LIST_COLLECTED", "AI分析中", "已投递", "未投递", "AI不匹配", "AI分析失败", "采集信息不足", "已过滤", "已跳过", "投递失败"]
export const EXPERIENCE_OPTIONS = ["在校/应届", "1年以内", "1-3年", "3-5年", "5-10年", "10年以上"]
export const DEGREE_OPTIONS = ["不限", "中专/中技", "高中", "大专", "本科", "硕士", "博士"]
diff --git a/front/app/boss/analysis/utils.ts b/front/app/boss/analysis/utils.ts
index d39c20e..6ced181 100644
--- a/front/app/boss/analysis/utils.ts
+++ b/front/app/boss/analysis/utils.ts
@@ -59,6 +59,8 @@ export function badgeClass(kind: "delivery" | "hr" | "recruitment", value?: stri
if (kind === "delivery") {
if (v.includes("已投递")) return `${base} bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300`
if (v.includes("待确认")) return `${base} bg-cyan-100 text-cyan-700 dark:bg-cyan-900/30 dark:text-cyan-300`
+ if (v.includes("投递确认中")) return `${base} bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300`
+ if (v.includes("投递结果待确认")) return `${base} bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300`
if (v === "LIST_COLLECTED") return `${base} bg-teal-100 text-teal-700 dark:bg-teal-900/30 dark:text-teal-300`
if (v.includes("AI分析中")) return `${base} bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300`
if (v.includes("采集信息不足")) return `${base} bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-300`
diff --git a/front/app/components/Sidebar.tsx b/front/app/components/Sidebar.tsx
index 128a990..fb49ada 100644
--- a/front/app/components/Sidebar.tsx
+++ b/front/app/components/Sidebar.tsx
@@ -96,9 +96,9 @@ export default function Sidebar() {
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), 3000)
try {
- let res = await fetch(`${API_BASE}/api/health`, { signal: controller.signal })
+ let res = await fetch(`${API_BASE}/api/ready`, { signal: controller.signal })
if (res.status === 404) {
- res = await fetch(`${API_BASE}/actuator/health`, { signal: controller.signal })
+ res = await fetch(`${API_BASE}/api/health`, { signal: controller.signal })
}
if (!res.ok) throw new Error(`status ${res.status}`)
const data = await res.json()
diff --git a/front/app/env-config/page.tsx b/front/app/env-config/page.tsx
index 5f1ae22..17790c7 100644
--- a/front/app/env-config/page.tsx
+++ b/front/app/env-config/page.tsx
@@ -12,6 +12,11 @@ import { API_BASE } from '@/lib/api'
export default function EnvConfig() {
const [envConfig, setEnvConfig] = useState({
hookUrl: '',
+ aiProvider: 'codex',
+ codexPath: 'codex',
+ codexModel: 'gpt-5.6-sol',
+ codexTimeoutSeconds: '300',
+ apiTimeoutSeconds: '120',
baseUrl: '',
apiKey: '',
model: '',
@@ -19,6 +24,8 @@ export default function EnvConfig() {
})
const [showApiKey, setShowApiKey] = useState(false)
+ const [sensitiveConfigured, setSensitiveConfigured] = useState({ hookUrl: false, apiKey: false })
+ const [sensitiveDirty, setSensitiveDirty] = useState({ hookUrl: false, apiKey: false })
const [loading, setLoading] = useState(true)
const [saving, setSaving] = useState(false)
const [showSaveDialog, setShowSaveDialog] = useState(false)
@@ -43,9 +50,14 @@ export default function EnvConfig() {
if (result.success && result.data) {
setEnvConfig({
- hookUrl: result.data.HOOK_URL || '',
+ hookUrl: '',
+ aiProvider: result.data.AI_PROVIDER === 'api' || result.data.AI_PROVIDER === 'remote' ? 'api' : 'codex',
+ codexPath: result.data.CODEX_PATH || 'codex',
+ codexModel: result.data.CODEX_MODEL || 'gpt-5.6-sol',
+ codexTimeoutSeconds: result.data.CODEX_TIMEOUT_SECONDS || '300',
+ apiTimeoutSeconds: result.data.AI_REQUEST_TIMEOUT_SECONDS || '120',
baseUrl: result.data.BASE_URL || '',
- apiKey: result.data.API_KEY || '',
+ apiKey: '',
model: result.data.MODEL || '',
botIsSend: (() => {
const raw = result.data.BOT_IS_SEND
@@ -53,6 +65,11 @@ export default function EnvConfig() {
return val === '1' || val === 'true' ? 1 : 0
})(),
})
+ setSensitiveConfigured({
+ hookUrl: result.sensitive?.HOOK_URL === true,
+ apiKey: result.sensitive?.API_KEY === true,
+ })
+ setSensitiveDirty({ hookUrl: false, apiKey: false })
}
} catch (error) {
console.error('获取配置失败:', error)
@@ -70,13 +87,22 @@ export default function EnvConfig() {
try {
setSaving(true)
- const configMap = {
- HOOK_URL: envConfig.hookUrl,
+ const configMap: Record = {
+ AI_PROVIDER: envConfig.aiProvider,
+ CODEX_PATH: envConfig.codexPath,
+ CODEX_MODEL: envConfig.codexModel,
+ CODEX_TIMEOUT_SECONDS: envConfig.codexTimeoutSeconds,
+ AI_REQUEST_TIMEOUT_SECONDS: envConfig.apiTimeoutSeconds,
BASE_URL: envConfig.baseUrl,
- API_KEY: envConfig.apiKey,
MODEL: envConfig.model,
BOT_IS_SEND: String(envConfig.botIsSend ?? 0),
}
+ if (sensitiveDirty.hookUrl && envConfig.hookUrl.trim()) {
+ configMap.HOOK_URL = envConfig.hookUrl.trim()
+ }
+ if (sensitiveDirty.apiKey && envConfig.apiKey.trim()) {
+ configMap.API_KEY = envConfig.apiKey.trim()
+ }
const response = await fetch(`${API_BASE}/api/config`, {
method: 'POST',
@@ -93,6 +119,12 @@ export default function EnvConfig() {
const result = await response.json()
if (result.success) {
+ setSensitiveConfigured((current) => ({
+ hookUrl: sensitiveDirty.hookUrl && envConfig.hookUrl.trim() ? true : current.hookUrl,
+ apiKey: sensitiveDirty.apiKey && envConfig.apiKey.trim() ? true : current.apiKey,
+ }))
+ setSensitiveDirty({ hookUrl: false, apiKey: false })
+ setEnvConfig((current) => ({ ...current, hookUrl: '', apiKey: '' }))
if (!silent) {
setSaveResult({ success: true, message: '保存成功' })
setShowSaveDialog(true)
@@ -111,6 +143,39 @@ export default function EnvConfig() {
}
}
+ const clearSensitiveConfig = async (key: 'HOOK_URL' | 'API_KEY') => {
+ const label = key === 'HOOK_URL' ? 'Webhook URL' : 'API Key'
+ if (!window.confirm(`确定清除已保存的 ${label} 吗?清除后相关功能将无法使用,直到重新填写。`)) {
+ return
+ }
+
+ try {
+ setSaving(true)
+ const response = await fetch(`${API_BASE}/api/config/${key}`, { method: 'DELETE' })
+ const result = await response.json()
+ if (!response.ok || !result.success) {
+ throw new Error(result.message || '清除失败')
+ }
+ if (key === 'HOOK_URL') {
+ setSensitiveConfigured((current) => ({ ...current, hookUrl: result.configured === true }))
+ setSensitiveDirty((current) => ({ ...current, hookUrl: false }))
+ setEnvConfig((current) => ({ ...current, hookUrl: '' }))
+ } else {
+ setSensitiveConfigured((current) => ({ ...current, apiKey: result.configured === true }))
+ setSensitiveDirty((current) => ({ ...current, apiKey: false }))
+ setEnvConfig((current) => ({ ...current, apiKey: '' }))
+ }
+ setSaveResult({ success: true, message: result.message || `${label} 已清除` })
+ setShowSaveDialog(true)
+ } catch (error) {
+ console.error('清除敏感配置失败:', error)
+ setSaveResult({ success: false, message: `${label} 清除失败,请检查后端服务。` })
+ setShowSaveDialog(true)
+ } finally {
+ setSaving(false)
+ }
+ }
+
return (
Webhook URL
setEnvConfig({ ...envConfig, hookUrl: e.target.value })}
- placeholder="https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=your_key"
+ onChange={(e) => {
+ setEnvConfig({ ...envConfig, hookUrl: e.target.value })
+ setSensitiveDirty({ ...sensitiveDirty, hookUrl: true })
+ }}
+ placeholder={sensitiveConfigured.hookUrl ? '已配置;输入新值可替换' : 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=your_key'}
/>
-
- 企业微信群机器人webhook地址,用于接收通知消息
-
+
+
+ {sensitiveConfigured.hookUrl ? '已配置,页面不会读取或显示原值。' : '尚未配置企业微信 Webhook。'}
+
+ {sensitiveConfigured.hookUrl && (
+ clearSensitiveConfig('HOOK_URL')}>
+ 清除已保存值
+
+ )}
+
- {/* API 配置 */}
+ {/* AI 调用方式 */}
- API 配置
+ AI 调用方式
- 配置 API 服务器地址和使用的 AI 模型
+ 本机默认复用 Codex/ChatGPT 登录态;需要时仍可手动切回远程 API
+
+
+
+
+
+
+
+ 下面的远程 API 配置仅在 Provider 选择“远程 API”时使用。
@@ -211,6 +315,18 @@ export default function EnvConfig() {
/>
DeepSeek 推荐模型 deepseek-chat;也可以填写其他 OpenAI-compatible 模型名
+
+
+ setEnvConfig({ ...envConfig, apiTimeoutSeconds: e.target.value })}
+ />
+ 默认 120 秒;超时或网络中断不会自动重发,以避免重复计费。
+
@@ -232,8 +348,11 @@ export default function EnvConfig() {
id="apiKey"
type={showApiKey ? 'text' : 'password'}
value={envConfig.apiKey}
- onChange={(e) => setEnvConfig({ ...envConfig, apiKey: e.target.value })}
- placeholder="sk-xxxxxxxxxxxxxxxxx"
+ onChange={(e) => {
+ setEnvConfig({ ...envConfig, apiKey: e.target.value })
+ setSensitiveDirty({ ...sensitiveDirty, apiKey: true })
+ }}
+ placeholder={sensitiveConfigured.apiKey ? '已配置;输入新值可替换' : 'sk-xxxxxxxxxxxxxxxxx'}
/>
setShowApiKey(!showApiKey)}
@@ -245,9 +364,16 @@ export default function EnvConfig() {
{showApiKey ? '隐藏' : '显示'}
-
- 🔐 API密钥将被安全存储,请妥善保管
-
+
+
+ {sensitiveConfigured.apiKey ? '已配置,页面不会读取或显示原值。' : '尚未配置远程 API Key。'}
+
+ {sensitiveConfigured.apiKey && (
+ clearSensitiveConfig('API_KEY')}>
+ 清除已保存值
+
+ )}
+
@@ -259,9 +385,8 @@ export default function EnvConfig() {
- 提示: 这些环境变量将保存到{' '}
- .env{' '}
- 文件中。请勿将包含敏感信息的 .env 文件提交到版本控制系统。
+ 提示: 配置保存在本机项目数据库中。API Key 和 Webhook
+ 只允许写入,页面只显示“是否已配置”,不会读取或回显原值。
diff --git a/front/app/liepin/analysis/AnalysisContent.tsx b/front/app/liepin/analysis/AnalysisContent.tsx
index 9e2dbcb..baafe7a 100644
--- a/front/app/liepin/analysis/AnalysisContent.tsx
+++ b/front/app/liepin/analysis/AnalysisContent.tsx
@@ -19,6 +19,8 @@ type StatsResponse = {
total: number
delivered: number
pending: number
+ requested: number
+ unknown: number
filtered: number
failed: number
avgMonthlyK?: number | null
@@ -51,9 +53,16 @@ type LiepinJob = {
hrName?: string
hrTitle?: string
delivered?: number
+ deliveryStatus?: string
createTime?: string
}
+function deliveryStatusOf(job: LiepinJob) {
+ const status = job.deliveryStatus?.trim()
+ if (status) return status
+ return job.delivered === 1 ? "已投递" : "未投递"
+}
+
type PagedResult = {
items: LiepinJob[]
total: number
@@ -224,10 +233,11 @@ export default function AnalysisContent({ showHeader = false }: { showHeader?: b
const [keyword, setKeyword] = useState("")
const [loadingList, setLoadingList] = useState(false)
const [exporting, setExporting] = useState(false)
+ const [recoveringJobId, setRecoveringJobId] = useState(null)
const [detailJob, setDetailJob] = useState(null)
const [computedSalaryBuckets, setComputedSalaryBuckets] = useState([])
- const statusOptions = ["未投递", "已投递"]
+ const statusOptions = ["未投递", "投递确认中", "投递结果待确认", "已投递", "投递失败"]
useEffect(() => {
loadStats()
@@ -349,7 +359,7 @@ export default function AnalysisContent({ showHeader = false }: { showHeader?: b
it.jobExpReq || "",
it.jobEduReq || "",
it.hrName || "",
- (it.delivered === 1 ? "已投递" : "未投递"),
+ deliveryStatusOf(it),
it.jobLink || "",
it.createTime || "",
])
@@ -371,6 +381,39 @@ export default function AnalysisContent({ showHeader = false }: { showHeader?: b
}
}
+ const recoverDelivery = async (jobId: number, action: "confirmed" | "failed" | "retry") => {
+ const prompt = action === "confirmed"
+ ? "请先到猎聘平台核对:这个岗位确实已经投递成功。确认写入已投递吗?"
+ : action === "failed"
+ ? "请先到猎聘平台核对:这个岗位确实没有投递成功。确认写入失败吗?"
+ : "确认允许这个岗位在下一次猎聘任务中重新投递吗?这不会立即打开浏览器。"
+ if (!window.confirm(prompt)) return
+ const path = action === "retry" ? "delivery-retry" : "delivery-reconcile"
+ const init: RequestInit = action === "retry"
+ ? { method: "POST" }
+ : {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ outcome: action === "confirmed" ? "CONFIRMED" : "FAILED",
+ message: "用户在猎聘页面人工核对",
+ }),
+ }
+ try {
+ setRecoveringJobId(jobId)
+ const response = await fetch(`${API_BASE}/api/liepin/jobs/${jobId}/${path}`, init)
+ const data = await response.json()
+ if (!response.ok || !data.success) throw new Error(data.message || `HTTP ${response.status}`)
+ window.alert(data.message || "处理完成")
+ await loadList(page, size)
+ await loadStats()
+ } catch (error) {
+ window.alert(error instanceof Error ? error.message : "恢复操作失败")
+ } finally {
+ setRecoveringJobId(null)
+ }
+ }
+
// 当后端的薪资分布为空或全零时,使用全量分页数据计算分布
const refreshComputedSalaryBuckets = async () => {
try {
@@ -466,6 +509,8 @@ export default function AnalysisContent({ showHeader = false }: { showHeader?: b
return [
{ title: "总岗位数", value: k?.total ?? 0 },
{ title: "已投递", value: k?.delivered ?? 0 },
+ { title: "投递确认中", value: k?.requested ?? 0 },
+ { title: "结果待确认", value: k?.unknown ?? 0 },
{ title: "未投递", value: k?.pending ?? 0 },
{ title: "平均月薪(K)", value: (k?.avgMonthlyK ?? avgMonthlyKFromItems ?? 0) },
]
@@ -771,7 +816,19 @@ export default function AnalysisContent({ showHeader = false }: { showHeader?: b
{it.jobEduReq || ""} |
{it.hrName || ""} |
- {it.delivered === 1 ? "已投递" : "未投递"}
+ {deliveryStatusOf(it)}
+ {deliveryStatusOf(it) === "投递结果待确认" && (
+
+ recoverDelivery(it.jobId, "confirmed")}>核对已投递
+ recoverDelivery(it.jobId, "failed")}>核对失败
+ recoverDelivery(it.jobId, "retry")}>允许重试
+
+ )}
+ {deliveryStatusOf(it) === "投递失败" && (
+
+ recoverDelivery(it.jobId, "retry")}>允许重试
+
+ )}
|
{it.jobLink ? (
@@ -804,7 +861,7 @@ export default function AnalysisContent({ showHeader = false }: { showHeader?: b
经验:{detailJob.jobExpReq || ""}
学历:{detailJob.jobEduReq || ""}
HR:{detailJob.hrName || ""}
- 状态:{detailJob.delivered === 1 ? "已投递" : "未投递"}
+ 状态:{deliveryStatusOf(detailJob)}
创建时间:{formatDateOnly(detailJob.createTime)}
diff --git a/front/app/zhilian/analysis/AnalysisContent.tsx b/front/app/zhilian/analysis/AnalysisContent.tsx
index 03a19a5..49e342b 100644
--- a/front/app/zhilian/analysis/AnalysisContent.tsx
+++ b/front/app/zhilian/analysis/AnalysisContent.tsx
@@ -93,6 +93,76 @@ type PagedResult = {
size: number
}
+async function markZhilianUnknownReservations(
+ tasks: Array<{ id?: number; requestKey?: string }>,
+ reason: string,
+) {
+ const results = await Promise.allSettled(tasks.map(async (task) => {
+ if (!task.id || !task.requestKey) return
+ const response = await fetch(`${API_BASE}/api/zhilian/jobs/${task.id}/delivery-result`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ requestKey: task.requestKey,
+ outcome: "UNKNOWN",
+ evidence: "NO_CONFIRMATION",
+ message: reason,
+ }),
+ })
+ const data = await response.json().catch(() => ({}))
+ if (!response.ok || data.success === false) {
+ throw new Error(data.message || "智联 UNKNOWN 状态回写失败")
+ }
+ }))
+ const failed = results.filter((result) => result.status === "rejected")
+ if (failed.length > 0) console.error("智联 UNKNOWN 状态回写失败", failed)
+}
+
+async function postZhilianJsonWithRetry(url: string, body?: unknown) {
+ let lastError: unknown
+ for (let attempt = 0; attempt < 2; attempt += 1) {
+ try {
+ const response = await fetch(url, {
+ method: "POST",
+ headers: body === undefined ? undefined : { "Content-Type": "application/json" },
+ body: body === undefined ? undefined : JSON.stringify(body),
+ })
+ return await response.json()
+ } catch (error) {
+ lastError = error
+ }
+ }
+ throw lastError instanceof Error ? lastError : new Error("智联投递请求未收到响应")
+}
+
+function unresolvedZhilianReservations(
+ tasks: Array<{ id?: number; requestKey?: string }>,
+ result: Record ,
+) {
+ const rows = Array.isArray(result.results) ? result.results : []
+ if (rows.length === 0) return tasks
+ const persistedKeys = new Set(rows.map((row) => {
+ if (!row || typeof row !== "object") return ""
+ const item = row as { requestKey?: unknown; persisted?: unknown }
+ return item.persisted === true ? String(item.requestKey || "") : ""
+ }))
+ return tasks.filter((task) => !task.requestKey || !persistedKeys.has(task.requestKey))
+}
+
+function formatZhilianBatchResult(result: Record) {
+ const summary = String(result.message || "批量投递任务已结束。")
+ const rows = Array.isArray(result.results) ? result.results : []
+ if (rows.length === 0) return summary
+ const details = rows.slice(0, 50).map((row, index) => {
+ const item = row && typeof row === "object"
+ ? row as { id?: unknown; outcome?: unknown; evidence?: unknown; persisted?: unknown; message?: unknown }
+ : {}
+ const persisted = item.persisted === true ? "已落库" : "待补偿"
+ return `${index + 1}. 岗位 ${String(item.id || "-")} · ${String(item.outcome || "UNKNOWN")} · ${persisted} · ${String(item.evidence || "-")}\n${String(item.message || "")}`
+ })
+ return `${summary}\n\n逐条结果:\n${details.join("\n")}`
+}
+
type ChartRef = { destroy: () => void }
const CATEGORY_COLORS = [
@@ -528,7 +598,7 @@ export default function AnalysisContent({ showHeader = false, refreshSignal = 0
const [pendingCardsExpanded, setPendingCardsExpanded] = useState(false)
const activeScanRunId = ""
- const statusOptions = ["待确认", "AI分析中", "未投递", "已投递", "已过滤", "投递失败", "AI不匹配", "AI分析失败"]
+ const statusOptions = ["待确认", "投递确认中", "投递结果待确认", "AI分析中", "未投递", "已投递", "已过滤", "投递失败", "AI不匹配", "AI分析失败"]
const loadList = async (toPage = page, toSize = size) => {
try {
@@ -785,58 +855,121 @@ export default function AnalysisContent({ showHeader = false, refreshSignal = 0
alert("该智联岗位缺少内部ID,无法确认投递。")
return
}
+ let reservedTasks: Array<{ id?: number; requestKey?: string }> = []
try {
setActingJobId(job.id)
- const res = await fetch(`${API_BASE}/api/zhilian/jobs/${job.id}/confirm`, { method: "POST" })
- const data = await res.json()
+ const ok = window.confirm(`将通过 Chrome 真实申请智联岗位:${job.companyName || ""} / ${job.jobTitle || ""}。确认继续?`)
+ if (!ok) return
+ const data = await postZhilianJsonWithRetry(`${API_BASE}/api/zhilian/jobs/${job.id}/confirm`)
if (!data.success) {
alert(data.message || "该智联岗位暂不能投递。")
return
}
- const ok = window.confirm(`将通过 Chrome 真实申请智联岗位:${job.companyName || ""} / ${job.jobTitle || ""}。确认继续?`)
- if (!ok) return
+ reservedTasks = [data.task]
const result = await sendChromeBridgeMessage({
type: "ZHILIAN_DELIVER_ONE",
platform: "zhilian",
task: data.task,
}, 120000)
+ if (result.persisted !== true) {
+ await markZhilianUnknownReservations(reservedTasks, result.message || "Chrome Bridge 未返回岗位结果")
+ }
alert(result.message || (result.success ? "已发送投递请求。" : "Chrome投递失败。"))
await loadList(page, size)
await loadStats()
await loadDashboardStats()
} catch {
+ await markZhilianUnknownReservations(reservedTasks, "前端未收到 Chrome 投递执行结果")
alert("确认投递失败:网络或服务异常。")
} finally {
setActingJobId(null)
}
}
+ const handleReconcileJob = async (job: ZhilianJob) => {
+ if (!job.id) return
+ const answer = window.prompt(
+ "请先在智联平台核对该岗位。输入“已投递”确认成功,输入“未投递”确认失败;其他内容不会修改状态。",
+ )?.trim()
+ if (answer !== "已投递" && answer !== "未投递") return
+ try {
+ setActingJobId(job.id)
+ const data = await postZhilianJsonWithRetry(`${API_BASE}/api/zhilian/jobs/${job.id}/delivery-reconcile`, {
+ outcome: answer === "已投递" ? "CONFIRMED" : "FAILED",
+ message: `用户在智联平台人工核对:${answer}`,
+ })
+ alert(data.message || (data.success ? "人工对账已保存。" : "人工对账失败。"))
+ await loadList(page, size)
+ await loadStats()
+ await loadDashboardStats()
+ } catch {
+ alert("人工对账失败:网络或服务异常。")
+ } finally {
+ setActingJobId(null)
+ }
+ }
+
+ const handleRetryJob = async (job: ZhilianJob) => {
+ if (!job.id) return
+ const ok = window.confirm("这会创建新的投递 attempt,并可能再次申请该智联岗位。确认显式重试?")
+ if (!ok) return
+ let reservedTasks: Array<{ id?: number; requestKey?: string }> = []
+ try {
+ setActingJobId(job.id)
+ const data = await postZhilianJsonWithRetry(`${API_BASE}/api/zhilian/jobs/${job.id}/delivery-retry`)
+ if (!data.success || !data.task) {
+ alert(data.message || "当前岗位不能重试。")
+ return
+ }
+ reservedTasks = [data.task]
+ const result = await sendChromeBridgeMessage({
+ type: "ZHILIAN_DELIVER_ONE",
+ platform: "zhilian",
+ task: data.task,
+ }, 120000)
+ if (result.persisted !== true) {
+ await markZhilianUnknownReservations(reservedTasks, result.message || "Chrome 重试结果未确认写入")
+ }
+ alert(result.message || "重试任务已结束。")
+ await loadList(page, size)
+ await loadStats()
+ await loadDashboardStats()
+ } catch {
+ await markZhilianUnknownReservations(reservedTasks, "前端未收到 Chrome 重试执行结果")
+ alert("重试失败:网络或服务异常,已保守标记待对账。")
+ } finally {
+ setActingJobId(null)
+ }
+ }
+
const handleConfirmBatch = async () => {
+ let reservedTasks: Array<{ id?: number; requestKey?: string }> = []
try {
setActingBatch(true)
- const res = await fetch(`${API_BASE}/api/zhilian/jobs/confirm-batch`, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify(currentBatchFilters()),
- })
- const data = await res.json()
+ const ok = window.confirm("将通过 Chrome 真实申请当前筛选范围内的智联待确认岗位。确认继续?")
+ if (!ok) return
+ const data = await postZhilianJsonWithRetry(`${API_BASE}/api/zhilian/jobs/confirm-batch`, currentBatchFilters())
const tasks = data.tasks || []
+ reservedTasks = tasks
if (!data.success || tasks.length === 0) {
alert(data.message || "当前筛选条件下没有智联待确认岗位。")
return
}
- const ok = window.confirm(`将通过 Chrome 真实申请 ${tasks.length} 个智联待确认岗位。确认继续?`)
- if (!ok) return
const result = await sendChromeBridgeMessage({
type: "ZHILIAN_DELIVER_BATCH",
platform: "zhilian",
tasks,
}, Math.max(120000, tasks.length * 30000))
- alert(result.message || "批量投递任务已结束。")
+ const unresolved = unresolvedZhilianReservations(reservedTasks, result)
+ if (unresolved.length > 0) {
+ await markZhilianUnknownReservations(unresolved, result.message || "Chrome Bridge 未确认写入完整批量结果")
+ }
+ alert(formatZhilianBatchResult(result))
await loadList(page, size)
await loadStats()
await loadDashboardStats()
} catch {
+ await markZhilianUnknownReservations(reservedTasks, "前端未收到 Chrome 批量投递执行结果")
alert("批量投递失败:网络或服务异常。")
} finally {
setActingBatch(false)
@@ -1180,14 +1313,27 @@ export default function AnalysisContent({ showHeader = false, refreshSignal = 0
}`}
>
- {it.deliveryStatus === "待确认" ? (
+ {it.deliveryStatus === "待确认" || it.deliveryStatus === "投递确认中" ? (
handleConfirmJob(it)}
className="h-7 rounded-lg px-3 text-xs"
>
- Chrome投递
+ {it.deliveryStatus === "投递确认中" ? "恢复投递" : "Chrome投递"}
+
+ ) : it.deliveryStatus === "投递结果待确认" ? (
+
+ handleReconcileJob(it)} className="h-7 rounded-lg px-3 text-xs">
+ 对账
+
+ handleRetryJob(it)} className="h-7 rounded-lg px-3 text-xs">
+ 重试
+
+
+ ) : it.deliveryStatus === "投递失败" ? (
+ handleRetryJob(it)} className="h-7 rounded-lg px-3 text-xs">
+ 重试
) : (it.deliveryStatus || "").trim() === "已投递" ? (
diff --git a/front/lib/setupChecklist.ts b/front/lib/setupChecklist.ts
index acc6c7e..b8ebeb5 100644
--- a/front/lib/setupChecklist.ts
+++ b/front/lib/setupChecklist.ts
@@ -94,10 +94,10 @@ async function fetchJson(url: string, timeoutMs = 4000): Promise {
async function checkBackend(): Promise {
try {
- const data = await fetchJson<{ status?: string; state?: string; service?: string }>(`${API_BASE}/api/health`, 3000)
+ const data = await fetchJson<{ status?: string; state?: string; service?: string }>(`${API_BASE}/api/ready`, 3000)
const status = String(data.status || data.state || "").toUpperCase()
const done = status === "HEALTHY" || status === "UP"
- return item("backend", "后端连接", done, done ? "本地后端服务运行正常" : "后端健康检查返回异常", "环境配置", "/env-config", !done)
+ return item("backend", "后端连接", done, done ? "本地后端已就绪,可以接收任务" : "后端尚未就绪,请检查数据库与任务队列", "环境配置", "/env-config", !done)
} catch {
return item("backend", "后端连接", false, `未检测到 ${API_BASE} 后端服务`, "环境配置", "/env-config", true)
}
diff --git a/front/package.json b/front/package.json
index 5558ed5..6ab223a 100644
--- a/front/package.json
+++ b/front/package.json
@@ -7,7 +7,8 @@
"build": "next build",
"build:prod": "next build && node scripts/copy-dist.mjs",
"start": "node start-prod.mjs",
- "lint": "eslint"
+ "lint": "eslint",
+ "typecheck": "tsc --noEmit"
},
"dependencies": {
"@radix-ui/react-label": "^2.1.7",
@@ -18,14 +19,13 @@
"clsx": "^2.1.1",
"framer-motion": "^12.23.24",
"lucide-react": "^0.468.0",
- "next": "16.0.1",
+ "next": "16.3.2",
"next-themes": "^0.4.6",
- "react": "19.2.0",
- "react-dom": "19.2.0",
+ "react": "19.2.8",
+ "react-dom": "19.2.8",
"react-icons": "^5.5.0",
"sql.js": "^1.13.0",
- "tailwind-merge": "^2.6.0",
- "tailwindcss-animate": "^1.0.7"
+ "tailwind-merge": "^2.6.0"
},
"devDependencies": {
"@types/node": "^20",
@@ -33,9 +33,10 @@
"@types/react-dom": "^19",
"autoprefixer": "^10.4.20",
"eslint": "^9",
- "eslint-config-next": "16.0.1",
+ "eslint-config-next": "16.3.2",
"postcss": "^8.4.49",
"tailwindcss": "^3.4.17",
+ "tailwindcss-animate": "^1.0.7",
"typescript": "^5"
},
"packageManager": "pnpm@10.20.0+sha512.cf9998222162dd85864d0a8102e7892e7ba4ceadebbf5a31f9c2fce48dfce317a9c53b9f6464d1ef9042cba2e02ae02a9f7c143a2b438cd93c91840f0192b9dd"
diff --git a/front/pnpm-lock.yaml b/front/pnpm-lock.yaml
index b5b596b..007de5e 100644
--- a/front/pnpm-lock.yaml
+++ b/front/pnpm-lock.yaml
@@ -10,13 +10,13 @@ importers:
dependencies:
'@radix-ui/react-label':
specifier: ^2.1.7
- version: 2.1.7(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+ version: 2.1.7(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
'@radix-ui/react-slot':
specifier: ^1.2.3
- version: 1.2.3(@types/react@19.2.2)(react@19.2.0)
+ version: 1.2.3(@types/react@19.2.2)(react@19.2.8)
'@radix-ui/react-tabs':
specifier: ^1.1.13
- version: 1.1.13(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+ version: 1.1.13(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
chart.js:
specifier: ^4.4.4
version: 4.5.1
@@ -28,34 +28,31 @@ importers:
version: 2.1.1
framer-motion:
specifier: ^12.23.24
- version: 12.23.24(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+ version: 12.23.24(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
lucide-react:
specifier: ^0.468.0
- version: 0.468.0(react@19.2.0)
+ version: 0.468.0(react@19.2.8)
next:
- specifier: 16.0.1
- version: 16.0.1(@babel/core@7.28.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+ specifier: 16.3.2
+ version: 16.3.2(@babel/core@7.29.6)(@types/node@20.19.24)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
next-themes:
specifier: ^0.4.6
- version: 0.4.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+ version: 0.4.6(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
react:
- specifier: 19.2.0
- version: 19.2.0
+ specifier: 19.2.8
+ version: 19.2.8
react-dom:
- specifier: 19.2.0
- version: 19.2.0(react@19.2.0)
+ specifier: 19.2.8
+ version: 19.2.8(react@19.2.8)
react-icons:
specifier: ^5.5.0
- version: 5.5.0(react@19.2.0)
+ version: 5.5.0(react@19.2.8)
sql.js:
specifier: ^1.13.0
version: 1.13.0
tailwind-merge:
specifier: ^2.6.0
version: 2.6.0
- tailwindcss-animate:
- specifier: ^1.0.7
- version: 1.0.7(tailwindcss@3.4.18)
devDependencies:
'@types/node':
specifier: ^20
@@ -73,14 +70,17 @@ importers:
specifier: ^9
version: 9.39.0(jiti@1.21.7)
eslint-config-next:
- specifier: 16.0.1
- version: 16.0.1(@typescript-eslint/parser@8.46.2(eslint@9.39.0(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.0(jiti@1.21.7))(typescript@5.9.3)
+ specifier: 16.3.2
+ version: 16.3.2(@typescript-eslint/parser@8.46.2(eslint@9.39.0(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.0(jiti@1.21.7))(typescript@5.9.3)
postcss:
specifier: ^8.4.49
version: 8.5.6
tailwindcss:
specifier: ^3.4.17
version: 3.4.18
+ tailwindcss-animate:
+ specifier: ^1.0.7
+ version: 1.0.7(tailwindcss@3.4.18)
typescript:
specifier: ^5
version: 5.9.3
@@ -91,36 +91,36 @@ packages:
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
engines: {node: '>=10'}
- '@babel/code-frame@7.27.1':
- resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==}
+ '@babel/code-frame@7.29.7':
+ resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==}
engines: {node: '>=6.9.0'}
- '@babel/compat-data@7.28.5':
- resolution: {integrity: sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==}
+ '@babel/compat-data@7.29.7':
+ resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==}
engines: {node: '>=6.9.0'}
- '@babel/core@7.28.5':
- resolution: {integrity: sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==}
+ '@babel/core@7.29.6':
+ resolution: {integrity: sha512-QdxmAo/ikZqqRGA8s43ww8lcql6naWRvEz0FFrl6MIlc7Gi6TroXnSdWa5U/kq6fzcpqpHesicQxFZIieZbyIA==}
engines: {node: '>=6.9.0'}
- '@babel/generator@7.28.5':
- resolution: {integrity: sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==}
+ '@babel/generator@7.29.8':
+ resolution: {integrity: sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==}
engines: {node: '>=6.9.0'}
- '@babel/helper-compilation-targets@7.27.2':
- resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==}
+ '@babel/helper-compilation-targets@7.29.7':
+ resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==}
engines: {node: '>=6.9.0'}
- '@babel/helper-globals@7.28.0':
- resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==}
+ '@babel/helper-globals@7.29.7':
+ resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==}
engines: {node: '>=6.9.0'}
- '@babel/helper-module-imports@7.27.1':
- resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==}
+ '@babel/helper-module-imports@7.29.7':
+ resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==}
engines: {node: '>=6.9.0'}
- '@babel/helper-module-transforms@7.28.3':
- resolution: {integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==}
+ '@babel/helper-module-transforms@7.29.7':
+ resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0
@@ -129,16 +129,24 @@ packages:
resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
engines: {node: '>=6.9.0'}
+ '@babel/helper-string-parser@7.29.7':
+ resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==}
+ engines: {node: '>=6.9.0'}
+
'@babel/helper-validator-identifier@7.28.5':
resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==}
engines: {node: '>=6.9.0'}
- '@babel/helper-validator-option@7.27.1':
- resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==}
+ '@babel/helper-validator-identifier@7.29.7':
+ resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-validator-option@7.29.7':
+ resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==}
engines: {node: '>=6.9.0'}
- '@babel/helpers@7.28.4':
- resolution: {integrity: sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==}
+ '@babel/helpers@7.29.7':
+ resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==}
engines: {node: '>=6.9.0'}
'@babel/parser@7.28.5':
@@ -146,21 +154,33 @@ packages:
engines: {node: '>=6.0.0'}
hasBin: true
- '@babel/template@7.27.2':
- resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==}
+ '@babel/parser@7.29.8':
+ resolution: {integrity: sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==}
+ engines: {node: '>=6.0.0'}
+ hasBin: true
+
+ '@babel/template@7.29.7':
+ resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==}
engines: {node: '>=6.9.0'}
- '@babel/traverse@7.28.5':
- resolution: {integrity: sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==}
+ '@babel/traverse@7.29.8':
+ resolution: {integrity: sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==}
engines: {node: '>=6.9.0'}
'@babel/types@7.28.5':
resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==}
engines: {node: '>=6.9.0'}
+ '@babel/types@7.29.8':
+ resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==}
+ engines: {node: '>=6.9.0'}
+
'@emnapi/core@1.6.0':
resolution: {integrity: sha512-zq/ay+9fNIJJtJiZxdTnXS20PllcYMX3OE23ESc4HK/bdYu3cOWYVhsOhVnXALfU/uqJIxn5NBPd9z4v+SfoSg==}
+ '@emnapi/runtime@1.11.3':
+ resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==}
+
'@emnapi/runtime@1.6.0':
resolution: {integrity: sha512-obtUmAHTMjll499P+D9A3axeJFlhdjOWdKUNs/U6QIGT7V5RjcUW1xToAzjvmgTSQhDbYn/NwfTRoJcQ2rNBxA==}
@@ -173,6 +193,12 @@ packages:
peerDependencies:
eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
+ '@eslint-community/eslint-utils@4.9.1':
+ resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+ peerDependencies:
+ eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
+
'@eslint-community/regexpp@4.12.2':
resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==}
engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
@@ -221,129 +247,149 @@ packages:
resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
engines: {node: '>=18.18'}
- '@img/colour@1.0.0':
- resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==}
+ '@img/colour@1.1.0':
+ resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==}
engines: {node: '>=18'}
- '@img/sharp-darwin-arm64@0.34.4':
- resolution: {integrity: sha512-sitdlPzDVyvmINUdJle3TNHl+AG9QcwiAMsXmccqsCOMZNIdW2/7S26w0LyU8euiLVzFBL3dXPwVCq/ODnf2vA==}
- engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ '@img/sharp-darwin-arm64@0.35.3':
+ resolution: {integrity: sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg==}
+ engines: {node: '>=20.9.0'}
cpu: [arm64]
os: [darwin]
- '@img/sharp-darwin-x64@0.34.4':
- resolution: {integrity: sha512-rZheupWIoa3+SOdF/IcUe1ah4ZDpKBGWcsPX6MT0lYniH9micvIU7HQkYTfrx5Xi8u+YqwLtxC/3vl8TQN6rMg==}
- engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ '@img/sharp-darwin-x64@0.35.3':
+ resolution: {integrity: sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w==}
+ engines: {node: '>=20.9.0'}
cpu: [x64]
os: [darwin]
- '@img/sharp-libvips-darwin-arm64@1.2.3':
- resolution: {integrity: sha512-QzWAKo7kpHxbuHqUC28DZ9pIKpSi2ts2OJnoIGI26+HMgq92ZZ4vk8iJd4XsxN+tYfNJxzH6W62X5eTcsBymHw==}
+ '@img/sharp-freebsd-wasm32@0.35.3':
+ resolution: {integrity: sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg==}
+ engines: {node: '>=20.9.0'}
+ os: [freebsd]
+
+ '@img/sharp-libvips-darwin-arm64@1.3.2':
+ resolution: {integrity: sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg==}
cpu: [arm64]
os: [darwin]
- '@img/sharp-libvips-darwin-x64@1.2.3':
- resolution: {integrity: sha512-Ju+g2xn1E2AKO6YBhxjj+ACcsPQRHT0bhpglxcEf+3uyPY+/gL8veniKoo96335ZaPo03bdDXMv0t+BBFAbmRA==}
+ '@img/sharp-libvips-darwin-x64@1.3.2':
+ resolution: {integrity: sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw==}
cpu: [x64]
os: [darwin]
- '@img/sharp-libvips-linux-arm64@1.2.3':
- resolution: {integrity: sha512-I4RxkXU90cpufazhGPyVujYwfIm9Nk1QDEmiIsaPwdnm013F7RIceaCc87kAH+oUB1ezqEvC6ga4m7MSlqsJvQ==}
+ '@img/sharp-libvips-linux-arm64@1.3.2':
+ resolution: {integrity: sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA==}
cpu: [arm64]
os: [linux]
- '@img/sharp-libvips-linux-arm@1.2.3':
- resolution: {integrity: sha512-x1uE93lyP6wEwGvgAIV0gP6zmaL/a0tGzJs/BIDDG0zeBhMnuUPm7ptxGhUbcGs4okDJrk4nxgrmxpib9g6HpA==}
+ '@img/sharp-libvips-linux-arm@1.3.2':
+ resolution: {integrity: sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ==}
cpu: [arm]
os: [linux]
- '@img/sharp-libvips-linux-ppc64@1.2.3':
- resolution: {integrity: sha512-Y2T7IsQvJLMCBM+pmPbM3bKT/yYJvVtLJGfCs4Sp95SjvnFIjynbjzsa7dY1fRJX45FTSfDksbTp6AGWudiyCg==}
+ '@img/sharp-libvips-linux-ppc64@1.3.2':
+ resolution: {integrity: sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw==}
cpu: [ppc64]
os: [linux]
- '@img/sharp-libvips-linux-s390x@1.2.3':
- resolution: {integrity: sha512-RgWrs/gVU7f+K7P+KeHFaBAJlNkD1nIZuVXdQv6S+fNA6syCcoboNjsV2Pou7zNlVdNQoQUpQTk8SWDHUA3y/w==}
+ '@img/sharp-libvips-linux-riscv64@1.3.2':
+ resolution: {integrity: sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w==}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@img/sharp-libvips-linux-s390x@1.3.2':
+ resolution: {integrity: sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ==}
cpu: [s390x]
os: [linux]
- '@img/sharp-libvips-linux-x64@1.2.3':
- resolution: {integrity: sha512-3JU7LmR85K6bBiRzSUc/Ff9JBVIFVvq6bomKE0e63UXGeRw2HPVEjoJke1Yx+iU4rL7/7kUjES4dZ/81Qjhyxg==}
+ '@img/sharp-libvips-linux-x64@1.3.2':
+ resolution: {integrity: sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w==}
cpu: [x64]
os: [linux]
- '@img/sharp-libvips-linuxmusl-arm64@1.2.3':
- resolution: {integrity: sha512-F9q83RZ8yaCwENw1GieztSfj5msz7GGykG/BA+MOUefvER69K/ubgFHNeSyUu64amHIYKGDs4sRCMzXVj8sEyw==}
+ '@img/sharp-libvips-linuxmusl-arm64@1.3.2':
+ resolution: {integrity: sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw==}
cpu: [arm64]
os: [linux]
- '@img/sharp-libvips-linuxmusl-x64@1.2.3':
- resolution: {integrity: sha512-U5PUY5jbc45ANM6tSJpsgqmBF/VsL6LnxJmIf11kB7J5DctHgqm0SkuXzVWtIY90GnJxKnC/JT251TDnk1fu/g==}
+ '@img/sharp-libvips-linuxmusl-x64@1.3.2':
+ resolution: {integrity: sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ==}
cpu: [x64]
os: [linux]
- '@img/sharp-linux-arm64@0.34.4':
- resolution: {integrity: sha512-YXU1F/mN/Wu786tl72CyJjP/Ngl8mGHN1hST4BGl+hiW5jhCnV2uRVTNOcaYPs73NeT/H8Upm3y9582JVuZHrQ==}
- engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ '@img/sharp-linux-arm64@0.35.3':
+ resolution: {integrity: sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ==}
+ engines: {node: '>=20.9.0'}
cpu: [arm64]
os: [linux]
- '@img/sharp-linux-arm@0.34.4':
- resolution: {integrity: sha512-Xyam4mlqM0KkTHYVSuc6wXRmM7LGN0P12li03jAnZ3EJWZqj83+hi8Y9UxZUbxsgsK1qOEwg7O0Bc0LjqQVtxA==}
- engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ '@img/sharp-linux-arm@0.35.3':
+ resolution: {integrity: sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA==}
+ engines: {node: '>=20.9.0'}
cpu: [arm]
os: [linux]
- '@img/sharp-linux-ppc64@0.34.4':
- resolution: {integrity: sha512-F4PDtF4Cy8L8hXA2p3TO6s4aDt93v+LKmpcYFLAVdkkD3hSxZzee0rh6/+94FpAynsuMpLX5h+LRsSG3rIciUQ==}
- engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ '@img/sharp-linux-ppc64@0.35.3':
+ resolution: {integrity: sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA==}
+ engines: {node: '>=20.9.0'}
cpu: [ppc64]
os: [linux]
- '@img/sharp-linux-s390x@0.34.4':
- resolution: {integrity: sha512-qVrZKE9Bsnzy+myf7lFKvng6bQzhNUAYcVORq2P7bDlvmF6u2sCmK2KyEQEBdYk+u3T01pVsPrkj943T1aJAsw==}
- engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ '@img/sharp-linux-riscv64@0.35.3':
+ resolution: {integrity: sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ==}
+ engines: {node: '>=20.9.0'}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@img/sharp-linux-s390x@0.35.3':
+ resolution: {integrity: sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw==}
+ engines: {node: '>=20.9.0'}
cpu: [s390x]
os: [linux]
- '@img/sharp-linux-x64@0.34.4':
- resolution: {integrity: sha512-ZfGtcp2xS51iG79c6Vhw9CWqQC8l2Ot8dygxoDoIQPTat/Ov3qAa8qpxSrtAEAJW+UjTXc4yxCjNfxm4h6Xm2A==}
- engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ '@img/sharp-linux-x64@0.35.3':
+ resolution: {integrity: sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA==}
+ engines: {node: '>=20.9.0'}
cpu: [x64]
os: [linux]
- '@img/sharp-linuxmusl-arm64@0.34.4':
- resolution: {integrity: sha512-8hDVvW9eu4yHWnjaOOR8kHVrew1iIX+MUgwxSuH2XyYeNRtLUe4VNioSqbNkB7ZYQJj9rUTT4PyRscyk2PXFKA==}
- engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ '@img/sharp-linuxmusl-arm64@0.35.3':
+ resolution: {integrity: sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w==}
+ engines: {node: '>=20.9.0'}
cpu: [arm64]
os: [linux]
- '@img/sharp-linuxmusl-x64@0.34.4':
- resolution: {integrity: sha512-lU0aA5L8QTlfKjpDCEFOZsTYGn3AEiO6db8W5aQDxj0nQkVrZWmN3ZP9sYKWJdtq3PWPhUNlqehWyXpYDcI9Sg==}
- engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ '@img/sharp-linuxmusl-x64@0.35.3':
+ resolution: {integrity: sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg==}
+ engines: {node: '>=20.9.0'}
cpu: [x64]
os: [linux]
- '@img/sharp-wasm32@0.34.4':
- resolution: {integrity: sha512-33QL6ZO/qpRyG7woB/HUALz28WnTMI2W1jgX3Nu2bypqLIKx/QKMILLJzJjI+SIbvXdG9fUnmrxR7vbi1sTBeA==}
- engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ '@img/sharp-wasm32@0.35.3':
+ resolution: {integrity: sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w==}
+ engines: {node: '>=20.9.0'}
+
+ '@img/sharp-webcontainers-wasm32@0.35.3':
+ resolution: {integrity: sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q==}
+ engines: {node: '>=20.9.0'}
cpu: [wasm32]
- '@img/sharp-win32-arm64@0.34.4':
- resolution: {integrity: sha512-2Q250do/5WXTwxW3zjsEuMSv5sUU4Tq9VThWKlU2EYLm4MB7ZeMwF+SFJutldYODXF6jzc6YEOC+VfX0SZQPqA==}
- engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ '@img/sharp-win32-arm64@0.35.3':
+ resolution: {integrity: sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w==}
+ engines: {node: '>=20.9.0'}
cpu: [arm64]
os: [win32]
- '@img/sharp-win32-ia32@0.34.4':
- resolution: {integrity: sha512-3ZeLue5V82dT92CNL6rsal6I2weKw1cYu+rGKm8fOCCtJTR2gYeUfY3FqUnIJsMUPIH68oS5jmZ0NiJ508YpEw==}
- engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ '@img/sharp-win32-ia32@0.35.3':
+ resolution: {integrity: sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw==}
+ engines: {node: ^20.9.0}
cpu: [ia32]
os: [win32]
- '@img/sharp-win32-x64@0.34.4':
- resolution: {integrity: sha512-xIyj4wpYs8J18sVN3mSQjwrw7fKUqRw+Z5rnHNCy5fYTxigBz81u5mOMPmFumwjcn8+ld1ppptMBCLic1nz6ig==}
- engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ '@img/sharp-win32-x64@0.35.3':
+ resolution: {integrity: sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA==}
+ engines: {node: '>=20.9.0'}
cpu: [x64]
os: [win32]
@@ -373,56 +419,56 @@ packages:
'@napi-rs/wasm-runtime@0.2.12':
resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==}
- '@next/env@16.0.1':
- resolution: {integrity: sha512-LFvlK0TG2L3fEOX77OC35KowL8D7DlFF45C0OvKMC4hy8c/md1RC4UMNDlUGJqfCoCS2VWrZ4dSE6OjaX5+8mw==}
+ '@next/env@16.3.2':
+ resolution: {integrity: sha512-8k4YoG8cM7LWlkfzGNYCRBbFNlernLiMw4s0btVl+CmmWqn3VpYypA72/5Feb1UWdxe6tHqr5KHP4p4Y4m9luA==}
- '@next/eslint-plugin-next@16.0.1':
- resolution: {integrity: sha512-g4Cqmv/gyFEXNeVB2HkqDlYKfy+YrlM2k8AVIO/YQVEPfhVruH1VA99uT1zELLnPLIeOnx8IZ6Ddso0asfTIdw==}
+ '@next/eslint-plugin-next@16.3.2':
+ resolution: {integrity: sha512-z+HW1cZgt8QhByw8p2EbxF94AImgsKIYUbtSkA7Zld2T9yrKAlys4jNOcAOCtv6csX2CoA/5qCVyesL5pHmJ0A==}
- '@next/swc-darwin-arm64@16.0.1':
- resolution: {integrity: sha512-R0YxRp6/4W7yG1nKbfu41bp3d96a0EalonQXiMe+1H9GTHfKxGNCGFNWUho18avRBPsO8T3RmdWuzmfurlQPbg==}
+ '@next/swc-darwin-arm64@16.3.2':
+ resolution: {integrity: sha512-ib5Llm93YCKoKWDh6ZaHq6QWTuOZ2bRkSnUwMmX8dsRIOkBNL1vVlSiUKSfixPL9SSh9pvukzqajk/klkn5vqg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
- '@next/swc-darwin-x64@16.0.1':
- resolution: {integrity: sha512-kETZBocRux3xITiZtOtVoVvXyQLB7VBxN7L6EPqgI5paZiUlnsgYv4q8diTNYeHmF9EiehydOBo20lTttCbHAg==}
+ '@next/swc-darwin-x64@16.3.2':
+ resolution: {integrity: sha512-qd98fX2+I5nYJDioW2o7nSjoxM5KvWdeDefM80igia4+C/qSIEhH4MhTE+hO/7qKM7W37/Mq+dOWp8UePSyLHw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
- '@next/swc-linux-arm64-gnu@16.0.1':
- resolution: {integrity: sha512-hWg3BtsxQuSKhfe0LunJoqxjO4NEpBmKkE+P2Sroos7yB//OOX3jD5ISP2wv8QdUwtRehMdwYz6VB50mY6hqAg==}
+ '@next/swc-linux-arm64-gnu@16.3.2':
+ resolution: {integrity: sha512-vqsgb6FAOzcrCccsLXiKtAy5t8EzO+uOazuFaSkQxeY0tNONG3vpHYy8pyBafcI5SNFPTeyard6yTr6SzNGo2A==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
- '@next/swc-linux-arm64-musl@16.0.1':
- resolution: {integrity: sha512-UPnOvYg+fjAhP3b1iQStcYPWeBFRLrugEyK/lDKGk7kLNua8t5/DvDbAEFotfV1YfcOY6bru76qN9qnjLoyHCQ==}
+ '@next/swc-linux-arm64-musl@16.3.2':
+ resolution: {integrity: sha512-xIe1eujfHUB2XcxHGddxJyu6TJRPjC5NpIkQYB/32ESkt5VkQyIAjmLRS38c+s6QY+qjtY/4KarVDzXRuD7lZQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
- '@next/swc-linux-x64-gnu@16.0.1':
- resolution: {integrity: sha512-Et81SdWkcRqAJziIgFtsFyJizHoWne4fzJkvjd6V4wEkWTB4MX6J0uByUb0peiJQ4WeAt6GGmMszE5KrXK6WKg==}
+ '@next/swc-linux-x64-gnu@16.3.2':
+ resolution: {integrity: sha512-Fe0SA2j8X0kmc3aveuHD7UktO3AE2+mH3LguP60vGbz7u0z+MrDXbeb5iZFYAwR7EzzzXJ2Yk966w9mGTFMqfA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
- '@next/swc-linux-x64-musl@16.0.1':
- resolution: {integrity: sha512-qBbgYEBRrC1egcG03FZaVfVxrJm8wBl7vr8UFKplnxNRprctdP26xEv9nJ07Ggq4y1adwa0nz2mz83CELY7N6Q==}
+ '@next/swc-linux-x64-musl@16.3.2':
+ resolution: {integrity: sha512-TFBipb+gyesI/2Ve4zVu7kGltBWN/R466G5/1gtt2lECfc22G1pjkTxu68Q9aFcOaXiRGTQfvDbQQFe7mYgxiQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
- '@next/swc-win32-arm64-msvc@16.0.1':
- resolution: {integrity: sha512-cPuBjYP6I699/RdbHJonb3BiRNEDm5CKEBuJ6SD8k3oLam2fDRMKAvmrli4QMDgT2ixyRJ0+DTkiODbIQhRkeQ==}
+ '@next/swc-win32-arm64-msvc@16.3.2':
+ resolution: {integrity: sha512-rVtmnNpBYIosDnKD/96dKxFsJnwnn1WRGG/HioSe8XCm2ksSHNrd2R6+hSjvTBxeMNhJ9pYeu/90cWB1nQLuNA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
- '@next/swc-win32-x64-msvc@16.0.1':
- resolution: {integrity: sha512-XeEUJsE4JYtfrXe/LaJn3z1pD19fK0Q6Er8Qoufi+HqvdO4LEPyCxLUt4rxA+4RfYo6S9gMlmzCMU2F+AatFqQ==}
+ '@next/swc-win32-x64-msvc@16.3.2':
+ resolution: {integrity: sha512-H4Y2o2/JcHu8LtwzD5CXfHhwxwz8gfsx2HXDEw46Mtev5xHnEmB7HNtZtmriw5ReUOjRtcDqo7XSbU01FT9NlA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
@@ -612,8 +658,8 @@ packages:
'@rtsao/scc@1.1.0':
resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==}
- '@swc/helpers@0.5.15':
- resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
+ '@swc/helpers@0.5.23':
+ resolution: {integrity: sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==}
'@tybys/wasm-util@0.10.1':
resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
@@ -899,6 +945,11 @@ packages:
balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
+ baseline-browser-mapping@2.11.19:
+ resolution: {integrity: sha512-Grytf1xOxOEMTGRwx6rLGKkTabd4vMg3VrKdj/7joCmV0qgh4QwMMO6xh34YEXQqirAuUdgQGa5orJQQ+69RBw==}
+ engines: {node: '>=6.0.0'}
+ hasBin: true
+
baseline-browser-mapping@2.8.22:
resolution: {integrity: sha512-/tk9kky/d8T8CTXIQYASLyhAxR5VwL3zct1oAoVTaOUHwrmsGnfbRwNdEq+vOl2BN8i3PcDdP0o4Q+jjKQoFbQ==}
hasBin: true
@@ -1109,8 +1160,8 @@ packages:
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
engines: {node: '>=10'}
- eslint-config-next@16.0.1:
- resolution: {integrity: sha512-wNuHw5gNOxwLUvpg0cu6IL0crrVC9hAwdS/7UwleNkwyaMiWIOAwf8yzXVqBBzL3c9A7jVRngJxjoSpPP1aEhg==}
+ eslint-config-next@16.3.2:
+ resolution: {integrity: sha512-gTABOJmyc6pEgSX1Z1VOjBxkSmo5Hkdj+ePclDf8HLGTsnVWzgtDdrOeQrAnUkf0JNJJhwPuXrsIwmFZRKJLoQ==}
peerDependencies:
eslint: '>=9.0.0'
typescript: '>=3.3.1'
@@ -1198,6 +1249,7 @@ packages:
eslint@9.39.0:
resolution: {integrity: sha512-iy2GE3MHrYTL5lrCtMZ0X1KLEKKUjmK0kzwcnefhR66txcEmXZD2YWgR5GNdcEwkNx3a0siYkSvl0vIC+Svjmg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options.
hasBin: true
peerDependencies:
jiti: '*'
@@ -1346,6 +1398,7 @@ packages:
glob@10.4.5:
resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==}
+ deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
hasBin: true
globals@14.0.0:
@@ -1668,6 +1721,11 @@ packages:
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
+ nanoid@3.3.18:
+ resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==}
+ engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
+ hasBin: true
+
napi-postinstall@0.3.4:
resolution: {integrity: sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==}
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
@@ -1682,8 +1740,8 @@ packages:
react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
- next@16.0.1:
- resolution: {integrity: sha512-e9RLSssZwd35p7/vOa+hoDFggUZIUbZhIUSLZuETCwrCVvxOs87NamoUzT+vbcNAL8Ld9GobBnWOA6SbV/arOw==}
+ next@16.3.2:
+ resolution: {integrity: sha512-/ZCaubUy17Lld1SiPWxuPbCk2ihqAxF2QNQaPZeEaEb7t1I58qhsJN187D7AfpapHAqUPXH0f/thtdW9dWgWFg==}
engines: {node: '>=20.9.0'}
hasBin: true
peerDependencies:
@@ -1854,8 +1912,8 @@ packages:
postcss-value-parser@4.2.0:
resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
- postcss@8.4.31:
- resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==}
+ postcss@8.5.23:
+ resolution: {integrity: sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==}
engines: {node: ^10 || ^12 || >=14}
postcss@8.5.6:
@@ -1876,10 +1934,10 @@ packages:
queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
- react-dom@19.2.0:
- resolution: {integrity: sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==}
+ react-dom@19.2.8:
+ resolution: {integrity: sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==}
peerDependencies:
- react: ^19.2.0
+ react: ^19.2.8
react-icons@5.5.0:
resolution: {integrity: sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==}
@@ -1889,8 +1947,8 @@ packages:
react-is@16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
- react@19.2.0:
- resolution: {integrity: sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==}
+ react@19.2.8:
+ resolution: {integrity: sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==}
engines: {node: '>=0.10.0'}
read-cache@1.0.0:
@@ -1955,6 +2013,11 @@ packages:
engines: {node: '>=10'}
hasBin: true
+ semver@7.8.5:
+ resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==}
+ engines: {node: '>=10'}
+ hasBin: true
+
set-function-length@1.2.2:
resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==}
engines: {node: '>= 0.4'}
@@ -1967,9 +2030,14 @@ packages:
resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==}
engines: {node: '>= 0.4'}
- sharp@0.34.4:
- resolution: {integrity: sha512-FUH39xp3SBPnxWvd5iib1X8XY7J0K0X7d93sie9CJg2PO8/7gmg89Nve6OjItK53/MlAushNNxteBYfM6DEuoA==}
- engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ sharp@0.35.3:
+ resolution: {integrity: sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q==}
+ engines: {node: '>=20.9.0'}
+ peerDependencies:
+ '@types/node': '*'
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
shebang-command@2.0.0:
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
@@ -2236,25 +2304,25 @@ snapshots:
'@alloc/quick-lru@5.2.0': {}
- '@babel/code-frame@7.27.1':
+ '@babel/code-frame@7.29.7':
dependencies:
- '@babel/helper-validator-identifier': 7.28.5
+ '@babel/helper-validator-identifier': 7.29.7
js-tokens: 4.0.0
picocolors: 1.1.1
- '@babel/compat-data@7.28.5': {}
+ '@babel/compat-data@7.29.7': {}
- '@babel/core@7.28.5':
+ '@babel/core@7.29.6':
dependencies:
- '@babel/code-frame': 7.27.1
- '@babel/generator': 7.28.5
- '@babel/helper-compilation-targets': 7.27.2
- '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.5)
- '@babel/helpers': 7.28.4
- '@babel/parser': 7.28.5
- '@babel/template': 7.27.2
- '@babel/traverse': 7.28.5
- '@babel/types': 7.28.5
+ '@babel/code-frame': 7.29.7
+ '@babel/generator': 7.29.8
+ '@babel/helper-compilation-targets': 7.29.7
+ '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.6)
+ '@babel/helpers': 7.29.7
+ '@babel/parser': 7.29.8
+ '@babel/template': 7.29.7
+ '@babel/traverse': 7.29.8
+ '@babel/types': 7.29.8
'@jridgewell/remapping': 2.3.5
convert-source-map: 2.0.0
debug: 4.4.3
@@ -2264,69 +2332,77 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@babel/generator@7.28.5':
+ '@babel/generator@7.29.8':
dependencies:
- '@babel/parser': 7.28.5
- '@babel/types': 7.28.5
+ '@babel/parser': 7.29.8
+ '@babel/types': 7.29.8
'@jridgewell/gen-mapping': 0.3.13
'@jridgewell/trace-mapping': 0.3.31
jsesc: 3.1.0
- '@babel/helper-compilation-targets@7.27.2':
+ '@babel/helper-compilation-targets@7.29.7':
dependencies:
- '@babel/compat-data': 7.28.5
- '@babel/helper-validator-option': 7.27.1
+ '@babel/compat-data': 7.29.7
+ '@babel/helper-validator-option': 7.29.7
browserslist: 4.27.0
lru-cache: 5.1.1
semver: 6.3.1
- '@babel/helper-globals@7.28.0': {}
+ '@babel/helper-globals@7.29.7': {}
- '@babel/helper-module-imports@7.27.1':
+ '@babel/helper-module-imports@7.29.7':
dependencies:
- '@babel/traverse': 7.28.5
- '@babel/types': 7.28.5
+ '@babel/traverse': 7.29.8
+ '@babel/types': 7.29.8
transitivePeerDependencies:
- supports-color
- '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.5)':
+ '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.6)':
dependencies:
- '@babel/core': 7.28.5
- '@babel/helper-module-imports': 7.27.1
- '@babel/helper-validator-identifier': 7.28.5
- '@babel/traverse': 7.28.5
+ '@babel/core': 7.29.6
+ '@babel/helper-module-imports': 7.29.7
+ '@babel/helper-validator-identifier': 7.29.7
+ '@babel/traverse': 7.29.8
transitivePeerDependencies:
- supports-color
'@babel/helper-string-parser@7.27.1': {}
+ '@babel/helper-string-parser@7.29.7': {}
+
'@babel/helper-validator-identifier@7.28.5': {}
- '@babel/helper-validator-option@7.27.1': {}
+ '@babel/helper-validator-identifier@7.29.7': {}
+
+ '@babel/helper-validator-option@7.29.7': {}
- '@babel/helpers@7.28.4':
+ '@babel/helpers@7.29.7':
dependencies:
- '@babel/template': 7.27.2
- '@babel/types': 7.28.5
+ '@babel/template': 7.29.7
+ '@babel/types': 7.29.8
'@babel/parser@7.28.5':
dependencies:
'@babel/types': 7.28.5
- '@babel/template@7.27.2':
+ '@babel/parser@7.29.8':
dependencies:
- '@babel/code-frame': 7.27.1
- '@babel/parser': 7.28.5
- '@babel/types': 7.28.5
+ '@babel/types': 7.29.8
- '@babel/traverse@7.28.5':
+ '@babel/template@7.29.7':
dependencies:
- '@babel/code-frame': 7.27.1
- '@babel/generator': 7.28.5
- '@babel/helper-globals': 7.28.0
- '@babel/parser': 7.28.5
- '@babel/template': 7.27.2
- '@babel/types': 7.28.5
+ '@babel/code-frame': 7.29.7
+ '@babel/parser': 7.29.8
+ '@babel/types': 7.29.8
+
+ '@babel/traverse@7.29.8':
+ dependencies:
+ '@babel/code-frame': 7.29.7
+ '@babel/generator': 7.29.8
+ '@babel/helper-globals': 7.29.7
+ '@babel/parser': 7.29.8
+ '@babel/template': 7.29.7
+ '@babel/types': 7.29.8
debug: 4.4.3
transitivePeerDependencies:
- supports-color
@@ -2336,12 +2412,22 @@ snapshots:
'@babel/helper-string-parser': 7.27.1
'@babel/helper-validator-identifier': 7.28.5
+ '@babel/types@7.29.8':
+ dependencies:
+ '@babel/helper-string-parser': 7.29.7
+ '@babel/helper-validator-identifier': 7.29.7
+
'@emnapi/core@1.6.0':
dependencies:
'@emnapi/wasi-threads': 1.1.0
tslib: 2.8.1
optional: true
+ '@emnapi/runtime@1.11.3':
+ dependencies:
+ tslib: 2.8.1
+ optional: true
+
'@emnapi/runtime@1.6.0':
dependencies:
tslib: 2.8.1
@@ -2357,6 +2443,11 @@ snapshots:
eslint: 9.39.0(jiti@1.21.7)
eslint-visitor-keys: 3.4.3
+ '@eslint-community/eslint-utils@4.9.1(eslint@9.39.0(jiti@1.21.7))':
+ dependencies:
+ eslint: 9.39.0(jiti@1.21.7)
+ eslint-visitor-keys: 3.4.3
+
'@eslint-community/regexpp@4.12.2': {}
'@eslint/config-array@0.21.1':
@@ -2409,93 +2500,111 @@ snapshots:
'@humanwhocodes/retry@0.4.3': {}
- '@img/colour@1.0.0':
+ '@img/colour@1.1.0':
optional: true
- '@img/sharp-darwin-arm64@0.34.4':
+ '@img/sharp-darwin-arm64@0.35.3':
optionalDependencies:
- '@img/sharp-libvips-darwin-arm64': 1.2.3
+ '@img/sharp-libvips-darwin-arm64': 1.3.2
optional: true
- '@img/sharp-darwin-x64@0.34.4':
+ '@img/sharp-darwin-x64@0.35.3':
optionalDependencies:
- '@img/sharp-libvips-darwin-x64': 1.2.3
+ '@img/sharp-libvips-darwin-x64': 1.3.2
optional: true
- '@img/sharp-libvips-darwin-arm64@1.2.3':
+ '@img/sharp-freebsd-wasm32@0.35.3':
+ dependencies:
+ '@img/sharp-wasm32': 0.35.3
optional: true
- '@img/sharp-libvips-darwin-x64@1.2.3':
+ '@img/sharp-libvips-darwin-arm64@1.3.2':
optional: true
- '@img/sharp-libvips-linux-arm64@1.2.3':
+ '@img/sharp-libvips-darwin-x64@1.3.2':
optional: true
- '@img/sharp-libvips-linux-arm@1.2.3':
+ '@img/sharp-libvips-linux-arm64@1.3.2':
optional: true
- '@img/sharp-libvips-linux-ppc64@1.2.3':
+ '@img/sharp-libvips-linux-arm@1.3.2':
optional: true
- '@img/sharp-libvips-linux-s390x@1.2.3':
+ '@img/sharp-libvips-linux-ppc64@1.3.2':
optional: true
- '@img/sharp-libvips-linux-x64@1.2.3':
+ '@img/sharp-libvips-linux-riscv64@1.3.2':
optional: true
- '@img/sharp-libvips-linuxmusl-arm64@1.2.3':
+ '@img/sharp-libvips-linux-s390x@1.3.2':
optional: true
- '@img/sharp-libvips-linuxmusl-x64@1.2.3':
+ '@img/sharp-libvips-linux-x64@1.3.2':
optional: true
- '@img/sharp-linux-arm64@0.34.4':
+ '@img/sharp-libvips-linuxmusl-arm64@1.3.2':
+ optional: true
+
+ '@img/sharp-libvips-linuxmusl-x64@1.3.2':
+ optional: true
+
+ '@img/sharp-linux-arm64@0.35.3':
optionalDependencies:
- '@img/sharp-libvips-linux-arm64': 1.2.3
+ '@img/sharp-libvips-linux-arm64': 1.3.2
optional: true
- '@img/sharp-linux-arm@0.34.4':
+ '@img/sharp-linux-arm@0.35.3':
optionalDependencies:
- '@img/sharp-libvips-linux-arm': 1.2.3
+ '@img/sharp-libvips-linux-arm': 1.3.2
optional: true
- '@img/sharp-linux-ppc64@0.34.4':
+ '@img/sharp-linux-ppc64@0.35.3':
optionalDependencies:
- '@img/sharp-libvips-linux-ppc64': 1.2.3
+ '@img/sharp-libvips-linux-ppc64': 1.3.2
optional: true
- '@img/sharp-linux-s390x@0.34.4':
+ '@img/sharp-linux-riscv64@0.35.3':
optionalDependencies:
- '@img/sharp-libvips-linux-s390x': 1.2.3
+ '@img/sharp-libvips-linux-riscv64': 1.3.2
optional: true
- '@img/sharp-linux-x64@0.34.4':
+ '@img/sharp-linux-s390x@0.35.3':
optionalDependencies:
- '@img/sharp-libvips-linux-x64': 1.2.3
+ '@img/sharp-libvips-linux-s390x': 1.3.2
optional: true
- '@img/sharp-linuxmusl-arm64@0.34.4':
+ '@img/sharp-linux-x64@0.35.3':
optionalDependencies:
- '@img/sharp-libvips-linuxmusl-arm64': 1.2.3
+ '@img/sharp-libvips-linux-x64': 1.3.2
optional: true
- '@img/sharp-linuxmusl-x64@0.34.4':
+ '@img/sharp-linuxmusl-arm64@0.35.3':
optionalDependencies:
- '@img/sharp-libvips-linuxmusl-x64': 1.2.3
+ '@img/sharp-libvips-linuxmusl-arm64': 1.3.2
optional: true
- '@img/sharp-wasm32@0.34.4':
+ '@img/sharp-linuxmusl-x64@0.35.3':
+ optionalDependencies:
+ '@img/sharp-libvips-linuxmusl-x64': 1.3.2
+ optional: true
+
+ '@img/sharp-wasm32@0.35.3':
dependencies:
- '@emnapi/runtime': 1.6.0
+ '@emnapi/runtime': 1.11.3
+ optional: true
+
+ '@img/sharp-webcontainers-wasm32@0.35.3':
+ dependencies:
+ '@img/sharp-wasm32': 0.35.3
optional: true
- '@img/sharp-win32-arm64@0.34.4':
+ '@img/sharp-win32-arm64@0.35.3':
optional: true
- '@img/sharp-win32-ia32@0.34.4':
+ '@img/sharp-win32-ia32@0.35.3':
optional: true
- '@img/sharp-win32-x64@0.34.4':
+ '@img/sharp-win32-x64@0.35.3':
optional: true
'@isaacs/cliui@8.0.2':
@@ -2535,34 +2644,37 @@ snapshots:
'@tybys/wasm-util': 0.10.1
optional: true
- '@next/env@16.0.1': {}
+ '@next/env@16.3.2': {}
- '@next/eslint-plugin-next@16.0.1':
+ '@next/eslint-plugin-next@16.3.2(eslint@9.39.0(jiti@1.21.7))':
dependencies:
+ '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.0(jiti@1.21.7))
fast-glob: 3.3.1
+ transitivePeerDependencies:
+ - eslint
- '@next/swc-darwin-arm64@16.0.1':
+ '@next/swc-darwin-arm64@16.3.2':
optional: true
- '@next/swc-darwin-x64@16.0.1':
+ '@next/swc-darwin-x64@16.3.2':
optional: true
- '@next/swc-linux-arm64-gnu@16.0.1':
+ '@next/swc-linux-arm64-gnu@16.3.2':
optional: true
- '@next/swc-linux-arm64-musl@16.0.1':
+ '@next/swc-linux-arm64-musl@16.3.2':
optional: true
- '@next/swc-linux-x64-gnu@16.0.1':
+ '@next/swc-linux-x64-gnu@16.3.2':
optional: true
- '@next/swc-linux-x64-musl@16.0.1':
+ '@next/swc-linux-x64-musl@16.3.2':
optional: true
- '@next/swc-win32-arm64-msvc@16.0.1':
+ '@next/swc-win32-arm64-msvc@16.3.2':
optional: true
- '@next/swc-win32-x64-msvc@16.0.1':
+ '@next/swc-win32-x64-msvc@16.3.2':
optional: true
'@nodelib/fs.scandir@2.1.5':
@@ -2584,141 +2696,141 @@ snapshots:
'@radix-ui/primitive@1.1.3': {}
- '@radix-ui/react-collection@1.1.7(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+ '@radix-ui/react-collection@1.1.7(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)':
dependencies:
- '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0)
- '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.0)
- '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
- '@radix-ui/react-slot': 1.2.3(@types/react@19.2.2)(react@19.2.0)
- react: 19.2.0
- react-dom: 19.2.0(react@19.2.0)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.8)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.8)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
+ '@radix-ui/react-slot': 1.2.3(@types/react@19.2.2)(react@19.2.8)
+ react: 19.2.8
+ react-dom: 19.2.8(react@19.2.8)
optionalDependencies:
'@types/react': 19.2.2
'@types/react-dom': 19.2.2(@types/react@19.2.2)
- '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.2)(react@19.2.0)':
+ '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.2)(react@19.2.8)':
dependencies:
- react: 19.2.0
+ react: 19.2.8
optionalDependencies:
'@types/react': 19.2.2
- '@radix-ui/react-context@1.1.2(@types/react@19.2.2)(react@19.2.0)':
+ '@radix-ui/react-context@1.1.2(@types/react@19.2.2)(react@19.2.8)':
dependencies:
- react: 19.2.0
+ react: 19.2.8
optionalDependencies:
'@types/react': 19.2.2
- '@radix-ui/react-direction@1.1.1(@types/react@19.2.2)(react@19.2.0)':
+ '@radix-ui/react-direction@1.1.1(@types/react@19.2.2)(react@19.2.8)':
dependencies:
- react: 19.2.0
+ react: 19.2.8
optionalDependencies:
'@types/react': 19.2.2
- '@radix-ui/react-id@1.1.1(@types/react@19.2.2)(react@19.2.0)':
+ '@radix-ui/react-id@1.1.1(@types/react@19.2.2)(react@19.2.8)':
dependencies:
- '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.2)(react@19.2.0)
- react: 19.2.0
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.2)(react@19.2.8)
+ react: 19.2.8
optionalDependencies:
'@types/react': 19.2.2
- '@radix-ui/react-label@2.1.7(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+ '@radix-ui/react-label@2.1.7(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)':
dependencies:
- '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
- react: 19.2.0
- react-dom: 19.2.0(react@19.2.0)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
+ react: 19.2.8
+ react-dom: 19.2.8(react@19.2.8)
optionalDependencies:
'@types/react': 19.2.2
'@types/react-dom': 19.2.2(@types/react@19.2.2)
- '@radix-ui/react-presence@1.1.5(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+ '@radix-ui/react-presence@1.1.5(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)':
dependencies:
- '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0)
- '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.2)(react@19.2.0)
- react: 19.2.0
- react-dom: 19.2.0(react@19.2.0)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.8)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.2)(react@19.2.8)
+ react: 19.2.8
+ react-dom: 19.2.8(react@19.2.8)
optionalDependencies:
'@types/react': 19.2.2
'@types/react-dom': 19.2.2(@types/react@19.2.2)
- '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+ '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)':
dependencies:
- '@radix-ui/react-slot': 1.2.3(@types/react@19.2.2)(react@19.2.0)
- react: 19.2.0
- react-dom: 19.2.0(react@19.2.0)
+ '@radix-ui/react-slot': 1.2.3(@types/react@19.2.2)(react@19.2.8)
+ react: 19.2.8
+ react-dom: 19.2.8(react@19.2.8)
optionalDependencies:
'@types/react': 19.2.2
'@types/react-dom': 19.2.2(@types/react@19.2.2)
- '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+ '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)':
dependencies:
'@radix-ui/primitive': 1.1.3
- '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
- '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0)
- '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.0)
- '@radix-ui/react-direction': 1.1.1(@types/react@19.2.2)(react@19.2.0)
- '@radix-ui/react-id': 1.1.1(@types/react@19.2.2)(react@19.2.0)
- '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
- '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.2)(react@19.2.0)
- '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.0)
- react: 19.2.0
- react-dom: 19.2.0(react@19.2.0)
+ '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.8)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.8)
+ '@radix-ui/react-direction': 1.1.1(@types/react@19.2.2)(react@19.2.8)
+ '@radix-ui/react-id': 1.1.1(@types/react@19.2.2)(react@19.2.8)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.2)(react@19.2.8)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.8)
+ react: 19.2.8
+ react-dom: 19.2.8(react@19.2.8)
optionalDependencies:
'@types/react': 19.2.2
'@types/react-dom': 19.2.2(@types/react@19.2.2)
- '@radix-ui/react-slot@1.2.3(@types/react@19.2.2)(react@19.2.0)':
+ '@radix-ui/react-slot@1.2.3(@types/react@19.2.2)(react@19.2.8)':
dependencies:
- '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0)
- react: 19.2.0
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.8)
+ react: 19.2.8
optionalDependencies:
'@types/react': 19.2.2
- '@radix-ui/react-tabs@1.1.13(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+ '@radix-ui/react-tabs@1.1.13(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)':
dependencies:
'@radix-ui/primitive': 1.1.3
- '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.0)
- '@radix-ui/react-direction': 1.1.1(@types/react@19.2.2)(react@19.2.0)
- '@radix-ui/react-id': 1.1.1(@types/react@19.2.2)(react@19.2.0)
- '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
- '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
- '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
- '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.0)
- react: 19.2.0
- react-dom: 19.2.0(react@19.2.0)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.8)
+ '@radix-ui/react-direction': 1.1.1(@types/react@19.2.2)(react@19.2.8)
+ '@radix-ui/react-id': 1.1.1(@types/react@19.2.2)(react@19.2.8)
+ '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
+ '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.8)
+ react: 19.2.8
+ react-dom: 19.2.8(react@19.2.8)
optionalDependencies:
'@types/react': 19.2.2
'@types/react-dom': 19.2.2(@types/react@19.2.2)
- '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.2)(react@19.2.0)':
+ '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.2)(react@19.2.8)':
dependencies:
- react: 19.2.0
+ react: 19.2.8
optionalDependencies:
'@types/react': 19.2.2
- '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.2)(react@19.2.0)':
+ '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.2)(react@19.2.8)':
dependencies:
- '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.2)(react@19.2.0)
- '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.2)(react@19.2.0)
- react: 19.2.0
+ '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.2)(react@19.2.8)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.2)(react@19.2.8)
+ react: 19.2.8
optionalDependencies:
'@types/react': 19.2.2
- '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.2)(react@19.2.0)':
+ '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.2)(react@19.2.8)':
dependencies:
- '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.2)(react@19.2.0)
- react: 19.2.0
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.2)(react@19.2.8)
+ react: 19.2.8
optionalDependencies:
'@types/react': 19.2.2
- '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.2)(react@19.2.0)':
+ '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.2)(react@19.2.8)':
dependencies:
- react: 19.2.0
+ react: 19.2.8
optionalDependencies:
'@types/react': 19.2.2
'@rtsao/scc@1.1.0': {}
- '@swc/helpers@0.5.15':
+ '@swc/helpers@0.5.23':
dependencies:
tslib: 2.8.1
@@ -3024,6 +3136,8 @@ snapshots:
balanced-match@1.0.2: {}
+ baseline-browser-mapping@2.11.19: {}
+
baseline-browser-mapping@2.8.22: {}
binary-extensions@2.3.0: {}
@@ -3295,9 +3409,9 @@ snapshots:
escape-string-regexp@4.0.0: {}
- eslint-config-next@16.0.1(@typescript-eslint/parser@8.46.2(eslint@9.39.0(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.0(jiti@1.21.7))(typescript@5.9.3):
+ eslint-config-next@16.3.2(@typescript-eslint/parser@8.46.2(eslint@9.39.0(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.0(jiti@1.21.7))(typescript@5.9.3):
dependencies:
- '@next/eslint-plugin-next': 16.0.1
+ '@next/eslint-plugin-next': 16.3.2(eslint@9.39.0(jiti@1.21.7))
eslint: 9.39.0(jiti@1.21.7)
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.0(jiti@1.21.7))
@@ -3399,7 +3513,7 @@ snapshots:
eslint-plugin-react-hooks@7.0.1(eslint@9.39.0(jiti@1.21.7)):
dependencies:
- '@babel/core': 7.28.5
+ '@babel/core': 7.29.6
'@babel/parser': 7.28.5
eslint: 9.39.0(jiti@1.21.7)
hermes-parser: 0.25.1
@@ -3559,14 +3673,14 @@ snapshots:
fraction.js@4.3.7: {}
- framer-motion@12.23.24(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
+ framer-motion@12.23.24(react-dom@19.2.8(react@19.2.8))(react@19.2.8):
dependencies:
motion-dom: 12.23.23
motion-utils: 12.23.6
tslib: 2.8.1
optionalDependencies:
- react: 19.2.0
- react-dom: 19.2.0(react@19.2.0)
+ react: 19.2.8
+ react-dom: 19.2.8(react@19.2.8)
fsevents@2.3.3:
optional: true
@@ -3892,9 +4006,9 @@ snapshots:
dependencies:
yallist: 3.1.1
- lucide-react@0.468.0(react@19.2.0):
+ lucide-react@0.468.0(react@19.2.8):
dependencies:
- react: 19.2.0
+ react: 19.2.8
math-intrinsics@1.1.0: {}
@@ -3933,36 +4047,40 @@ snapshots:
nanoid@3.3.11: {}
+ nanoid@3.3.18: {}
+
napi-postinstall@0.3.4: {}
natural-compare@1.4.0: {}
- next-themes@0.4.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
+ next-themes@0.4.6(react-dom@19.2.8(react@19.2.8))(react@19.2.8):
dependencies:
- react: 19.2.0
- react-dom: 19.2.0(react@19.2.0)
+ react: 19.2.8
+ react-dom: 19.2.8(react@19.2.8)
- next@16.0.1(@babel/core@7.28.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
+ next@16.3.2(@babel/core@7.29.6)(@types/node@20.19.24)(react-dom@19.2.8(react@19.2.8))(react@19.2.8):
dependencies:
- '@next/env': 16.0.1
- '@swc/helpers': 0.5.15
+ '@next/env': 16.3.2
+ '@swc/helpers': 0.5.23
+ baseline-browser-mapping: 2.11.19
caniuse-lite: 1.0.30001752
- postcss: 8.4.31
- react: 19.2.0
- react-dom: 19.2.0(react@19.2.0)
- styled-jsx: 5.1.6(@babel/core@7.28.5)(react@19.2.0)
+ postcss: 8.5.23
+ react: 19.2.8
+ react-dom: 19.2.8(react@19.2.8)
+ styled-jsx: 5.1.6(@babel/core@7.29.6)(react@19.2.8)
optionalDependencies:
- '@next/swc-darwin-arm64': 16.0.1
- '@next/swc-darwin-x64': 16.0.1
- '@next/swc-linux-arm64-gnu': 16.0.1
- '@next/swc-linux-arm64-musl': 16.0.1
- '@next/swc-linux-x64-gnu': 16.0.1
- '@next/swc-linux-x64-musl': 16.0.1
- '@next/swc-win32-arm64-msvc': 16.0.1
- '@next/swc-win32-x64-msvc': 16.0.1
- sharp: 0.34.4
+ '@next/swc-darwin-arm64': 16.3.2
+ '@next/swc-darwin-x64': 16.3.2
+ '@next/swc-linux-arm64-gnu': 16.3.2
+ '@next/swc-linux-arm64-musl': 16.3.2
+ '@next/swc-linux-x64-gnu': 16.3.2
+ '@next/swc-linux-x64-musl': 16.3.2
+ '@next/swc-win32-arm64-msvc': 16.3.2
+ '@next/swc-win32-x64-msvc': 16.3.2
+ sharp: 0.35.3(@types/node@20.19.24)
transitivePeerDependencies:
- '@babel/core'
+ - '@types/node'
- babel-plugin-macros
node-releases@2.0.27: {}
@@ -4098,9 +4216,9 @@ snapshots:
postcss-value-parser@4.2.0: {}
- postcss@8.4.31:
+ postcss@8.5.23:
dependencies:
- nanoid: 3.3.11
+ nanoid: 3.3.18
picocolors: 1.1.1
source-map-js: 1.2.1
@@ -4122,18 +4240,18 @@ snapshots:
queue-microtask@1.2.3: {}
- react-dom@19.2.0(react@19.2.0):
+ react-dom@19.2.8(react@19.2.8):
dependencies:
- react: 19.2.0
+ react: 19.2.8
scheduler: 0.27.0
- react-icons@5.5.0(react@19.2.0):
+ react-icons@5.5.0(react@19.2.8):
dependencies:
- react: 19.2.0
+ react: 19.2.8
react-is@16.13.1: {}
- react@19.2.0: {}
+ react@19.2.8: {}
read-cache@1.0.0:
dependencies:
@@ -4210,6 +4328,9 @@ snapshots:
semver@7.7.3: {}
+ semver@7.8.5:
+ optional: true
+
set-function-length@1.2.2:
dependencies:
define-data-property: 1.1.4
@@ -4232,34 +4353,38 @@ snapshots:
es-errors: 1.3.0
es-object-atoms: 1.1.1
- sharp@0.34.4:
+ sharp@0.35.3(@types/node@20.19.24):
dependencies:
- '@img/colour': 1.0.0
+ '@img/colour': 1.1.0
detect-libc: 2.1.2
- semver: 7.7.3
+ semver: 7.8.5
optionalDependencies:
- '@img/sharp-darwin-arm64': 0.34.4
- '@img/sharp-darwin-x64': 0.34.4
- '@img/sharp-libvips-darwin-arm64': 1.2.3
- '@img/sharp-libvips-darwin-x64': 1.2.3
- '@img/sharp-libvips-linux-arm': 1.2.3
- '@img/sharp-libvips-linux-arm64': 1.2.3
- '@img/sharp-libvips-linux-ppc64': 1.2.3
- '@img/sharp-libvips-linux-s390x': 1.2.3
- '@img/sharp-libvips-linux-x64': 1.2.3
- '@img/sharp-libvips-linuxmusl-arm64': 1.2.3
- '@img/sharp-libvips-linuxmusl-x64': 1.2.3
- '@img/sharp-linux-arm': 0.34.4
- '@img/sharp-linux-arm64': 0.34.4
- '@img/sharp-linux-ppc64': 0.34.4
- '@img/sharp-linux-s390x': 0.34.4
- '@img/sharp-linux-x64': 0.34.4
- '@img/sharp-linuxmusl-arm64': 0.34.4
- '@img/sharp-linuxmusl-x64': 0.34.4
- '@img/sharp-wasm32': 0.34.4
- '@img/sharp-win32-arm64': 0.34.4
- '@img/sharp-win32-ia32': 0.34.4
- '@img/sharp-win32-x64': 0.34.4
+ '@img/sharp-darwin-arm64': 0.35.3
+ '@img/sharp-darwin-x64': 0.35.3
+ '@img/sharp-freebsd-wasm32': 0.35.3
+ '@img/sharp-libvips-darwin-arm64': 1.3.2
+ '@img/sharp-libvips-darwin-x64': 1.3.2
+ '@img/sharp-libvips-linux-arm': 1.3.2
+ '@img/sharp-libvips-linux-arm64': 1.3.2
+ '@img/sharp-libvips-linux-ppc64': 1.3.2
+ '@img/sharp-libvips-linux-riscv64': 1.3.2
+ '@img/sharp-libvips-linux-s390x': 1.3.2
+ '@img/sharp-libvips-linux-x64': 1.3.2
+ '@img/sharp-libvips-linuxmusl-arm64': 1.3.2
+ '@img/sharp-libvips-linuxmusl-x64': 1.3.2
+ '@img/sharp-linux-arm': 0.35.3
+ '@img/sharp-linux-arm64': 0.35.3
+ '@img/sharp-linux-ppc64': 0.35.3
+ '@img/sharp-linux-riscv64': 0.35.3
+ '@img/sharp-linux-s390x': 0.35.3
+ '@img/sharp-linux-x64': 0.35.3
+ '@img/sharp-linuxmusl-arm64': 0.35.3
+ '@img/sharp-linuxmusl-x64': 0.35.3
+ '@img/sharp-webcontainers-wasm32': 0.35.3
+ '@img/sharp-win32-arm64': 0.35.3
+ '@img/sharp-win32-ia32': 0.35.3
+ '@img/sharp-win32-x64': 0.35.3
+ '@types/node': 20.19.24
optional: true
shebang-command@2.0.0:
@@ -4383,12 +4508,12 @@ snapshots:
strip-json-comments@3.1.1: {}
- styled-jsx@5.1.6(@babel/core@7.28.5)(react@19.2.0):
+ styled-jsx@5.1.6(@babel/core@7.29.6)(react@19.2.8):
dependencies:
client-only: 0.0.1
- react: 19.2.0
+ react: 19.2.8
optionalDependencies:
- '@babel/core': 7.28.5
+ '@babel/core': 7.29.6
sucrase@3.35.0:
dependencies:
diff --git a/front/scripts/migrate-sort-order.mjs b/front/scripts/migrate-sort-order.mjs
index ad9af4a..3638b96 100644
--- a/front/scripts/migrate-sort-order.mjs
+++ b/front/scripts/migrate-sort-order.mjs
@@ -1,92 +1,8 @@
-// Migrate boss_option: add sort_order column and set city display order
-// Usage: pnpm exec node scripts/migrate-sort-order.mjs
-
-import fs from 'fs';
-import path from 'path';
-import { fileURLToPath } from 'url';
-import initSqlJs from 'sql.js';
-import { createRequire } from 'module';
-
-const log = (...args) => console.log('[migrate-sort-order]', ...args);
-
-async function main() {
- try {
- const __filename = fileURLToPath(import.meta.url);
- const __dirname = path.dirname(__filename);
- const projectRoot = path.resolve(__dirname, '..', '..');
- const dbPath = path.resolve(projectRoot, 'db', 'getjobs.db');
-
- if (!fs.existsSync(dbPath)) {
- throw new Error(`Database file not found: ${dbPath}`);
- }
-
- const require = createRequire(import.meta.url);
- const wasmDir = path.dirname(require.resolve('sql.js/dist/sql-wasm.wasm'));
- const SQL = await initSqlJs({ locateFile: (file) => path.join(wasmDir, file) });
-
- const fileBuffer = fs.readFileSync(dbPath);
- const u8 = new Uint8Array(fileBuffer);
- const db = new SQL.Database(u8);
-
- const hasColumn = (table, column) => {
- const res = db.exec(`PRAGMA table_info(${table});`);
- if (!res || res.length === 0) return false;
- const names = res[0].values.map((row) => String(row[1]).toLowerCase());
- return names.includes(String(column).toLowerCase());
- };
-
- // Ensure boss_option exists
- const tables = db.exec("SELECT name FROM sqlite_master WHERE type='table' AND name='boss_option';");
- if (!tables || tables.length === 0 || tables[0].values.length === 0) {
- throw new Error("Table 'boss_option' not found in database.");
- }
-
- // Add sort_order column if missing
- if (!hasColumn('boss_option', 'sort_order')) {
- log('Adding column sort_order to boss_option ...');
- db.exec('ALTER TABLE boss_option ADD COLUMN sort_order INTEGER;');
- } else {
- log('Column sort_order already exists.');
- }
-
- // Reset sort_order for cities
- db.exec("UPDATE boss_option SET sort_order = NULL WHERE type='city';");
-
- // Define preferred city display order (as requested)
- const cityOrder = [
- '全国', '北京', '上海', '广州', '深圳',
- '杭州', '天津', '西安', '苏州', '武汉',
- '厦门', '长沙', '成都', '郑州', '重庆'
- ];
-
- const stmt = db.prepare("UPDATE boss_option SET sort_order = ? WHERE type='city' AND name = ?;");
- let updated = 0;
- cityOrder.forEach((name, idx) => {
- stmt.run([idx + 1, name]);
- updated += 1;
- });
- stmt.free();
-
- log(`Applied sort_order to ${updated} city rows.`);
-
- // Persist changes back to file
- const out = db.export();
- fs.writeFileSync(dbPath, Buffer.from(out));
- log('Database updated:', dbPath);
-
- // Optional: verify a few rows
- const verify = db.exec("SELECT id, name, sort_order FROM boss_option WHERE type='city' AND sort_order IS NOT NULL ORDER BY sort_order ASC LIMIT 10;");
- if (verify && verify.length > 0) {
- const rows = verify[0].values.map((r) => ({ id: r[0], name: r[1], sort_order: r[2] }));
- log('Top cities after migration:', rows);
- }
-
- db.close();
- log('Migration completed successfully.');
- } catch (err) {
- console.error('[migrate-sort-order] Migration failed:', err);
- process.exitCode = 1;
- }
-}
-
-await main();
\ No newline at end of file
+// Historical migration entrypoint retained only to give old instructions a safe failure.
+// Schema changes are managed by backend Flyway migrations. Never overwrite a live SQLite file with sql.js.
+
+console.error(
+ '[migrate-sort-order] 已禁用:数据库结构现在只允许由后端 Flyway 管理。' +
+ '请停止服务、备份数据库,并通过正式 migration/rehearsal 流程升级。'
+)
+process.exitCode = 1
diff --git a/front/server.config.js b/front/server.config.js
index 2e90213..db198ab 100644
--- a/front/server.config.js
+++ b/front/server.config.js
@@ -21,7 +21,7 @@ module.exports = {
// 生产环境端口
port: 6866,
// 主机名
- hostname: '0.0.0.0',
+ hostname: process.env.FRONTEND_HOST || '127.0.0.1',
},
// API 配置(如果需要在构建时使用)
diff --git a/front/start-prod.mjs b/front/start-prod.mjs
index fe29bce..9d57b58 100644
--- a/front/start-prod.mjs
+++ b/front/start-prod.mjs
@@ -13,7 +13,7 @@ delete require.cache[require.resolve(configPath)];
const config = require(configPath);
const port = config.production?.port || config.port || 6866;
-const hostname = config.production?.hostname || '0.0.0.0';
+const hostname = process.env.FRONTEND_HOST || config.production?.hostname || '127.0.0.1';
const outDir = path.resolve(__dirname, 'out');
const contentTypes = new Map([
@@ -74,7 +74,17 @@ function contentTypeFor(filePath) {
}
const server = http.createServer((req, res) => {
- const filePath = resolveStaticFile(req.url);
+ let filePath;
+ try {
+ filePath = resolveStaticFile(req.url);
+ } catch (error) {
+ if (error instanceof URIError) {
+ res.writeHead(400, { 'Content-Type': 'text/plain; charset=utf-8' });
+ res.end('Bad Request');
+ return;
+ }
+ throw error;
+ }
const exists = isReadableFile(filePath);
res.writeHead(exists ? 200 : 404, {
'Content-Type': contentTypeFor(filePath),
diff --git a/scripts/run_backend.ps1 b/scripts/run_backend.ps1
index 597272e..9818adb 100644
--- a/scripts/run_backend.ps1
+++ b/scripts/run_backend.ps1
@@ -72,6 +72,9 @@ foreach ($dir in @($DbDir, $DataDir, $OutputDir, $CacheDir, $LogDir, $ChromeProf
if (-not $env:SPRING_DATASOURCE_URL) {
$env:SPRING_DATASOURCE_URL = "jdbc:sqlite:$(Join-Path $DbDir 'getjobs.db')"
}
+if (-not $env:SERVER_ADDRESS) {
+ $env:SERVER_ADDRESS = "127.0.0.1"
+}
if (-not $env:APP_DATA_DIR) {
$env:APP_DATA_DIR = $DataDir
}
diff --git a/sonar-project.properties b/sonar-project.properties
new file mode 100644
index 0000000..dcabe10
--- /dev/null
+++ b/sonar-project.properties
@@ -0,0 +1,16 @@
+sonar.projectKey=ai-jobpilot-v1-local-audit
+sonar.projectName=AI-JobPilot V1 Local Audit
+sonar.projectVersion=1.3.0-stable-v1-completion
+sonar.sourceEncoding=UTF-8
+
+sonar.sources=.
+sonar.tests=src/test/java,chrome-extension/tests
+sonar.exclusions=src/test/**,chrome-extension/tests/**,.git/**,.gradle/**,.pnpm-store/**,**/node_modules/**,front/.next/**,front/out/**,build/**,target/**,bin/**,logs/**,output/**,data/**,db/**,chrome-profile/**,AI-JobPilot/**,AI-JobPilot-boss-api-poc/**,.codemap/**,tasks/**,doc/**,docs/**,demo/**,front/public/**,chrome-extension/icons/**,front/pnpm-lock.yaml,**/*.jar,**/*.md,**/*.png,**/*.jpg,**/*.jpeg,**/*.gif,**/*.svg
+sonar.test.inclusions=src/test/java/**/*.java,chrome-extension/tests/**/*.cjs
+
+sonar.java.binaries=build/classes/java/main
+sonar.java.test.binaries=build/classes/java/test
+sonar.coverage.jacoco.xmlReportPaths=build/reports/jacoco/test/jacocoTestReport.xml
+sonar.javascript.lcov.reportPaths=target/audit/extension-lcov.info
+sonar.typescript.tsconfigPaths=front/tsconfig.json
+sonar.scm.provider=git
diff --git a/src/main/java/com/getjobs/application/config/CorsConfig.java b/src/main/java/com/getjobs/application/config/CorsConfig.java
index 6713cc4..199f0c2 100644
--- a/src/main/java/com/getjobs/application/config/CorsConfig.java
+++ b/src/main/java/com/getjobs/application/config/CorsConfig.java
@@ -32,7 +32,7 @@ public CorsFilter corsFilter() {
CorsConfiguration bossExtensionConfig = baseConfiguration();
bossExtensionConfig.setAllowedOrigins(LOCAL_FRONTEND_ORIGINS);
- bossExtensionConfig.addAllowedOriginPattern("chrome-extension://*");
+ // Chrome 扩展后台依靠 manifest host_permissions 访问本机 API;服务端不再信任任意扩展 ID。
// 必须先注册更具体的扩展接口,再注册全局本地前端规则。
for (String path : BOSS_EXTENSION_API_PATHS) {
diff --git a/src/main/java/com/getjobs/application/config/StaticResourceConfiguration.java b/src/main/java/com/getjobs/application/config/StaticResourceConfiguration.java
index cb98ec2..ba307b0 100644
--- a/src/main/java/com/getjobs/application/config/StaticResourceConfiguration.java
+++ b/src/main/java/com/getjobs/application/config/StaticResourceConfiguration.java
@@ -62,42 +62,7 @@ public void addResourceHandlers(ResourceHandlerRegistry registry) {
)
.setCachePeriod(0)
.resourceChain(true)
- .addResolver(new PathResourceResolver() {
- @Override
- protected Resource getResource(String resourcePath, Resource location) {
- try {
- Resource requestedResource = location.createRelative(resourcePath);
-
- // 如果请求的资源存在,直接返回
- if (requestedResource.exists() && requestedResource.isReadable()) {
- return requestedResource;
- }
-
- // Next.js 静态导出:无后缀路径优先匹配同名 .html / .txt
- if (!resourcePath.startsWith("api/") && !resourcePath.contains(".")) {
- // 1) 尝试同名 .html
- Resource htmlResource = location.createRelative(resourcePath + ".html");
- if (htmlResource.exists() && htmlResource.isReadable()) {
- return htmlResource;
- }
- // 2) 尝试同名 .txt(App Router RSC 数据)
- Resource txtResource = location.createRelative(resourcePath + ".txt");
- if (txtResource.exists() && txtResource.isReadable()) {
- return txtResource;
- }
- // 3) 兜底返回 index.html(SPA fallback)
- Resource indexResource = location.createRelative("index.html");
- if (indexResource.exists() && indexResource.isReadable()) {
- return indexResource;
- }
- }
- } catch (IOException e) {
- // 返回 null
- }
-
- return null;
- }
- });
+ .addResolver(new BoundedStaticPathResourceResolver());
} else {
log.warn("未找到静态资源目录 (dist 或 static)");
}
@@ -145,4 +110,50 @@ private boolean detectFrontendService() {
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("forward:/index.html");
}
+
+ static final class BoundedStaticPathResourceResolver extends PathResourceResolver {
+ @Override
+ protected Resource getResource(String resourcePath, Resource location) {
+ try {
+ Resource requestedResource = location.createRelative(resourcePath);
+ if (isReadableWithinLocation(requestedResource, location)) {
+ return requestedResource;
+ }
+
+ if (!resourcePath.startsWith("api/") && !resourcePath.contains(".")) {
+ Resource htmlResource = location.createRelative(resourcePath + ".html");
+ if (isReadableWithinLocation(htmlResource, location)) {
+ return htmlResource;
+ }
+ Resource txtResource = location.createRelative(resourcePath + ".txt");
+ if (isReadableWithinLocation(txtResource, location)) {
+ return txtResource;
+ }
+ Resource indexResource = location.createRelative("index.html");
+ if (isReadableWithinLocation(indexResource, location)) {
+ return indexResource;
+ }
+ }
+ } catch (IOException | RuntimeException ignored) {
+ // 无法证明位于静态根目录时按不存在处理,不向响应暴露本地路径。
+ }
+ return null;
+ }
+
+ Resource resolveForTest(String resourcePath, Resource location) {
+ return getResource(resourcePath, location);
+ }
+
+ private boolean isReadableWithinLocation(Resource resource, Resource location) throws IOException {
+ if (!resource.exists() || !resource.isReadable() || !checkResource(resource, location)) {
+ return false;
+ }
+ if (resource.isFile() && location.isFile()) {
+ Path root = location.getFile().toPath().toRealPath();
+ Path candidate = resource.getFile().toPath().toRealPath();
+ return candidate.startsWith(root);
+ }
+ return true;
+ }
+ }
}
diff --git a/src/main/java/com/getjobs/application/controller/AiConfigController.java b/src/main/java/com/getjobs/application/controller/AiConfigController.java
index 42356f2..4b4743e 100644
--- a/src/main/java/com/getjobs/application/controller/AiConfigController.java
+++ b/src/main/java/com/getjobs/application/controller/AiConfigController.java
@@ -2,6 +2,8 @@
import com.getjobs.application.entity.AiEntity;
import com.getjobs.application.service.AiService;
+import com.getjobs.application.service.ChromeJobAnalysisQueueService;
+import com.getjobs.application.service.JobAnalysisTaskStore;
import com.getjobs.application.service.JobAiAnalysisService;
import com.getjobs.application.service.ProfileService;
import lombok.Data;
@@ -34,6 +36,9 @@ public class AiConfigController {
@Autowired
private ProfileService profileService;
+ @Autowired
+ private ChromeJobAnalysisQueueService chromeJobAnalysisQueueService;
+
/**
* 获取AI配置
* @return AI配置信息
@@ -245,10 +250,13 @@ public ResponseEntity | | |