fix(default): resolve v1 frontend issue regressions

Fix v1 frontend regressions across channel forms, dashboard charts, wallet history, payment callbacks, invite links, API key groups, rate-limit errors, and usage-log scrolling.

Fixes #4715
Fixes #4618
Fixes #4699
Fixes #4651
Fixes #4637
Fixes #4682
Fixes #4691
Fixes #4565
Fixes #4334
This commit is contained in:
CaIon 2026-05-11 11:25:05 +08:00
parent 5fa103fa5b
commit ba474393fb
No known key found for this signature in database
GPG Key ID: 0CFA613529A9921D
27 changed files with 267 additions and 41 deletions

View File

@ -401,9 +401,6 @@ func GetChannelById(id int, selectAll bool) (*Channel, error) {
if err != nil {
return nil, err
}
if channel == nil {
return nil, errors.New("channel not found")
}
return channel, nil
}
@ -758,7 +755,7 @@ func EditChannelByTag(tag string, newTag *string, modelMapping *string, models *
updateData.Tag = newTag
updatedTag = *newTag
}
if modelMapping != nil && *modelMapping != "" {
if modelMapping != nil {
updateData.ModelMapping = modelMapping
}
if models != nil && *models != "" {

View File

@ -36,6 +36,7 @@ interface ComboboxInputProps {
emptyText?: string
className?: string
id?: string
allowCustomValue?: boolean
}
export function ComboboxInput({
@ -46,23 +47,30 @@ export function ComboboxInput({
emptyText = 'No option found.',
className,
id,
allowCustomValue = false,
}: ComboboxInputProps) {
const { t } = useTranslation()
const [open, setOpen] = React.useState(false)
const [searchValue, setSearchValue] = React.useState('')
const [highlightedIndex, setHighlightedIndex] = React.useState(-1)
const containerRef = React.useRef<HTMLDivElement>(null)
const inputRef = React.useRef<HTMLInputElement>(null)
const listRef = React.useRef<HTMLUListElement>(null)
const selectedOption = React.useMemo(
() => options.find((option) => option.value === value),
[options, value]
)
const displayValue = open ? searchValue : (selectedOption?.label ?? value)
const filteredOptions = React.useMemo(() => {
if (!value.trim()) return options
const search = value.toLowerCase().trim()
if (!searchValue.trim()) return options
const search = searchValue.toLowerCase().trim()
return options.filter(
(option) =>
option.label.toLowerCase().includes(search) ||
option.value.toLowerCase().includes(search)
)
}, [options, value])
}, [options, searchValue])
// Reset highlight when filtered options change
React.useEffect(() => {
@ -79,6 +87,7 @@ export function ComboboxInput({
!containerRef.current.contains(e.target as Node)
) {
setOpen(false)
setSearchValue('')
}
}
@ -89,6 +98,7 @@ export function ComboboxInput({
const handleSelect = (selectedValue: string) => {
onValueChange(selectedValue)
setOpen(false)
setSearchValue('')
inputRef.current?.focus()
}
@ -117,14 +127,18 @@ export function ComboboxInput({
e.preventDefault()
if (highlightedIndex >= 0 && filteredOptions[highlightedIndex]) {
handleSelect(filteredOptions[highlightedIndex].value)
} else if (allowCustomValue && searchValue.trim()) {
handleSelect(searchValue.trim())
} else {
// No highlighted option, just close the dropdown and keep current value
setOpen(false)
setSearchValue('')
}
break
case 'Escape':
e.preventDefault()
setOpen(false)
setSearchValue('')
break
}
}
@ -136,7 +150,9 @@ export function ComboboxInput({
item?.scrollIntoView({ block: 'nearest' })
}, [highlightedIndex])
const showDropdown = open && (filteredOptions.length > 0 || value.trim())
const showDropdown =
open &&
(filteredOptions.length > 0 || (allowCustomValue && searchValue.trim()))
return (
<div ref={containerRef} className='relative'>
@ -150,12 +166,19 @@ export function ComboboxInput({
aria-autocomplete='list'
autoComplete='off'
placeholder={placeholder}
value={value}
value={displayValue}
onChange={(e) => {
onValueChange(e.target.value)
const nextValue = e.target.value
setSearchValue(nextValue)
if (allowCustomValue) {
onValueChange(nextValue)
}
if (!open) setOpen(true)
}}
onFocus={() => setOpen(true)}
onFocus={() => {
setSearchValue(allowCustomValue ? value : '')
setOpen(true)
}}
onKeyDown={handleKeyDown}
className={cn('pr-9', className)}
/>
@ -200,10 +223,12 @@ export function ComboboxInput({
</ul>
) : (
<div className='px-2 py-6 text-center text-sm'>
{emptyText}
{value.trim() && (
{t(emptyText)}
{allowCustomValue && searchValue.trim() && (
<div className='text-muted-foreground mt-1 text-xs'>
{t('Press Enter to use "{{value}}"', { value: value.trim() })}
{t('Press Enter to use "{{value}}"', {
value: searchValue.trim(),
})}
</div>
)}
</div>

View File

@ -68,6 +68,7 @@ function Combobox(
placeholder={props.searchPlaceholder ?? props.placeholder}
emptyText={props.emptyText}
className={props.className}
allowCustomValue={props.allowCustomValue}
/>
)
}

View File

@ -484,6 +484,12 @@ export function transformFormDataToUpdatePayload(
}
})
// Send explicit empty strings for nullable JSON/text fields so GORM updates can clear them.
payload.model_mapping = formData.model_mapping || ''
payload.status_code_mapping = formData.status_code_mapping || ''
payload.param_override = formData.param_override || ''
payload.header_override = formData.header_override || ''
return payload
}

View File

@ -143,6 +143,12 @@ function replaceToken(source: string, token: string, value: string) {
return source.split(token).join(value)
}
function normalizeApiKey(apiKey: string): string {
const trimmed = apiKey.trim()
if (!trimmed) return ''
return trimmed.startsWith('sk-') ? trimmed : `sk-${trimmed}`
}
export function resolveChatUrl({
template,
apiKey,
@ -151,7 +157,7 @@ export function resolveChatUrl({
let url = template
const safeServerAddress = serverAddress || ''
const safeApiKey = apiKey || ''
const safeApiKey = normalizeApiKey(apiKey || '')
if (url.includes('{cherryConfig}')) {
const payload = {

View File

@ -115,6 +115,15 @@ export function ConsumptionDistributionChart(
]
)
const spec = chartType === 'bar' ? chartData.spec_line : chartData.spec_area
const specType = typeof spec?.type === 'string' ? spec.type : chartType
const chartKey = [
chartType,
specType,
props.loading ? 'loading' : 'ready',
props.data.length,
resolvedTheme,
customization.preset,
].join('-')
return (
<div className='overflow-hidden rounded-lg border'>
@ -152,7 +161,7 @@ export function ConsumptionDistributionChart(
<div className='h-[300px] p-1.5 sm:h-96 sm:p-2'>
{themeReady && spec && (
<VChart
key={`${chartType}-${resolvedTheme}-${customization.preset}`}
key={chartKey}
spec={{
...spec,
theme: resolvedTheme === 'dark' ? 'dark' : 'light',

View File

@ -114,6 +114,15 @@ export function ModelCharts(props: ModelChartsProps) {
)
const spec = chartData[CHART_SPEC_KEYS[activeTab]]
const specType = typeof spec?.type === 'string' ? spec.type : activeTab
const chartKey = [
activeTab,
specType,
props.loading ? 'loading' : 'ready',
props.data.length,
resolvedTheme,
customization.preset,
].join('-')
return (
<div className='overflow-hidden rounded-lg border'>
@ -149,7 +158,7 @@ export function ModelCharts(props: ModelChartsProps) {
<div className='h-[300px] p-1.5 sm:h-96 sm:p-2'>
{themeReady && spec && (
<VChart
key={`${activeTab}-${resolvedTheme}-${customization.preset}`}
key={chartKey}
spec={{
...spec,
theme: resolvedTheme === 'dark' ? 'dark' : 'light',

View File

@ -212,7 +212,7 @@ export function processChartData(
legends: { visible: true, selectMode: 'single' },
},
spec_model_line: {
type: 'line',
type: 'area',
data: [{ id: 'lineData', values: [] }],
xField: 'Time',
yField: 'Count',

View File

@ -25,27 +25,45 @@ const FEEDBACK_URL = 'https://github.com/QuantumNous/new-api/issues'
type GeneralErrorProps = React.HTMLAttributes<HTMLDivElement> & {
minimal?: boolean
error?: unknown
}
function getHttpStatus(error: unknown): number | undefined {
if (typeof error !== 'object' || error === null) return undefined
const response = (error as Record<string, unknown>).response
if (typeof response !== 'object' || response === null) return undefined
const status = (response as Record<string, unknown>).status
return typeof status === 'number' ? status : undefined
}
export function GeneralError({
className,
minimal = false,
error,
}: GeneralErrorProps) {
const { t } = useTranslation()
const navigate = useNavigate()
const { history } = useRouter()
const status = getHttpStatus(error)
const isRateLimited = status === 429
const title = isRateLimited
? t('Too many requests')
: `${t('Oops! Something went wrong')} ${`:')`}`
const description = isRateLimited
? t('Please wait a moment before trying again.')
: t('Please try again later.')
return (
<div className={cn('h-svh w-full', className)}>
<div className='m-auto flex h-full w-full flex-col items-center justify-center gap-2'>
{!minimal && (
<h1 className='text-[7rem] leading-tight font-bold'>500</h1>
<h1 className='text-[7rem] leading-tight font-bold'>
{status ?? 500}
</h1>
)}
<span className='font-medium'>
{t('Oops! Something went wrong')} {`:')`}
</span>
<span className='font-medium'>{title}</span>
<p className='text-muted-foreground text-center'>
{t('We apologize for the inconvenience.')} <br />{' '}
{t('Please try again later.')}
{t('We apologize for the inconvenience.')} <br /> {description}
</p>
{!minimal && (
<p className='text-muted-foreground text-center text-sm'>

View File

@ -145,7 +145,7 @@ export function ApiKeyGroupCombobox({
<span className='flex min-w-0 flex-1 items-center justify-between gap-2 sm:gap-3'>
<span className='min-w-0'>
<span className='block truncate font-medium'>
{selectedOption?.value || placeholder || t('Select a group')}
{selectedOption?.label || placeholder || t('Select a group')}
</span>
{selectedOption?.desc && (
<span className='text-muted-foreground block truncate text-[11px] sm:text-xs'>
@ -178,7 +178,7 @@ export function ApiKeyGroupCombobox({
<CommandItem
key={option.value}
value={option.value}
onSelect={handleSelect}
onSelect={() => handleSelect(option.value)}
className='data-[selected=true]:bg-muted items-start gap-3 rounded-lg px-3 py-3 transition-colors'
>
<Check
@ -189,7 +189,7 @@ export function ApiKeyGroupCombobox({
/>
<span className='min-w-0 flex-1'>
<span className='block truncate font-medium'>
{option.value}
{option.label}
</span>
{option.desc && (
<span className='text-muted-foreground block truncate text-xs'>

View File

@ -126,7 +126,7 @@ export function RateLimitSection({ defaultValues }: RateLimitSectionProps) {
</FormLabel>
<FormDescription>
{t(
'Restrict user model request frequency (may impact high concurrency performance)'
'This controls model request rate limiting. Web/API route throttling is configured by environment variables and may still return 429.'
)}
</FormDescription>
</div>

View File

@ -171,6 +171,7 @@ export function UsageLogsTable({ logCategory }: UsageLogsTableProps) {
'No usage logs available. Logs will appear here once API calls are made.'
)}
skeletonKeyPrefix='usage-log-skeleton'
tableClassName='max-h-[calc(100dvh-13rem)] overflow-auto sm:max-h-[calc(100dvh-14rem)]'
tableHeaderClassName='bg-muted/30 sticky top-0 z-10'
toolbar={
isCommon ? (

View File

@ -176,8 +176,8 @@ export function BillingHistoryDialog({
</p>
<p className='mt-1 text-xs'>
{keyword
? 'Try adjusting your search'
: 'Your transaction history will appear here'}
? t('Try adjusting your search')
: t('Your transaction history will appear here')}
</p>
</div>
) : (
@ -233,15 +233,15 @@ export function BillingHistoryDialog({
<div className='mt-3 grid grid-cols-2 gap-3 sm:mt-4 sm:grid-cols-3 sm:gap-4'>
<div className='space-y-1'>
<Label className='text-muted-foreground text-xs'>
Payment Method
{t('Payment Method')}
</Label>
<div className='text-sm font-medium'>
{getPaymentMethodName(record.payment_method)}
{getPaymentMethodName(record.payment_method, t)}
</div>
</div>
<div className='space-y-1'>
<Label className='text-muted-foreground text-xs'>
Amount
{t('Amount')}
</Label>
<div className='text-sm font-semibold'>
{formatCurrencyFromUSD(record.amount, {
@ -253,7 +253,7 @@ export function BillingHistoryDialog({
</div>
<div className='space-y-1'>
<Label className='text-muted-foreground text-xs'>
Payment
{t('Payment')}
</Label>
<div className='text-sm font-semibold text-red-600'>
{formatNumber(record.money)}
@ -270,7 +270,7 @@ export function BillingHistoryDialog({
onClick={() => setConfirmTradeNo(record.trade_no)}
disabled={completing}
>
Complete Order
{t('Complete Order')}
</Button>
</div>
)}
@ -341,7 +341,7 @@ export function BillingHistoryDialog({
onClick={handleConfirmComplete}
disabled={completing}
>
{completing ? 'Processing...' : 'Confirm'}
{completing ? t('Processing...') : t('Confirm')}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>

View File

@ -67,8 +67,12 @@ export const PAYMENT_METHOD_NAMES: Record<string, string> = {
/**
* Get payment method display name
*/
export function getPaymentMethodName(method: string): string {
return PAYMENT_METHOD_NAMES[method] || method
export function getPaymentMethodName(
method: string,
t?: (key: string) => string
): string {
const name = PAYMENT_METHOD_NAMES[method] || method
return t ? t(name) : name
}
/**

View File

@ -17,13 +17,13 @@
"file": "ja.json",
"missingCount": 0,
"extrasCount": 0,
"untranslatedCount": 90
"untranslatedCount": 92
},
"ru": {
"file": "ru.json",
"missingCount": 0,
"extrasCount": 0,
"untranslatedCount": 105
"untranslatedCount": 107
},
"vi": {
"file": "vi.json",

View File

@ -88,5 +88,7 @@
"Webhook URL:": "Webhook URL:",
"whsec_xxx": "whsec_xxx",
"Xinference": "Xinference",
"Xunfei": "Xunfei"
"Xunfei": "Xunfei",
"Alipay": "Alipay",
"WeChat Pay": "WeChat Pay"
}

View File

@ -103,5 +103,7 @@
"whsec_xxx": "whsec_xxx",
"Xinference": "Xinference",
"Xunfei": "Xunfei",
"Alipay": "Alipay",
"WeChat Pay": "WeChat Pay",
"Zhipu V4": "Zhipu V4"
}

View File

@ -4400,6 +4400,13 @@
"Your Telegram Bot Token": "Your Telegram Bot Token",
"Your Turnstile secret key": "Your Turnstile secret key",
"Your Turnstile site key": "Your Turnstile site key",
"Alipay": "Alipay",
"Please wait a moment before trying again.": "Please wait a moment before trying again.",
"This controls model request rate limiting. Web/API route throttling is configured by environment variables and may still return 429.": "This controls model request rate limiting. Web/API route throttling is configured by environment variables and may still return 429.",
"Too many requests": "Too many requests",
"Try adjusting your search": "Try adjusting your search",
"WeChat Pay": "WeChat Pay",
"Your transaction history will appear here": "Your transaction history will appear here",
"Zero retention": "Zero retention",
"Zhipu": "Zhipu",
"Zhipu V4": "Zhipu V4",

View File

@ -4400,6 +4400,13 @@
"Your Telegram Bot Token": "Votre Jeton de Bot Telegram",
"Your Turnstile secret key": "Votre clé secrète Turnstile",
"Your Turnstile site key": "Votre clé de site Turnstile",
"Alipay": "Alipay",
"Please wait a moment before trying again.": "Veuillez patienter un instant avant de réessayer.",
"This controls model request rate limiting. Web/API route throttling is configured by environment variables and may still return 429.": "Ce réglage contrôle la limitation des requêtes de modèles. La limitation des routes Web/API se configure via les variables d'environnement et peut encore renvoyer 429.",
"Too many requests": "Trop de requêtes",
"Try adjusting your search": "Essayez d'ajuster votre recherche",
"WeChat Pay": "WeChat Pay",
"Your transaction history will appear here": "Votre historique de transactions apparaîtra ici",
"Zero retention": "Aucune rétention",
"Zhipu": "Zhipu",
"Zhipu V4": "Zhipu V4",

View File

@ -4400,6 +4400,13 @@
"Your Telegram Bot Token": "あなたのTelegramボットトークン",
"Your Turnstile secret key": "あなたのTurnstileシークレットキー",
"Your Turnstile site key": "あなたのTurnstileサイトキー",
"Alipay": "Alipay",
"Please wait a moment before trying again.": "しばらく待ってからもう一度お試しください。",
"This controls model request rate limiting. Web/API route throttling is configured by environment variables and may still return 429.": "これはモデルリクエストのレート制限を制御します。Web/API ルートのスロットリングは環境変数で設定され、引き続き 429 を返す場合があります。",
"Too many requests": "リクエストが多すぎます",
"Try adjusting your search": "検索条件を調整してみてください",
"WeChat Pay": "WeChat Pay",
"Your transaction history will appear here": "取引履歴はここに表示されます",
"Zero retention": "データ保持なし",
"Zhipu": "Zhipu",
"Zhipu V4": "Zhipu V 4",

View File

@ -4400,6 +4400,13 @@
"Your Telegram Bot Token": "Ваш токен Telegram-бота",
"Your Turnstile secret key": "Секретный ключ Turnstile",
"Your Turnstile site key": "Ключ сайта Turnstile",
"Alipay": "Alipay",
"Please wait a moment before trying again.": "Пожалуйста, подождите немного и попробуйте снова.",
"This controls model request rate limiting. Web/API route throttling is configured by environment variables and may still return 429.": "Этот параметр управляет ограничением частоты запросов к моделям. Ограничение маршрутов Web/API настраивается переменными окружения и всё ещё может возвращать 429.",
"Too many requests": "Слишком много запросов",
"Try adjusting your search": "Попробуйте изменить условия поиска",
"WeChat Pay": "WeChat Pay",
"Your transaction history will appear here": "Ваша история транзакций появится здесь",
"Zero retention": "Без хранения данных",
"Zhipu": "Zhipu",
"Zhipu V4": "Zhipu V4",

View File

@ -4400,6 +4400,13 @@
"Your Telegram Bot Token": "Mã thông báo bot Telegram của bạn",
"Your Turnstile secret key": "Khóa bí mật Turnstile của bạn",
"Your Turnstile site key": "Khóa site Turnstile của bạn",
"Alipay": "Alipay",
"Please wait a moment before trying again.": "Vui lòng chờ một lát rồi thử lại.",
"This controls model request rate limiting. Web/API route throttling is configured by environment variables and may still return 429.": "Thiết lập này kiểm soát giới hạn tốc độ yêu cầu mô hình. Giới hạn tuyến Web/API được cấu hình bằng biến môi trường và vẫn có thể trả về 429.",
"Too many requests": "Quá nhiều yêu cầu",
"Try adjusting your search": "Hãy thử điều chỉnh tìm kiếm",
"WeChat Pay": "WeChat Pay",
"Your transaction history will appear here": "Lịch sử giao dịch của bạn sẽ xuất hiện ở đây",
"Zero retention": "Không lưu dữ liệu",
"Zhipu": "Zhipu",
"Zhipu V4": "Zhipu V4",

View File

@ -4400,6 +4400,13 @@
"Your Telegram Bot Token": "您的 Telegram 机器人令牌",
"Your Turnstile secret key": "您的 Turnstile 密钥",
"Your Turnstile site key": "您的 Turnstile 站点密钥",
"Alipay": "支付宝",
"Please wait a moment before trying again.": "请稍候再试。",
"This controls model request rate limiting. Web/API route throttling is configured by environment variables and may still return 429.": "此处仅控制模型请求速率限制。Web/API 路由限流由环境变量配置,仍可能返回 429。",
"Too many requests": "请求过于频繁",
"Try adjusting your search": "请尝试调整搜索条件",
"WeChat Pay": "微信支付",
"Your transaction history will appear here": "您的交易历史会显示在这里",
"Zero retention": "零数据保留",
"Zhipu": "智谱",
"Zhipu V4": "智谱 V4",

View File

@ -19,6 +19,8 @@ import { Route as RankingsIndexRouteImport } from './routes/rankings/index'
import { Route as PricingIndexRouteImport } from './routes/pricing/index'
import { Route as AboutIndexRouteImport } from './routes/about/index'
import { Route as OauthProviderRouteImport } from './routes/oauth/$provider'
import { Route as ConsoleTopupRouteImport } from './routes/console/topup'
import { Route as ConsoleLogRouteImport } from './routes/console/log'
import { Route as AuthenticatedChat2linkRouteImport } from './routes/_authenticated/chat2link'
import { Route as errors503RouteImport } from './routes/(errors)/503'
import { Route as errors500RouteImport } from './routes/(errors)/500'
@ -114,6 +116,16 @@ const OauthProviderRoute = OauthProviderRouteImport.update({
path: '/oauth/$provider',
getParentRoute: () => rootRouteImport,
} as any)
const ConsoleTopupRoute = ConsoleTopupRouteImport.update({
id: '/console/topup',
path: '/console/topup',
getParentRoute: () => rootRouteImport,
} as any)
const ConsoleLogRoute = ConsoleLogRouteImport.update({
id: '/console/log',
path: '/console/log',
getParentRoute: () => rootRouteImport,
} as any)
const AuthenticatedChat2linkRoute = AuthenticatedChat2linkRouteImport.update({
id: '/chat2link',
path: '/chat2link',
@ -391,6 +403,8 @@ export interface FileRoutesByFullPath {
'/500': typeof errors500Route
'/503': typeof errors503Route
'/chat2link': typeof AuthenticatedChat2linkRoute
'/console/log': typeof ConsoleLogRoute
'/console/topup': typeof ConsoleTopupRoute
'/oauth/$provider': typeof OauthProviderRoute
'/about/': typeof AboutIndexRoute
'/pricing/': typeof PricingIndexRoute
@ -446,6 +460,8 @@ export interface FileRoutesByTo {
'/500': typeof errors500Route
'/503': typeof errors503Route
'/chat2link': typeof AuthenticatedChat2linkRoute
'/console/log': typeof ConsoleLogRoute
'/console/topup': typeof ConsoleTopupRoute
'/oauth/$provider': typeof OauthProviderRoute
'/about': typeof AboutIndexRoute
'/pricing': typeof PricingIndexRoute
@ -505,6 +521,8 @@ export interface FileRoutesById {
'/(errors)/500': typeof errors500Route
'/(errors)/503': typeof errors503Route
'/_authenticated/chat2link': typeof AuthenticatedChat2linkRoute
'/console/log': typeof ConsoleLogRoute
'/console/topup': typeof ConsoleTopupRoute
'/oauth/$provider': typeof OauthProviderRoute
'/about/': typeof AboutIndexRoute
'/pricing/': typeof PricingIndexRoute
@ -563,6 +581,8 @@ export interface FileRouteTypes {
| '/500'
| '/503'
| '/chat2link'
| '/console/log'
| '/console/topup'
| '/oauth/$provider'
| '/about/'
| '/pricing/'
@ -618,6 +638,8 @@ export interface FileRouteTypes {
| '/500'
| '/503'
| '/chat2link'
| '/console/log'
| '/console/topup'
| '/oauth/$provider'
| '/about'
| '/pricing'
@ -676,6 +698,8 @@ export interface FileRouteTypes {
| '/(errors)/500'
| '/(errors)/503'
| '/_authenticated/chat2link'
| '/console/log'
| '/console/topup'
| '/oauth/$provider'
| '/about/'
| '/pricing/'
@ -727,6 +751,8 @@ export interface RootRouteChildren {
errors404Route: typeof errors404Route
errors500Route: typeof errors500Route
errors503Route: typeof errors503Route
ConsoleLogRoute: typeof ConsoleLogRoute
ConsoleTopupRoute: typeof ConsoleTopupRoute
OauthProviderRoute: typeof OauthProviderRoute
AboutIndexRoute: typeof AboutIndexRoute
PricingIndexRoute: typeof PricingIndexRoute
@ -807,6 +833,20 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof OauthProviderRouteImport
parentRoute: typeof rootRouteImport
}
'/console/topup': {
id: '/console/topup'
path: '/console/topup'
fullPath: '/console/topup'
preLoaderRoute: typeof ConsoleTopupRouteImport
parentRoute: typeof rootRouteImport
}
'/console/log': {
id: '/console/log'
path: '/console/log'
fullPath: '/console/log'
preLoaderRoute: typeof ConsoleLogRouteImport
parentRoute: typeof rootRouteImport
}
'/_authenticated/chat2link': {
id: '/_authenticated/chat2link'
path: '/chat2link'
@ -1271,6 +1311,8 @@ const rootRouteChildren: RootRouteChildren = {
errors404Route: errors404Route,
errors500Route: errors500Route,
errors503Route: errors503Route,
ConsoleLogRoute: ConsoleLogRoute,
ConsoleTopupRoute: ConsoleTopupRoute,
OauthProviderRoute: OauthProviderRoute,
AboutIndexRoute: AboutIndexRoute,
PricingIndexRoute: PricingIndexRoute,

View File

@ -16,6 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { useEffect } from 'react'
import { type QueryClient } from '@tanstack/react-query'
import {
createRootRouteWithContext,
@ -31,11 +32,19 @@ import { NavigationProgress } from '@/components/navigation-progress'
import { GeneralError } from '@/features/errors/general-error'
import { NotFoundError } from '@/features/errors/not-found-error'
import { getSetupStatus } from '@/features/setup/api'
import { saveAffiliateCode } from '@/features/auth/lib/storage'
function RootComponent() {
// Load system configuration (logo, system name, etc.) from backend
useSystemConfig({ autoLoad: true })
useEffect(() => {
const aff = new URLSearchParams(window.location.search).get('aff')?.trim()
if (aff) {
saveAffiliateCode(aff)
}
}, [])
return (
<ThemeCustomizationProvider>
<NavigationProgress />

25
web/default/src/routes/console/log.tsx vendored Normal file
View File

@ -0,0 +1,25 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { createFileRoute, redirect } from '@tanstack/react-router'
export const Route = createFileRoute('/console/log')({
beforeLoad: () => {
throw redirect({ to: '/usage-logs' })
},
})

View File

@ -0,0 +1,28 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { createFileRoute, redirect } from '@tanstack/react-router'
export const Route = createFileRoute('/console/topup')({
beforeLoad: () => {
throw redirect({
to: '/wallet',
search: { show_history: true },
})
},
})