feat(api): Phase 7 — API & Code Quality

7.1 — Consolidated rate-limit routes
  - Merged rate-limit/ and rate-limits/ into rate-limits/route.js
  - GET returns connections + overview + lockouts + cacheStats (unified)
  - POST handles toggle protection
  - Old rate-limit/ now redirects 308 → rate-limits/
  - Updated frontend fetch URL in providers/[id]/page.js

7.2 — Zod schema for provider constants
  - New: src/shared/validation/providerSchema.js
  - Validates FREE_PROVIDERS, OAUTH_PROVIDERS, APIKEY_PROVIDERS at module load
  - Catches config drift (invalid colors, missing fields) at startup

7.3 — TailwindCSS error pages
  - Converted not-found.js inline styles → Tailwind classes
  - Converted global-error.js inline styles → Tailwind classes
  - Replaced JS hover handlers with Tailwind hover: utilities

7.4 — Fixed GitHub link in privacy page
  - decolua/omniroute → diegosouzapw/OmniRoute

7.5 — Already completed in Phase 5

Tests: 295 pass | Build: success
This commit is contained in:
diegosouzapw
2026-02-15 10:15:44 -03:00
parent 05e93fa9b0
commit 7cf8ae8db6
8 changed files with 135 additions and 170 deletions

View File

