feat: sync deployed v1.0.0-rc.38-custom.42
Sync production source including routing, billing, Astra detection and public uptime. Update custom build instructions and license packaging.
This commit is contained in:
1 parent
aa6c3f50f4
commit
ceddfd3d6b
828 files changed
+122253
-13702
No files matched your search
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
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 { renderHook } from '@testing-library/react'
|
||||
import { afterEach, expect, test, vi } from 'vitest'
|
||||
|
||||
import { useRedirectPanelPlacement } from '../use-redirect-panel-placement'
|
||||
|
||||
function mockSheet(rect: { left: number; right: number }, innerWidth: number) {
|
||||
Object.defineProperty(window, 'innerWidth', {
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: innerWidth,
|
||||
})
|
||||
const anchor = document.createElement('div')
|
||||
anchor.dataset.slot = 'sheet-content'
|
||||
document.body.append(anchor)
|
||||
vi.spyOn(anchor, 'getBoundingClientRect').mockReturnValue({
|
||||
x: rect.left,
|
||||
y: 0,
|
||||
left: rect.left,
|
||||
top: 0,
|
||||
right: rect.right,
|
||||
bottom: 800,
|
||||
width: rect.right - rect.left,
|
||||
height: 800,
|
||||
toJSON: () => ({}),
|
||||
} as DOMRect)
|
||||
return { current: anchor }
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = ''
|
||||
Object.defineProperty(window, 'innerWidth', {
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: 1024,
|
||||
})
|
||||
})
|
||||
|
||||
test('a left-hand sheet starts the window in the free space on its right', () => {
|
||||
const ref = mockSheet({ left: 0, right: 900 }, 1920)
|
||||
const { result } = renderHook(() =>
|
||||
useRedirectPanelPlacement(ref, true, 'left')
|
||||
)
|
||||
expect(result.current).toEqual({ x: 916, y: 16, width: 420 })
|
||||
})
|
||||
|
||||
test('a right-hand sheet starts the window on its left, overlapping the sheet when the viewport has no room beside it', () => {
|
||||
const roomy = mockSheet({ left: 1000, right: 1920 }, 1920)
|
||||
expect(
|
||||
renderHook(() => useRedirectPanelPlacement(roomy, true, 'right')).result
|
||||
.current
|
||||
).toEqual({ x: 564, y: 16, width: 420 })
|
||||
const cramped = mockSheet({ left: 100, right: 1296 }, 1296)
|
||||
expect(
|
||||
renderHook(() => useRedirectPanelPlacement(cramped, true, 'right')).result
|
||||
.current
|
||||
).toEqual({ x: 16, y: 16, width: 420 })
|
||||
})
|
||||
|
||||
test('there is no placement on narrow viewports or while disabled', () => {
|
||||
const narrow = mockSheet({ left: 0, right: 700 }, 1200)
|
||||
expect(
|
||||
renderHook(() => useRedirectPanelPlacement(narrow, true, 'left')).result
|
||||
.current
|
||||
).toBe(null)
|
||||
const disabled = mockSheet({ left: 0, right: 900 }, 1920)
|
||||
expect(
|
||||
renderHook(() => useRedirectPanelPlacement(disabled, false, 'left')).result
|
||||
.current
|
||||
).toBe(null)
|
||||
})
|
||||
@@ -21,7 +21,9 @@ import { useTranslation } from 'react-i18next'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { useSecureVerification } from '@/features/auth/secure-verification'
|
||||
import { handleServerError } from '@/lib/handle-server-error'
|
||||
import { AuthOperationError } from '@/lib/secure-verification'
|
||||
import { createServerError } from '@/lib/server-error-message'
|
||||
|
||||
import { getChannelKey } from '../api'
|
||||
|
||||
@@ -72,13 +74,13 @@ export function useChannelKeyDisclosure(
|
||||
)
|
||||
if (operation.current !== current) return
|
||||
if (!res.success) {
|
||||
throw new Error(res.message || t('Failed to fetch channel key'))
|
||||
throw createServerError(res, t('Failed to fetch channel key'))
|
||||
}
|
||||
setDisclosedKey({ channelId, key: res.data?.key ?? '' })
|
||||
toast.success(t('Channel key unlocked'))
|
||||
} catch (error) {
|
||||
if (operation.current === current && !current.signal.aborted) {
|
||||
toast.error(t(AuthOperationError.from(error).message))
|
||||
handleServerError(AuthOperationError.from(error))
|
||||
}
|
||||
} finally {
|
||||
if (operation.current === current) {
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
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 { useCallback, useEffect, useRef, useState } from 'react'
|
||||
|
||||
import { createServerError } from '@/lib/server-error-message'
|
||||
|
||||
import { fetchModels, fetchUpstreamModels } from '../api'
|
||||
|
||||
type DiscoveryState = {
|
||||
status: 'idle' | 'loading' | 'success' | 'error' | 'stale'
|
||||
models: string[]
|
||||
error?: unknown
|
||||
}
|
||||
|
||||
export type ChannelModelDiscoveryRequest =
|
||||
| { kind: 'saved'; channelId: number }
|
||||
| { kind: 'preview'; data: Parameters<typeof fetchModels>[0] }
|
||||
|
||||
type ChannelModelDiscoveryProps = {
|
||||
enabled: boolean
|
||||
request: ChannelModelDiscoveryRequest
|
||||
}
|
||||
|
||||
export function useChannelModelDiscovery(props: ChannelModelDiscoveryProps) {
|
||||
const [state, setState] = useState<DiscoveryState>({
|
||||
status: 'idle',
|
||||
models: [],
|
||||
})
|
||||
const sequence = useRef(0)
|
||||
|
||||
useEffect(() => {
|
||||
sequence.current += 1
|
||||
setState((previous) => {
|
||||
if (previous.status === 'idle') return previous
|
||||
if (!props.enabled) return { status: 'idle', models: [] }
|
||||
return { status: 'stale', models: previous.models }
|
||||
})
|
||||
return () => {
|
||||
sequence.current += 1
|
||||
}
|
||||
}, [props.enabled, props.request])
|
||||
|
||||
const fetch = useCallback(async () => {
|
||||
if (!props.enabled) return
|
||||
const requestSequence = ++sequence.current
|
||||
setState((previous) => ({ status: 'loading', models: previous.models }))
|
||||
try {
|
||||
const response =
|
||||
props.request.kind === 'saved'
|
||||
? await fetchUpstreamModels(props.request.channelId)
|
||||
: await fetchModels(props.request.data)
|
||||
if (requestSequence !== sequence.current) return
|
||||
if (!response.success) {
|
||||
throw createServerError(response, 'Failed to fetch models')
|
||||
}
|
||||
setState({ status: 'success', models: response.data ?? [] })
|
||||
} catch (error) {
|
||||
if (requestSequence !== sequence.current) return
|
||||
setState((previous) => ({
|
||||
status: 'error',
|
||||
models: previous.models,
|
||||
error,
|
||||
}))
|
||||
}
|
||||
}, [props.enabled, props.request])
|
||||
|
||||
return { ...state, fetch }
|
||||
}
|
||||
@@ -25,6 +25,8 @@ import {
|
||||
ADMIN_PERMISSION_RESOURCES,
|
||||
hasPermission,
|
||||
} from '@/lib/admin-permissions'
|
||||
import { handleServerError } from '@/lib/handle-server-error'
|
||||
import { createServerError } from '@/lib/server-error-message'
|
||||
import { useAuthStore } from '@/stores/auth-store'
|
||||
|
||||
import { createChannel, updateChannel } from '../api'
|
||||
@@ -55,31 +57,6 @@ const SENSITIVE_UPDATE_FIELDS = [
|
||||
'other',
|
||||
] satisfies (keyof Channel)[]
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null
|
||||
}
|
||||
|
||||
function getErrorMessage(error: unknown): string | undefined {
|
||||
if (error instanceof Error && typeof error.message === 'string') {
|
||||
return error.message
|
||||
}
|
||||
|
||||
if (!isRecord(error)) return undefined
|
||||
|
||||
const response = error.response
|
||||
if (isRecord(response)) {
|
||||
const data = response.data
|
||||
if (isRecord(data)) {
|
||||
const message = data.message
|
||||
if (typeof message === 'string') return message
|
||||
}
|
||||
}
|
||||
|
||||
const message = error.message
|
||||
if (typeof message === 'string') return message
|
||||
return undefined
|
||||
}
|
||||
|
||||
export function useChannelMutateForm(props: UseChannelMutateFormParams) {
|
||||
const { t } = useTranslation()
|
||||
const currentUser = useAuthStore((s) => s.auth.user)
|
||||
@@ -115,12 +92,14 @@ export function useChannelMutateForm(props: UseChannelMutateFormParams) {
|
||||
}
|
||||
: payload
|
||||
|
||||
const response = await updateChannel(
|
||||
props.currentRow.id,
|
||||
payloadWithKeyMode
|
||||
)
|
||||
const response = await updateChannel(props.currentRow.id, {
|
||||
...payloadWithKeyMode,
|
||||
...(canEditSensitive && props.isMultiKeyChannel
|
||||
? { multi_key_mode: data.multi_key_type }
|
||||
: {}),
|
||||
})
|
||||
if (!response.success) {
|
||||
throw new Error(response.message || t(ERROR_MESSAGES.UPDATE_FAILED))
|
||||
throw createServerError(response, t(ERROR_MESSAGES.UPDATE_FAILED))
|
||||
}
|
||||
return SUCCESS_MESSAGES.UPDATED
|
||||
}
|
||||
@@ -128,7 +107,7 @@ export function useChannelMutateForm(props: UseChannelMutateFormParams) {
|
||||
const payload = transformFormDataToCreatePayload(data)
|
||||
const response = await createChannel(payload)
|
||||
if (!response.success) {
|
||||
throw new Error(response.message || t(ERROR_MESSAGES.CREATE_FAILED))
|
||||
throw createServerError(response, t(ERROR_MESSAGES.CREATE_FAILED))
|
||||
}
|
||||
return SUCCESS_MESSAGES.CREATED
|
||||
},
|
||||
@@ -137,7 +116,7 @@ export function useChannelMutateForm(props: UseChannelMutateFormParams) {
|
||||
props.onSuccess()
|
||||
},
|
||||
onError: (error: unknown) => {
|
||||
toast.error(getErrorMessage(error) || t(ERROR_MESSAGES.CREATE_FAILED))
|
||||
handleServerError(error, t(ERROR_MESSAGES.CREATE_FAILED))
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { api, type ApiRequestConfig } from '@/lib/api'
|
||||
import { handleServerError } from '@/lib/handle-server-error'
|
||||
|
||||
import { normalizeModelList } from '../lib/upstream-update-utils'
|
||||
|
||||
@@ -31,9 +32,9 @@ const upstreamUpdateRequestConfig = {
|
||||
|
||||
function getManualIgnoredModelCount(settings: unknown): number {
|
||||
let parsed: Record<string, unknown> | null = null
|
||||
if (settings && typeof settings === 'object')
|
||||
if (settings && typeof settings === 'object') {
|
||||
parsed = settings as Record<string, unknown>
|
||||
else if (typeof settings === 'string') {
|
||||
} else if (typeof settings === 'string') {
|
||||
try {
|
||||
parsed = JSON.parse(settings)
|
||||
} catch {
|
||||
@@ -126,9 +127,9 @@ export function useChannelUpstreamUpdates(refresh: () => Promise<void>) {
|
||||
},
|
||||
upstreamUpdateRequestConfig
|
||||
)
|
||||
const { success, message, data } = res.data || {}
|
||||
const { success, data } = res.data || {}
|
||||
if (!success) {
|
||||
toast.error(message || t('Operation failed'))
|
||||
handleServerError(res.data, t('Operation failed'))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -146,13 +147,7 @@ export function useChannelUpstreamUpdates(refresh: () => Promise<void>) {
|
||||
closeModal()
|
||||
await refresh()
|
||||
} catch (e: unknown) {
|
||||
const err = e as {
|
||||
response?: { data?: { message?: string } }
|
||||
message?: string
|
||||
}
|
||||
toast.error(
|
||||
err?.response?.data?.message || err?.message || t('Operation failed')
|
||||
)
|
||||
handleServerError(e, t('Operation failed'))
|
||||
} finally {
|
||||
applyRef.current = false
|
||||
setApplyLoading(false)
|
||||
@@ -171,9 +166,9 @@ export function useChannelUpstreamUpdates(refresh: () => Promise<void>) {
|
||||
{},
|
||||
upstreamUpdateRequestConfig
|
||||
)
|
||||
const { success, message, data } = res.data || {}
|
||||
const { success, data } = res.data || {}
|
||||
if (!success) {
|
||||
toast.error(message || t('Batch processing failed'))
|
||||
handleServerError(res.data, t('Batch processing failed'))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -190,15 +185,7 @@ export function useChannelUpstreamUpdates(refresh: () => Promise<void>) {
|
||||
)
|
||||
await refresh()
|
||||
} catch (e: unknown) {
|
||||
const err = e as {
|
||||
response?: { data?: { message?: string } }
|
||||
message?: string
|
||||
}
|
||||
toast.error(
|
||||
err?.response?.data?.message ||
|
||||
err?.message ||
|
||||
t('Batch processing failed')
|
||||
)
|
||||
handleServerError(e, t('Batch processing failed'))
|
||||
} finally {
|
||||
applyAllRef.current = false
|
||||
setApplyAllLoading(false)
|
||||
@@ -215,9 +202,9 @@ export function useChannelUpstreamUpdates(refresh: () => Promise<void>) {
|
||||
{ id: ch.id },
|
||||
upstreamUpdateRequestConfig
|
||||
)
|
||||
const { success, message, data } = res.data || {}
|
||||
const { success, data } = res.data || {}
|
||||
if (!success) {
|
||||
toast.error(message || t('Detection failed'))
|
||||
handleServerError(res.data, t('Detection failed'))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -229,13 +216,7 @@ export function useChannelUpstreamUpdates(refresh: () => Promise<void>) {
|
||||
)
|
||||
await refresh()
|
||||
} catch (e: unknown) {
|
||||
const err = e as {
|
||||
response?: { data?: { message?: string } }
|
||||
message?: string
|
||||
}
|
||||
toast.error(
|
||||
err?.response?.data?.message || err?.message || t('Detection failed')
|
||||
)
|
||||
handleServerError(e, t('Detection failed'))
|
||||
} finally {
|
||||
detectRef.current = false
|
||||
}
|
||||
@@ -253,9 +234,9 @@ export function useChannelUpstreamUpdates(refresh: () => Promise<void>) {
|
||||
{},
|
||||
upstreamUpdateRequestConfig
|
||||
)
|
||||
const { success, message } = res.data || {}
|
||||
const { success } = res.data || {}
|
||||
if (!success) {
|
||||
toast.error(message || t('Batch detection failed'))
|
||||
handleServerError(res.data, t('Batch detection failed'))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -266,15 +247,7 @@ export function useChannelUpstreamUpdates(refresh: () => Promise<void>) {
|
||||
)
|
||||
await refresh()
|
||||
} catch (e: unknown) {
|
||||
const err = e as {
|
||||
response?: { data?: { message?: string } }
|
||||
message?: string
|
||||
}
|
||||
toast.error(
|
||||
err?.response?.data?.message ||
|
||||
err?.message ||
|
||||
t('Batch detection failed')
|
||||
)
|
||||
handleServerError(e, t('Batch detection failed'))
|
||||
} finally {
|
||||
detectAllRef.current = false
|
||||
setDetectAllLoading(false)
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
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 { type RefObject, useLayoutEffect, useState } from 'react'
|
||||
|
||||
export type RedirectPanelPlacement = {
|
||||
x: number
|
||||
y: number
|
||||
width: number
|
||||
}
|
||||
|
||||
/** Below this viewport width the floating panel is not offered at all. */
|
||||
export const REDIRECT_PANEL_MIN_VIEWPORT = 1280
|
||||
const GAP = 16
|
||||
const WIDTH = 420
|
||||
|
||||
/**
|
||||
* Picks where the redirect window first appears: beside the channel sheet on
|
||||
* its free side, or overlapping the sheet's inner edge when the viewport has
|
||||
* no room there. The window is draggable afterwards, so this is only a start
|
||||
* position. `null` means the viewport is too narrow for a floating window.
|
||||
*/
|
||||
export function useRedirectPanelPlacement(
|
||||
anchorRef: RefObject<HTMLElement | null>,
|
||||
enabled: boolean,
|
||||
sheetSide: 'left' | 'right'
|
||||
): RedirectPanelPlacement | null {
|
||||
const [placement, setPlacement] = useState<RedirectPanelPlacement | null>(
|
||||
null
|
||||
)
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!enabled || window.innerWidth < REDIRECT_PANEL_MIN_VIEWPORT) {
|
||||
setPlacement(null)
|
||||
return
|
||||
}
|
||||
const anchor =
|
||||
anchorRef.current?.closest<HTMLElement>('[data-slot="sheet-content"]') ??
|
||||
anchorRef.current
|
||||
if (!anchor) {
|
||||
setPlacement(null)
|
||||
return
|
||||
}
|
||||
const rect = anchor.getBoundingClientRect()
|
||||
const beside =
|
||||
sheetSide === 'left' ? rect.right + GAP : rect.left - GAP - WIDTH
|
||||
const x = Math.min(Math.max(GAP, beside), window.innerWidth - WIDTH - GAP)
|
||||
setPlacement({ x, y: rect.top + GAP, width: WIDTH })
|
||||
}, [anchorRef, enabled, sheetSide])
|
||||
|
||||
return placement
|
||||
}
|
||||
Reference in new issue
Block a user