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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions messages/en/settings/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ export default {
common,
config,
providers,
providerTypes: providersFormProviderTypes,
prices,
sensitiveWords,
requestFilters,
Expand Down
18 changes: 11 additions & 7 deletions messages/en/settings/statusPage.json
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
{
"title": "Public Status Page",
"description": "Configure the public status window, aggregation interval, and which groups/models should be exposed.",
"description": "Configure the public status page's stats window, chart bucket size, and which groups/models should be exposed.",
"form": {
"windowHours": "Display Window (hours)",
"windowHoursDesc": "Controls how much history the public page aggregates. Default: 24 hours.",
"aggregationIntervalMinutes": "Aggregation Interval (minutes)",
"aggregationIntervalMinutesDesc": "How often the background job refreshes the public snapshot. It only runs after at least one group declares public models.",
"windowHours": "Stats Window (hours)",
"windowHoursDesc": "Window length used to compute TTFB and availability, and the total span the chart covers. Default: 24 hours.",
"aggregationIntervalMinutes": "Chart Bucket (minutes)",
"aggregationIntervalMinutesDesc": "Length of each bucket on the chart timeline; controls chart granularity. Choose 5 / 15 / 30 / 60 minutes.",
"aggregationIntervalMinutesInvalid": "Public status aggregation interval must be one of 5, 15, 30, or 60 minutes.",
"intervalOption": "{minutes} min",
"slug": "Slug",
"slugTooltip": "URL identifier for this group's dedicated status page. Leave empty to auto-generate from the public display name. Lowercase letters, digits, and hyphens only.",
"copy": "Copy",
"sortOrder": "Sort Order",
"sortOrderTooltip": "Display order on the public status page. Lower values appear first (same convention as provider priority). Equal values fall back to group name.",
"descriptionTooLong": "Public status configuration exceeds the 16 KB UTF-8 limit of provider_groups.description.",
"projectionPublishFailed": "Public status Redis projection publish failed.",
"groupsTitle": "Public Groups and Models",
Expand Down Expand Up @@ -41,7 +43,7 @@
"public": {
"systemStatus": "System Status",
"heroPrimary": "AI SERVICES",
"heroSecondary": "INTELLIGENCE MONITOR",
"heroSecondary": "SERVICE STATUS DASHBOARD",
"generatedAt": "Updated",
"ttfb": "TTFB",
"tps": "TPS",
Expand Down Expand Up @@ -85,6 +87,8 @@
"modelsLabel": "models",
"issuesLabel": "issues",
"clearSearch": "Clear search",
"dragHandle": "Drag to reorder"
"dragHandle": "Drag to reorder",
"toggleGroup": "Toggle group",
"openGroupPage": "Open group status page"
}
}
1 change: 1 addition & 0 deletions messages/ja/settings/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ export default {
common,
config,
providers,
providerTypes: providersFormProviderTypes,
prices,
sensitiveWords,
requestFilters,
Expand Down
18 changes: 11 additions & 7 deletions messages/ja/settings/statusPage.json
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
{
"title": "公開ステータスページ",
"description": "公開ステータスページの集計期間、集計間隔、および公開するグループ/モデルを設定します。",
"description": "公開ステータスページの統計ウィンドウ、チャートのバケットサイズ、および公開するグループ/モデルを設定します。",
"form": {
"windowHours": "表示期間(時間)",
"windowHoursDesc": "公開ページが集計する履歴期間を指定します。既定値は 24 時間です。",
"aggregationIntervalMinutes": "集計間隔(分)",
"aggregationIntervalMinutesDesc": "バックグラウンド集計がスナップショットを更新する周期です。公開グループとモデルが設定された場合のみ有効になります。",
"windowHours": "統計ウィンドウ(時間)",
"windowHoursDesc": "TTFB と可用率の算出に使う統計期間であり、チャートがカバーする総期間でもあります。既定値は 24 時間です。",
"aggregationIntervalMinutes": "チャートのバケット(分)",
"aggregationIntervalMinutesDesc": "チャート時間軸の各バケットの長さで、チャートの粒度を決定します。5 / 15 / 30 / 60 分から選択できます。",
"aggregationIntervalMinutesInvalid": "公開ステータスの集計間隔は 5、15、30、60 分のいずれかである必要があります。",
"intervalOption": "{minutes} 分",
"slug": "Slug",
"slugTooltip": "グループ専用ステータスページの URL 識別子。空欄の場合は公開表示名から自動生成されます。小文字、数字、ハイフンのみ使用可能です。",
"copy": "説明文",
"sortOrder": "並び順",
"sortOrderTooltip": "公開ステータスページでの表示順。値が小さいほど先頭に表示されます(プロバイダー優先度と同じ規則)。同値の場合はグループ名順となります。",
"descriptionTooLong": "公開ステータス設定が provider_groups.description の 16 KB UTF-8 制限を超えています。",
"projectionPublishFailed": "公開ステータスの Redis 投影の公開に失敗しました。",
"groupsTitle": "公開グループとモデル",
Expand Down Expand Up @@ -41,7 +43,7 @@
"public": {
"systemStatus": "システム状態",
"heroPrimary": "AI SERVICES",
"heroSecondary": "INTELLIGENCE MONITOR",
"heroSecondary": "SERVICE STATUS DASHBOARD",
"generatedAt": "更新",
"ttfb": "TTFB",
"tps": "TPS",
Expand Down Expand Up @@ -85,6 +87,8 @@
"modelsLabel": "モデル",
"issuesLabel": "異常",
"clearSearch": "検索をクリア",
"dragHandle": "ドラッグして並べ替え"
"dragHandle": "ドラッグして並べ替え",
"toggleGroup": "グループを展開または折りたたむ",
"openGroupPage": "グループ専用ステータスページを開く"
}
}
1 change: 1 addition & 0 deletions messages/ru/settings/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ export default {
common,
config,
providers,
providerTypes: providersFormProviderTypes,
prices,
sensitiveWords,
requestFilters,
Expand Down
18 changes: 11 additions & 7 deletions messages/ru/settings/statusPage.json
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
{
"title": "Публичная страница статуса",
"description": "Настройте окно агрегации, интервал агрегации и группы/модели, которые будут показаны публично.",
"description": "Настройте окно статистики, размер бакета графика и группы/модели, которые будут показаны публично.",
"form": {
"windowHours": "Окно отображения (часы)",
"windowHoursDesc": "Определяет глубину истории для публичной страницы. По умолчанию: 24 часа.",
"aggregationIntervalMinutes": "Интервал агрегации (минуты)",
"aggregationIntervalMinutesDesc": "Как часто фоновая задача обновляет публичный снимок. Запускается только после настройки хотя бы одной группы с моделями.",
"windowHours": "Окно статистики (часы)",
"windowHoursDesc": "Длина окна для расчёта TTFB и доступности, а также общий период, покрываемый графиком. По умолчанию: 24 часа.",
"aggregationIntervalMinutes": "Бакет графика (минуты)",
"aggregationIntervalMinutesDesc": "Длина каждого бакета на оси времени графика; определяет детализацию графика. Допустимые значения: 5 / 15 / 30 / 60 минут.",
"aggregationIntervalMinutesInvalid": "Интервал агрегации публичного статуса должен быть одним из 5, 15, 30 или 60 минут.",
"intervalOption": "{minutes} мин",
"slug": "Slug",
"slugTooltip": "Идентификатор URL для выделенной страницы статуса этой группы. Если оставить пустым, будет создан автоматически из публичного имени. Разрешены только строчные буквы, цифры и дефисы.",
"copy": "Пояснение",
"sortOrder": "Порядок",
"sortOrderTooltip": "Порядок отображения на публичной странице статуса. Меньшие значения — выше (как у приоритета провайдеров). При равных значениях порядок определяется по имени группы.",
"descriptionTooLong": "Конфигурация публичного статуса превышает лимит 16 КБ UTF-8 для provider_groups.description.",
"projectionPublishFailed": "Не удалось опубликовать Redis-проекцию публичного статуса.",
"groupsTitle": "Публичные группы и модели",
Expand Down Expand Up @@ -41,7 +43,7 @@
"public": {
"systemStatus": "Статус системы",
"heroPrimary": "AI SERVICES",
"heroSecondary": "INTELLIGENCE MONITOR",
"heroSecondary": "SERVICE STATUS DASHBOARD",
"generatedAt": "Обновлено",
"ttfb": "TTFB",
"tps": "TPS",
Expand Down Expand Up @@ -85,6 +87,8 @@
"modelsLabel": "моделей",
"issuesLabel": "проблем",
"clearSearch": "Очистить поиск",
"dragHandle": "Перетащить"
"dragHandle": "Перетащить",
"toggleGroup": "Развернуть или свернуть группу",
"openGroupPage": "Открыть страницу статуса группы"
}
}
1 change: 1 addition & 0 deletions messages/zh-CN/settings/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ export default {
common,
config,
providers,
providerTypes: providersFormProviderTypes,
prices,
sensitiveWords,
requestFilters,
Expand Down
18 changes: 11 additions & 7 deletions messages/zh-CN/settings/statusPage.json
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
{
"title": "公开状态页面",
"description": "配置公开状态页面的聚合窗口、聚合间隔,以及需要对外展示的分组和模型。",
"description": "配置公开状态页面的统计窗口、图表分桶,以及需要对外展示的分组和模型。",
"form": {
"windowHours": "展示时间长度(小时)",
"windowHoursDesc": "控制公开页面聚合覆盖的时间窗口。默认 24 小时。",
"aggregationIntervalMinutes": "聚合间隔(分钟)",
"aggregationIntervalMinutesDesc": "定时聚合写快照的周期。只有配置了公开分组和模型后才会生效。",
"windowHours": "统计窗口(小时)",
"windowHoursDesc": "用于计算 TTFB 与在线率的统计窗口长度,也是图表覆盖的总时间跨度。默认 24 小时。",
"aggregationIntervalMinutes": "图表分桶(分钟)",
"aggregationIntervalMinutesDesc": "图表时间线每个分桶的时长,决定图表粒度。可选 5 / 15 / 30 / 60 分钟。",
"aggregationIntervalMinutesInvalid": "公开状态聚合间隔只能是 5、15、30、60 分钟之一。",
"intervalOption": "{minutes} 分钟",
"slug": "Slug",
"slugTooltip": "用于分组独立状态页的 URL 标识。留空将基于对外显示名自动生成。只能包含小写字母、数字和连字符。",
"copy": "说明文案",
"sortOrder": "排序",
"sortOrderTooltip": "控制公开状态页中分组的显示顺序。数值越小越靠前(与供应商优先级一致)。相同数值按分组名排序。",
"descriptionTooLong": "公开状态配置超过 provider_groups.description 的 16 KB UTF-8 限制。",
"projectionPublishFailed": "公开状态 Redis 投影发布失败。",
"groupsTitle": "公开分组与模型",
Expand Down Expand Up @@ -41,7 +43,7 @@
"public": {
"systemStatus": "系统状态",
"heroPrimary": "AI 服务",
"heroSecondary": "智能状态面板",
"heroSecondary": "服务状态面板",
"generatedAt": "更新于",
"ttfb": "TTFB",
"tps": "TPS",
Expand Down Expand Up @@ -85,6 +87,8 @@
"modelsLabel": "模型",
"issuesLabel": "异常",
"clearSearch": "清除搜索",
"dragHandle": "拖动重排"
"dragHandle": "拖动重排",
"toggleGroup": "展开或折叠分组",
"openGroupPage": "打开分组独立状态页"
}
}
1 change: 1 addition & 0 deletions messages/zh-TW/settings/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ export default {
common,
config,
providers,
providerTypes: providersFormProviderTypes,
prices,
sensitiveWords,
requestFilters,
Expand Down
18 changes: 11 additions & 7 deletions messages/zh-TW/settings/statusPage.json
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
{
"title": "公開狀態頁面",
"description": "設定公開狀態頁面的聚合視窗、聚合間隔,以及需要對外展示的分組和模型。",
"description": "設定公開狀態頁面的統計視窗、圖表分桶,以及需要對外展示的分組和模型。",
"form": {
"windowHours": "展示時間長度(小時)",
"windowHoursDesc": "控制公開頁面聚合覆蓋的時間視窗。預設 24 小時。",
"aggregationIntervalMinutes": "聚合間隔(分鐘)",
"aggregationIntervalMinutesDesc": "定時聚合寫入快照的週期。只有在設定了公開分組和模型後才會生效。",
"windowHours": "統計視窗(小時)",
"windowHoursDesc": "用於計算 TTFB 與在線率的統計視窗長度,也是圖表覆蓋的總時間跨度。預設 24 小時。",
"aggregationIntervalMinutes": "圖表分桶(分鐘)",
"aggregationIntervalMinutesDesc": "圖表時間軸每個分桶的時長,決定圖表粒度。可選 5 / 15 / 30 / 60 分鐘。",
"aggregationIntervalMinutesInvalid": "公開狀態聚合間隔只能是 5、15、30、60 分鐘之一。",
"intervalOption": "{minutes} 分鐘",
"slug": "Slug",
"slugTooltip": "用於分組獨立狀態頁的 URL 識別碼。留空將根據對外顯示名自動產生。只能包含小寫字母、數字與連字號。",
"copy": "說明文案",
"sortOrder": "排序",
"sortOrderTooltip": "控制公開狀態頁中分組的顯示順序。數值越小越靠前(與供應商優先級一致)。相同數值按分組名排序。",
"descriptionTooLong": "公開狀態設定超過 provider_groups.description 的 16 KB UTF-8 限制。",
"projectionPublishFailed": "公開狀態 Redis 投影發布失敗。",
"groupsTitle": "公開分組與模型",
Expand Down Expand Up @@ -41,7 +43,7 @@
"public": {
"systemStatus": "系統狀態",
"heroPrimary": "AI 服務",
"heroSecondary": "智慧狀態面板",
"heroSecondary": "服務狀態面板",
"generatedAt": "更新於",
"ttfb": "TTFB",
"tps": "TPS",
Expand Down Expand Up @@ -85,6 +87,8 @@
"modelsLabel": "模型",
"issuesLabel": "異常",
"clearSearch": "清除搜尋",
"dragHandle": "拖曳重排"
"dragHandle": "拖曳重排",
"toggleGroup": "展開或收合分組",
"openGroupPage": "開啟分組獨立狀態頁"
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { ChevronDown, ChevronRight, ExternalLink, Save } from "lucide-react";
import { ChevronDown, ChevronRight, ExternalLink, Info, Save } from "lucide-react";
import { useRouter } from "next/navigation";
import { useTranslations } from "next-intl";
import { useMemo, useState, useTransition } from "react";
Expand All @@ -25,6 +25,7 @@ import {
SelectValue,
} from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { Link } from "@/i18n/routing";
import {
getProviderTypeTranslationKey,
Expand Down Expand Up @@ -58,6 +59,37 @@ function getPublishableGroupCount(groups: PublicStatusSettingsFormGroup[]): numb
return groups.filter((group) => group.enabled && group.publicModels.length > 0).length;
}

function slugifyGroupName(input: string): string {
const trimmed = (input || "").trim().toLowerCase();
if (!trimmed) return "";
return trimmed
.replace(/[^a-z0-9\s-]+/g, "")
.replace(/\s+/g, "-")
.replace(/-+/g, "-")
.replace(/^-+|-+$/g, "");
}
Comment on lines +62 to +70
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 CJK characters survive slug generation despite tooltip documenting ASCII-only constraint

slugifyGroupName explicitly keeps Unicode range \u4e00-\u9fa5 (common CJK characters), so a display name like "GPT 服务" auto-generates the slug "gpt-服务". However, the slugTooltip text in all 5 locales says "Lowercase letters, digits, and hyphens only." — those two contracts are mutually exclusive. A user whose group has a Chinese/Japanese display name silently gets an auto-populated slug with non-ASCII characters that violates the documented format; if they save without noticing, the URL becomes %E6%9C%8D%E5%8A%A1-style encoded, which is unintuitive and may break slug-equality lookups depending on normalisation.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/[locale]/settings/status-page/_components/public-status-settings-form.tsx
Line: 62-70

Comment:
**CJK characters survive slug generation despite tooltip documenting ASCII-only constraint**

`slugifyGroupName` explicitly keeps Unicode range `\u4e00-\u9fa5` (common CJK characters), so a display name like `"GPT 服务"` auto-generates the slug `"gpt-服务"`. However, the `slugTooltip` text in all 5 locales says *"Lowercase letters, digits, and hyphens only."* — those two contracts are mutually exclusive. A user whose group has a Chinese/Japanese display name silently gets an auto-populated slug with non-ASCII characters that violates the documented format; if they save without noticing, the URL becomes `%E6%9C%8D%E5%8A%A1`-style encoded, which is unintuitive and may break slug-equality lookups depending on normalisation.

How can I resolve this? If you propose a fix, please make it concise.

Comment thread
coderabbitai[bot] marked this conversation as resolved.

function InfoTip({ text }: { text: string }) {
return (
<TooltipProvider delayDuration={100}>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
className="inline-flex h-4 w-4 items-center justify-center text-muted-foreground hover:text-foreground"
aria-label={text}
>
<Info className="h-3.5 w-3.5" />
</button>
</TooltipTrigger>
<TooltipContent side="top" className="max-w-xs text-xs leading-relaxed">
{text}
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}

export function PublicStatusSettingsForm({
initialWindowHours,
initialAggregationIntervalMinutes,
Expand Down Expand Up @@ -148,7 +180,10 @@ export function PublicStatusSettingsForm({
>
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="public-status-window-hours">{t("statusPage.form.windowHours")}</Label>
<div className="flex items-center gap-1.5">
<Label htmlFor="public-status-window-hours">{t("statusPage.form.windowHours")}</Label>
<InfoTip text={t("statusPage.form.windowHoursDesc")} />
</div>
<Input
id="public-status-window-hours"
type="number"
Expand All @@ -158,13 +193,15 @@ export function PublicStatusSettingsForm({
onChange={(event) => setWindowHours(event.target.value)}
disabled={isPending}
/>
<p className="text-sm text-muted-foreground">{t("statusPage.form.windowHoursDesc")}</p>
</div>

<div className="space-y-2">
<Label htmlFor="public-status-aggregation-interval">
{t("statusPage.form.aggregationIntervalMinutes")}
</Label>
<div className="flex items-center gap-1.5">
<Label htmlFor="public-status-aggregation-interval">
{t("statusPage.form.aggregationIntervalMinutes")}
</Label>
<InfoTip text={t("statusPage.form.aggregationIntervalMinutesDesc")} />
</div>
<Select
value={aggregationIntervalMinutes}
onValueChange={setAggregationIntervalMinutes}
Expand All @@ -181,9 +218,6 @@ export function PublicStatusSettingsForm({
))}
</SelectContent>
</Select>
<p className="text-sm text-muted-foreground">
{t("statusPage.form.aggregationIntervalMinutesDesc")}
</p>
</div>
</div>

Expand Down Expand Up @@ -279,15 +313,18 @@ export function PublicStatusSettingsForm({
</div>

<div className="space-y-2">
<Label>{t("statusPage.form.slug")}</Label>
<div className="flex items-center gap-1.5">
<Label>{t("statusPage.form.slug")}</Label>
<InfoTip text={t("statusPage.form.slugTooltip")} />
</div>
<Input
value={group.publicGroupSlug}
onChange={(event) =>
updateGroup(index, {
publicGroupSlug: event.target.value,
})
}
placeholder={group.groupName.toLowerCase()}
placeholder={slugifyGroupName(group.displayName || group.groupName)}
disabled={isPending}
/>
</div>
Expand All @@ -307,7 +344,10 @@ export function PublicStatusSettingsForm({
</div>

<div className="space-y-2">
<Label>{t("statusPage.form.sortOrder")}</Label>
<div className="flex items-center gap-1.5">
<Label>{t("statusPage.form.sortOrder")}</Label>
<InfoTip text={t("statusPage.form.sortOrderTooltip")} />
</div>
<Input
type="number"
value={String(group.sortOrder)}
Expand Down
Loading
Loading