@@ -276,7 +276,7 @@ export default function ProviderDetailPage() {
const handleToggleRateLimit = async (connectionId, enabled) => {
try {
const res = await fetch("/api/rate-limit", {
const res = await fetch("/api/rate-limits", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ connectionId, enabled }),

View File

@@ -1,63 +1,18 @@
import { NextResponse } from "next/server";
import { getProviderConnections, updateProviderConnection } from "@/lib/localDb";
import {
enableRateLimitProtection,
disableRateLimitProtection,
getRateLimitStatus,
getAllRateLimitStatus,
} from "@omniroute/open-sse/services/rateLimitManager.js";
/**
* GET /api/rate-limit — Get rate limit status for all connections
* @deprecated Use /api/rate-limits instead.
* This route redirects to the consolidated rate-limits endpoint.
*/
export async function GET() {
try {
const connections = await getProviderConnections();
const statuses = connections.map((conn) => ({
connectionId: conn.id,
provider: conn.provider,
name: conn.name || conn.email || conn.id.slice(0, 8),
rateLimitProtection: !!conn.rateLimitProtection,
...getRateLimitStatus(conn.provider, conn.id),
}));
return NextResponse.json({
connections: statuses,
overview: getAllRateLimitStatus(),
});
} catch (error) {
console.error("[API ERROR] /api/rate-limit GET:", error);
return NextResponse.json({ error: "Failed to get rate limit status" }, { status: 500 });
}
export async function GET(request) {
const url = new URL(request.url);
url.pathname = "/api/rate-limits";
return NextResponse.redirect(url, 308);
}
/**
* POST /api/rate-limit — Toggle rate limit protection for a connection
* Body: { connectionId: string, enabled: boolean }
*/
export async function POST(request) {
try {
const { connectionId, enabled } = await request.json();
if (!connectionId) {
return NextResponse.json({ error: "Missing connectionId" }, { status: 400 });
}
// Update in-memory state
if (enabled) {
enableRateLimitProtection(connectionId);
} else {
disableRateLimitProtection(connectionId);
}
// Persist to database
await updateProviderConnection(connectionId, {
rateLimitProtection: !!enabled,
});
return NextResponse.json({ success: true, connectionId, enabled: !!enabled });
} catch (error) {
console.error("[API ERROR] /api/rate-limit POST:", error);
return NextResponse.json({ error: "Failed to toggle rate limit" }, { status: 500 });
}
const url = new URL(request.url);
url.pathname = "/api/rate-limits";
return NextResponse.redirect(url, 308);
}

View File

@@ -1,15 +1,76 @@
import { NextResponse } from "next/server";
import {
getAllModelLockouts,
} from "@omniroute/open-sse/services/accountFallback.js";
import { getAllModelLockouts } from "@omniroute/open-sse/services/accountFallback.js";
import { getCacheStats } from "@omniroute/open-sse/services/signatureCache.js";
import { getProviderConnections, updateProviderConnection } from "@/lib/localDb";
import {
enableRateLimitProtection,
disableRateLimitProtection,
getRateLimitStatus,
getAllRateLimitStatus,
} from "@omniroute/open-sse/services/rateLimitManager.js";
/**
* GET /api/rate-limits — Consolidated rate-limit status
*
* Returns:
* - Per-connection rate-limit status (protection toggle, current state)
* - Global overview (all providers)
* - Model lockouts
* - Signature cache stats
*/
export async function GET() {
try {
const connections = await getProviderConnections();
const statuses = connections.map((conn) => ({
connectionId: conn.id,
provider: conn.provider,
name: conn.name || conn.email || conn.id.slice(0, 8),
rateLimitProtection: !!conn.rateLimitProtection,
...getRateLimitStatus(conn.provider, conn.id),
}));
const lockouts = getAllModelLockouts();
const cacheStats = getCacheStats();
return NextResponse.json({ lockouts, cacheStats });
return NextResponse.json({
connections: statuses,
overview: getAllRateLimitStatus(),
lockouts,
cacheStats,
});
} catch (error) {
return NextResponse.json({ error: error.message }, { status: 500 });
console.error("[API ERROR] /api/rate-limits GET:", error);
return NextResponse.json({ error: "Failed to get rate limit status" }, { status: 500 });
}
}
/**
* POST /api/rate-limits — Toggle rate limit protection for a connection
* Body: { connectionId: string, enabled: boolean }
*/
export async function POST(request) {
try {
const { connectionId, enabled } = await request.json();
if (!connectionId) {
return NextResponse.json({ error: "Missing connectionId" }, { status: 400 });
}
// Update in-memory state
if (enabled) {
enableRateLimitProtection(connectionId);
} else {
disableRateLimitProtection(connectionId);
}
// Persist to database
await updateProviderConnection(connectionId, {
rateLimitProtection: !!enabled,
});
return NextResponse.json({ success: true, connectionId, enabled: !!enabled });
} catch (error) {
console.error("[API ERROR] /api/rate-limits POST:", error);
return NextResponse.json({ error: "Failed to toggle rate limit" }, { status: 500 });
}
}

View File

@@ -6,88 +6,29 @@
* Root-level error boundary for unrecoverable errors.
* This is the last resort — catches errors that the per-page
* error.js boundaries don't handle.
* Styled with TailwindCSS 4 (Phase 7.3).
*/
export default function GlobalError({ error, reset }) {
return (
<html>
<body
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
minHeight: "100vh",
padding: "24px",
background: "#0a0a0f",
color: "#e0e0e0",
fontFamily: "system-ui, -apple-system, sans-serif",
textAlign: "center",
margin: 0,
}}
>
<div
style={{
fontSize: "64px",
marginBottom: "16px",
}}
>
</div>
<h1
style={{
fontSize: "28px",
fontWeight: 700,
marginBottom: "8px",
}}
>
Something went wrong
</h1>
<p
style={{
fontSize: "15px",
color: "#888",
maxWidth: "400px",
lineHeight: 1.5,
marginBottom: "24px",
}}
>
<body className="flex flex-col items-center justify-center min-h-screen p-6 bg-[#0a0a0f] text-[#e0e0e0] font-[system-ui,-apple-system,sans-serif] text-center m-0">
<div className="text-[64px] mb-4"></div>
<h1 className="text-[28px] font-bold mb-2">Something went wrong</h1>
<p className="text-[15px] text-[#888] max-w-[400px] leading-relaxed mb-6">
An unexpected error occurred. This has been logged and our team will investigate.
</p>
{process.env.NODE_ENV === "development" && error?.message && (
<pre
style={{
padding: "16px",
borderRadius: "8px",
background: "rgba(239, 68, 68, 0.1)",
border: "1px solid rgba(239, 68, 68, 0.3)",
color: "#ef4444",
fontSize: "12px",
maxWidth: "600px",
overflow: "auto",
textAlign: "left",
marginBottom: "24px",
}}
>
<pre className="p-4 rounded-lg bg-red-500/10 border border-red-500/30 text-red-500 text-xs max-w-[600px] overflow-auto text-left mb-6">
{error.message}
</pre>
)}
<button
onClick={reset}
className="px-8 py-3 rounded-[10px] text-white border-none text-sm font-semibold cursor-pointer transition-transform duration-200 shadow-[0_4px_16px_rgba(99,102,241,0.3)] hover:-translate-y-0.5"
style={{
padding: "12px 32px",
borderRadius: "10px",
background: "linear-gradient(135deg, #6366f1, #8b5cf6)",
color: "#fff",
border: "none",
fontSize: "14px",
fontWeight: 600,
cursor: "pointer",
transition: "transform 0.2s",
boxShadow: "0 4px 16px rgba(99, 102, 241, 0.3)",
}}
onMouseEnter={(e) => (e.target.style.transform = "translateY(-2px)")}
onMouseLeave={(e) => (e.target.style.transform = "translateY(0)")}
>
Try Again
</button>

View File

@@ -4,70 +4,33 @@
* Custom Not Found Page — FASE-04 Error Handling
*
* Displayed when a user navigates to a non-existent route.
* Styled with TailwindCSS 4 (Phase 7.3).
*/
import Link from "next/link";
export default function NotFound() {
return (
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
minHeight: "100vh",
padding: "24px",
background: "var(--bg-primary, #0a0a0f)",
color: "var(--text-primary, #e0e0e0)",
textAlign: "center",
}}
>
<div className="flex flex-col items-center justify-center min-h-screen p-6 bg-[var(--bg-primary,#0a0a0f)] text-[var(--text-primary,#e0e0e0)] text-center">
<div
className="text-[96px] font-extrabold leading-none mb-2"
style={{
fontSize: "96px",
fontWeight: 800,
background: "linear-gradient(135deg, #6366f1 0%, #8b5cf6 50%, #a855f7 100%)",
WebkitBackgroundClip: "text",
WebkitTextFillColor: "transparent",
lineHeight: 1,
marginBottom: "8px",
}}
>
404
</div>
<h1
style={{
fontSize: "24px",
fontWeight: 600,
marginBottom: "8px",
}}
>
Page not found
</h1>
<p
style={{
fontSize: "15px",
color: "var(--text-secondary, #888)",
maxWidth: "400px",
lineHeight: 1.5,
marginBottom: "32px",
}}
>
<h1 className="text-2xl font-semibold mb-2">Page not found</h1>
<p className="text-[15px] text-[var(--text-secondary,#888)] max-w-[400px] leading-relaxed mb-8">
The page you&apos;re looking for doesn&apos;t exist or has been moved.
</p>
<Link
href="/dashboard"
className="px-8 py-3 rounded-[10px] text-white text-sm font-semibold no-underline transition-all duration-200 shadow-[0_4px_16px_rgba(99,102,241,0.3)] hover:-translate-y-0.5"
style={{
padding: "12px 32px",
borderRadius: "10px",
background: "linear-gradient(135deg, #6366f1, #8b5cf6)",
color: "#fff",
fontSize: "14px",
fontWeight: 600,
textDecoration: "none",
transition: "all 0.2s",
boxShadow: "0 4px 16px rgba(99, 102, 241, 0.3)",
}}
>
Go to Dashboard

View File

@@ -145,7 +145,7 @@ export default function PrivacyPage() {
<p>
Questions? Visit our{" "}
<a
href="https://github.com/decolua/omniroute"
href="https://github.com/diegosouzapw/OmniRoute"
className="text-primary hover:underline"
target="_blank"
rel="noopener noreferrer"

View File

@@ -329,3 +329,10 @@ export const ID_TO_ALIAS = Object.values(AI_PROVIDERS).reduce((acc, p) => {
// Providers that support usage/quota API
export const USAGE_SUPPORTED_PROVIDERS = ["antigravity", "kiro", "github", "codex", "claude"];
// ── Zod validation at module load (Phase 7.2) ──
import { validateProviders } from "../validation/providerSchema.js";
validateProviders(FREE_PROVIDERS, "FREE_PROVIDERS");
validateProviders(OAUTH_PROVIDERS, "OAUTH_PROVIDERS");
validateProviders(APIKEY_PROVIDERS, "APIKEY_PROVIDERS");

View File

@@ -0,0 +1,38 @@
/**
* Provider Schema Validation — Phase 7.2
*
* Zod schemas for provider constant validation.
* Validates FREE_PROVIDERS, OAUTH_PROVIDERS, and APIKEY_PROVIDERS
* at module load time to catch configuration drift early.
*
* @module shared/validation/providerSchema
*/
import { z } from "zod";
export const ProviderSchema = z.object({
id: z.string().min(1),
alias: z.string().min(1),
name: z.string().min(1),
icon: z.string().min(1),
color: z.string().regex(/^#[0-9A-Fa-f]{6}$/, "Must be a valid hex color (#RRGGBB)"),
textIcon: z.string().optional(),
website: z.string().url().optional(),
passthroughModels: z.boolean().optional(),
});
export const ProvidersMapSchema = z.record(z.string(), ProviderSchema);
/**
* Validate a providers map, throwing a descriptive error on failure.
* @param {Record<string, object>} map - The providers map to validate
* @param {string} name - Name of the map for error messages
*/
export function validateProviders(map, name) {
const result = ProvidersMapSchema.safeParse(map);
if (!result.success) {
const issues = result.error.issues.map((i) => ` ${i.path.join(".")}: ${i.message}`).join("\n");
console.error(`[PROVIDER VALIDATION] ${name} has invalid entries:\n${issues}`);
throw new Error(`Provider validation failed for ${name}`);
}
}