mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-01 21:02:12 +03:00
* refactor(dashboard): sidebar subtitles, providers UX, monaco self-host
Sidebar:
- Add subtitleKey to all ~50 menu items so every entry shows a short
description line (previously only 7 had subtitles).
- Inject 50 new sidebar.*Subtitle keys across all 42 i18n locales
(en/pt-BR translated; remaining locales fall back to English).
- Fix DATA_÷IC glitch: replace invalid Material Symbols icon
"data_compression" with "compress" on analytics-compression.
- Rename "Compression Combos" -> "Engine Combos" to avoid truncation
and disambiguate from the OmniProxy Combos item.
Auto-routing banner:
- Move AutoRoutingBanner from DashboardLayout (rendered on every page)
to /home only, so it no longer follows the user across the dashboard.
Monaco editor:
- Add shared MonacoEditor wrapper that calls loader.config({ monaco })
to load Monaco from the bundled monaco-editor package instead of the
jsdelivr CDN (blocked by CSP script-src 'self'), fixing the
"Monaco initialization: error" runtime error.
- Migrate the 5 direct dynamic imports of @monaco-editor/react to the
wrapper (playground, search-tools, translator).
Providers page UX:
- Replace the static legend + 4-stat divider + duplicate filter chips
with a single row of clickable category pills with embedded counters
(All, Free, OAuth, API Key, IDE, Compatible, Web Cookie, Search,
Audio, Local, Cloud Agent).
- Remove redundant per-section Free only / Configured only toggles and
the Zed Import button from the OAuth section header.
- Introduce a dedicated "IDE Providers" section (Cursor, Zed, Trae)
rendered under OAuth Providers; exclude IDE_PROVIDER_IDS from OAuth
to avoid duplication.
- Add zed and trae providers, plus IDE_PROVIDER_IDS set.
- Move the Zed keychain import to /dashboard/providers/zed as a
contextual Card, instead of cluttering every OAuth section header.
- Extend test-batch route and validation schema with mode: "ide".
* feat(dashboard): add token saver controls to endpoint and context pages
Expose the Token Saver master settings from the endpoint page and
surface its disabled state across the Caveman and RTK context pages.
Update default compression behavior to start disabled with lighter
intensity presets, and add Caveman input compression controls so users
can configure input and output modes separately.
Tighten provider page section rendering by gating compatible, free,
oauth, and api key blocks behind their visibility checks and simplify
several summary labels.
* feat(dashboard): add runtime page and rename limits to quota
Introduce a dedicated runtime observability page for circuit
breakers, cooldowns, lockouts, sessions, and quota alerts.
Split provider quota out of the old limits route, add a redirect from
`/dashboard/limits` to `/dashboard/quota`, and update sidebar/header
labels and i18n keys to match the new navigation.
Enhance provider quota filtering state and remove the tier coverage
widget from the dashboard home view.
* feat(dashboard): redesign budget page and add quota sharing preview
Budget (/dashboard/costs/budget) gets a full rewrite:
- Hero with 6 KPIs (today, month, projected EOM, blocked, at-risk, active)
- Status pills with counts (blocked/alerting/warning/safe/no-limit)
- Templates row (localStorage) with bulk-apply to selected keys
- Multi-key table with checkbox selection and colored progress bar
- Expandable rows with projection card and per-provider cost breakdown
(last 30d via /api/usage/analytics?apiKeyIds=) plus inline edit form
Quota sharing (/dashboard/costs/quota-share) is new and ships as a beta
UI preview backed by localStorage. Pool persistence in the DB and
request-pipeline enforcement are intentionally deferred to a follow-up.
The page lets operators:
- Create pools from a provider connection + quota window
- Edit allocations per API key with % split and equal-split helper
- Toggle hard/soft/burst policy intent
Donut chart rendered with inline SVG; allocation editor in a modal.
Sidebar gets the new "Quota Sharing" entry under Costs Parameters; i18n
keys propagated across all 41 locales with PT-BR translation.
---------
Co-authored-by: diegosouzapw <diego.souza.pw@gmail.com>
243 lines
8.1 KiB
TypeScript
243 lines
8.1 KiB
TypeScript
"use client";
|
|
|
|
import { usePathname, useRouter } from "next/navigation";
|
|
import ThemeToggle from "./ThemeToggle";
|
|
import TokenHealthBadge from "./TokenHealthBadge";
|
|
import DegradationBadge from "./DegradationBadge";
|
|
import LanguageSelector from "./LanguageSelector";
|
|
import ProviderIcon from "./ProviderIcon";
|
|
import { useTranslations } from "next-intl";
|
|
import {
|
|
OAUTH_PROVIDERS,
|
|
APIKEY_PROVIDERS,
|
|
FREE_PROVIDERS,
|
|
CLAUDE_CODE_COMPATIBLE_PREFIX,
|
|
OPENAI_COMPATIBLE_PREFIX,
|
|
ANTHROPIC_COMPATIBLE_PREFIX,
|
|
} from "@/shared/constants/providers";
|
|
import {
|
|
SIDEBAR_SECTIONS,
|
|
getSectionItems,
|
|
type SidebarItemDefinition,
|
|
type HideableSidebarItemId,
|
|
} from "@/shared/constants/sidebarVisibility";
|
|
import { useIsElectron } from "@/shared/hooks/useElectron";
|
|
|
|
const isE2EMode = process.env.NEXT_PUBLIC_OMNIROUTE_E2E_MODE === "1";
|
|
|
|
// Map sidebar item id → header description i18n key
|
|
const HEADER_DESCRIPTIONS: Partial<Record<HideableSidebarItemId, string>> = {
|
|
home: "homeDescription",
|
|
endpoints: "endpointDescription",
|
|
"api-manager": "apiManagerDescription",
|
|
providers: "providerDescription",
|
|
combos: "comboDescription",
|
|
batch: "batchDescription",
|
|
costs: "costsDescription",
|
|
analytics: "analyticsDescription",
|
|
cache: "cacheDescription",
|
|
quota: "limitsDescription",
|
|
runtime: "runtimeDescription",
|
|
media: "mediaDescription",
|
|
agents: "agentsDescription",
|
|
"cloud-agents": "cloudAgentsDescription",
|
|
memory: "memoryDescription",
|
|
skills: "skillsDescription",
|
|
"agent-skills": "agentSkillsDescription",
|
|
settings: "settingsDescription",
|
|
"context-caveman": "contextCavemanDescription",
|
|
"context-rtk": "contextRtkDescription",
|
|
"context-combos": "contextCombosDescription",
|
|
translator: "translatorDescription",
|
|
playground: "playgroundDescription",
|
|
"search-tools": "searchToolsDescription",
|
|
logs: "logsDescription",
|
|
audit: "auditDescription",
|
|
webhooks: "webhooksDescription",
|
|
health: "healthDescription",
|
|
proxy: "proxyDescription",
|
|
changelog: "changelogDescription",
|
|
// Protocols
|
|
mcp: "mcpDescription",
|
|
a2a: "a2aDescription",
|
|
"api-endpoints": "apiEndpointsDescription",
|
|
// Agents & AI sub-pages
|
|
"batch-files": "batchFilesDescription",
|
|
// Analytics sub-pages
|
|
"analytics-evals": "analyticsEvalsDescription",
|
|
"analytics-search": "analyticsSearchDescription",
|
|
"analytics-utilization": "analyticsUtilizationDescription",
|
|
"analytics-combo-health": "analyticsComboHealthDescription",
|
|
"analytics-compression": "analyticsCompressionDescription",
|
|
// Costs sub-pages
|
|
"costs-budget": "costsBudgetDescription",
|
|
"costs-pricing": "costsPricingDescription",
|
|
// Logs sub-pages
|
|
"logs-proxy": "logsProxyDescription",
|
|
"logs-console": "logsConsoleDescription",
|
|
"logs-activity": "logsActivityDescription",
|
|
// Audit sub-pages
|
|
"audit-mcp": "auditMcpDescription",
|
|
// Settings sub-pages
|
|
"settings-general": "settingsGeneralDescription",
|
|
"settings-appearance": "settingsAppearanceDescription",
|
|
"settings-ai": "settingsAiDescription",
|
|
"settings-security": "settingsSecurityDescription",
|
|
"settings-routing": "settingsRoutingDescription",
|
|
"settings-resilience": "settingsResilienceDescription",
|
|
"settings-advanced": "settingsAdvancedDescription",
|
|
// Proxy sub-pages
|
|
"mitm-proxy": "mitmProxyDescription",
|
|
"1proxy": "oneProxyDescription",
|
|
// OmniProxy items
|
|
"cli-tools": "cliToolsDescription",
|
|
};
|
|
|
|
// Build href → sidebar item lookup (non-external items only)
|
|
const sidebarByHref = new Map<string, SidebarItemDefinition>();
|
|
for (const section of SIDEBAR_SECTIONS) {
|
|
for (const item of getSectionItems(section)) {
|
|
if (!item.external) sidebarByHref.set(item.href, item);
|
|
}
|
|
}
|
|
|
|
function getSidebarItem(pathname: string): SidebarItemDefinition | undefined {
|
|
const exact = sidebarByHref.get(pathname);
|
|
if (exact) return exact;
|
|
// Longest prefix match
|
|
let best: SidebarItemDefinition | undefined;
|
|
let bestLen = 0;
|
|
for (const [href, item] of sidebarByHref) {
|
|
if (pathname.startsWith(href) && href.length > bestLen) {
|
|
best = item;
|
|
bestLen = href.length;
|
|
}
|
|
}
|
|
return best;
|
|
}
|
|
|
|
type HeaderProps = {
|
|
onMenuClick?: () => void;
|
|
showMenuButton?: boolean;
|
|
};
|
|
|
|
type PageInfo = {
|
|
title: string;
|
|
description: string;
|
|
icon?: string;
|
|
providerId?: string;
|
|
};
|
|
|
|
function usePageInfo(pathname: string | null): PageInfo {
|
|
const ts = useTranslations("sidebar");
|
|
const th = useTranslations("header");
|
|
|
|
if (!pathname) return { title: "", description: "" };
|
|
|
|
// Special: provider detail page /dashboard/providers/[id]
|
|
const providerMatch = pathname.match(/\/providers\/([^/]+)$/);
|
|
if (providerMatch) {
|
|
const pid = providerMatch[1];
|
|
const info = OAUTH_PROVIDERS[pid] || FREE_PROVIDERS[pid] || APIKEY_PROVIDERS[pid];
|
|
if (info) return { title: info.name, description: "", providerId: info.id };
|
|
if (pid.startsWith(CLAUDE_CODE_COMPATIBLE_PREFIX))
|
|
return { title: "CC Compatible", description: "", providerId: "claude" };
|
|
if (pid.startsWith(OPENAI_COMPATIBLE_PREFIX))
|
|
return { title: th("openaiCompatible"), description: "", providerId: "oai-cc" };
|
|
if (pid.startsWith(ANTHROPIC_COMPATIBLE_PREFIX))
|
|
return { title: th("anthropicCompatible"), description: "", providerId: "anthropic-m" };
|
|
}
|
|
|
|
// Derive from sidebar
|
|
const item = getSidebarItem(pathname);
|
|
if (item) {
|
|
const descKey = HEADER_DESCRIPTIONS[item.id];
|
|
return {
|
|
title: ts(item.i18nKey),
|
|
description: descKey ? th(descKey) : "",
|
|
icon: item.icon,
|
|
};
|
|
}
|
|
|
|
return { title: "", description: "" };
|
|
}
|
|
|
|
export default function Header({ onMenuClick, showMenuButton = true }: HeaderProps) {
|
|
const pathname = usePathname();
|
|
const router = useRouter();
|
|
const isElectron = useIsElectron();
|
|
const t = useTranslations("header");
|
|
const { title, description, icon, providerId } = usePageInfo(pathname);
|
|
const isMacElectron =
|
|
isElectron &&
|
|
typeof window !== "undefined" &&
|
|
(window as any).electronAPI?.platform === "darwin";
|
|
|
|
const handleLogout = async () => {
|
|
try {
|
|
const res = await fetch("/api/auth/logout", { method: "POST" });
|
|
if (res.ok) {
|
|
router.push("/login");
|
|
router.refresh();
|
|
}
|
|
} catch (err) {
|
|
console.error("Failed to logout:", err);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<header
|
|
className="sticky top-0 z-10 flex items-center justify-between border-b border-black/5 bg-bg px-8 py-4 dark:border-white/5"
|
|
style={{
|
|
paddingTop: isMacElectron ? "calc(1rem + var(--desktop-safe-top))" : undefined,
|
|
}}
|
|
>
|
|
{/* Mobile menu button */}
|
|
<div className="flex items-center gap-3 lg:hidden">
|
|
{showMenuButton && (
|
|
<button
|
|
onClick={onMenuClick}
|
|
className="text-text-main hover:text-primary transition-colors"
|
|
>
|
|
<span className="material-symbols-outlined">menu</span>
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
{/* Page title with icon - desktop */}
|
|
<div className="hidden lg:flex items-center gap-3">
|
|
{(icon || providerId) && (
|
|
<div className="flex items-center justify-center size-9 rounded-lg bg-primary/10 shrink-0">
|
|
{icon ? (
|
|
<span className="material-symbols-outlined text-primary text-[20px]">{icon}</span>
|
|
) : (
|
|
providerId && <ProviderIcon providerId={providerId} size={22} type="color" />
|
|
)}
|
|
</div>
|
|
)}
|
|
{title && (
|
|
<div>
|
|
<h1 className="text-xl font-semibold text-text-main tracking-tight">{title}</h1>
|
|
{description && <p className="text-xs text-text-muted mt-0.5">{description}</p>}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Right actions */}
|
|
<div className="flex items-center gap-3 ml-auto">
|
|
<LanguageSelector />
|
|
<ThemeToggle />
|
|
{!isE2EMode && <DegradationBadge />}
|
|
{!isE2EMode && <TokenHealthBadge />}
|
|
<button
|
|
onClick={handleLogout}
|
|
className="flex items-center justify-center p-2 rounded-lg text-text-muted hover:text-red-500 hover:bg-red-500/10 transition-all"
|
|
title={t("logout")}
|
|
>
|
|
<span className="material-symbols-outlined">logout</span>
|
|
</button>
|
|
</div>
|
|
</header>
|
|
);
|
|
}
|