Skip to content

Commit 1ef6d36

Browse files
committed
feat: Add dashboard loading and error UIs, introduce standardized API response utilities, enhance call log redaction and configurable retention, and document architectural decisions.
1 parent 3519aca commit 1ef6d36

File tree

14 files changed

+390
-10
lines changed

14 files changed

+390
-10
lines changed

.github/workflows/ci.yml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,3 +108,39 @@ jobs:
108108
- run: npx playwright install --with-deps chromium
109109
- run: npm run build
110110
- run: npm run test:e2e
111+
112+
test-integration:
113+
name: Integration Tests
114+
runs-on: ubuntu-latest
115+
needs: build
116+
env:
117+
JWT_SECRET: ci-test-secret-with-sufficient-length-for-validation
118+
API_KEY_SECRET: ci-test-api-key-secret-long
119+
INITIAL_PASSWORD: ci-test-password-for-integration
120+
DATA_DIR: /tmp/omniroute-ci
121+
steps:
122+
- uses: actions/checkout@v6
123+
- uses: actions/setup-node@v6
124+
with:
125+
node-version: 22
126+
cache: npm
127+
- run: npm ci
128+
- run: npm run test:integration
129+
continue-on-error: true
130+
131+
test-security:
132+
name: Security Tests
133+
runs-on: ubuntu-latest
134+
needs: build
135+
env:
136+
JWT_SECRET: ci-test-secret-with-sufficient-length-for-validation
137+
API_KEY_SECRET: ci-test-api-key-secret-long
138+
steps:
139+
- uses: actions/checkout@v6
140+
- uses: actions/setup-node@v6
141+
with:
142+
node-version: 22
143+
cache: npm
144+
- run: npm ci
145+
- run: npm run test:security
146+
continue-on-error: true

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ src/ # TypeScript (.ts / .tsx)
159159
│ ├── cacheLayer.ts # LRU cache
160160
│ ├── semanticCache.ts # Semantic response cache
161161
│ ├── idempotencyLayer.ts # Request deduplication
162-
│ └── localDb.ts # LowDB (JSON) storage
162+
│ └── localDb.ts # Settings facade (LowDB for config, SQLite for domain data)
163163
├── shared/
164164
│ ├── components/ # React components (.tsx)
165165
│ ├── middleware/ # Correlation IDs, etc.

docs/ARCHITECTURE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -771,8 +771,8 @@ Environment variables actively used by code:
771771

772772
## Operational Verification Checklist
773773

774-
- Build from source: `cd /root/dev/omniroute && npm run build`
775-
- Build Docker image: `cd /root/dev/omniroute && docker build -t omniroute .`
774+
- Build from source: `npm run build`
775+
- Build Docker image: `docker build -t omniroute .`
776776
- Start service and verify:
777777
- `GET /api/settings`
778778
- `GET /api/v1/models`
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# ADR-001: Next.js as the Foundation for an AI Gateway
2+
3+
## Status: Accepted
4+
5+
## Context
6+
7+
OmniRoute is an AI routing gateway that translates, forwards, and manages requests across 20+ LLM providers. We needed a framework that could serve both the API proxy layer and a management dashboard from a single codebase.
8+
9+
**Alternatives considered:**
10+
11+
- **Express.js only** — Simpler proxy, but requires separate frontend tooling
12+
- **Fastify** — Fast, but no built-in SSR/dashboard support
13+
- **Next.js** — Unified full-stack framework with API routes, SSR, and static pages
14+
15+
## Decision
16+
17+
We chose Next.js because:
18+
19+
1. **Single deployment** — API routes (`/api/*`) and dashboard UI in one process
20+
2. **Middleware layer** — Native request interception for auth guards and request tracing
21+
3. **File-based routing** — Easy to map provider endpoints to handlers
22+
4. **Built-in TypeScript** — Type safety across the entire codebase
23+
24+
## Consequences
25+
26+
**Positive:**
27+
28+
- One `npm run build` produces both API and UI
29+
- Middleware provides centralized auth and request tracing
30+
- Dashboard gets automatic code splitting and optimization
31+
32+
**Negative:**
33+
34+
- Next.js middleware has limitations (no heavy imports, edge runtime constraints)
35+
- Serverless deployment model doesn't align with persistent WebSocket/SSE connections
36+
- Build times are longer than Express-only setups
37+
- The SSE proxy layer (`open-sse/`) operates outside Next.js conventions
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# ADR-002: Hub-and-Spoke Translation with OpenAI as Intermediate Format
2+
3+
## Status: Accepted
4+
5+
## Context
6+
7+
OmniRoute routes requests across 20+ providers, each with its own API format (OpenAI, Anthropic Messages, Google Gemini, AWS Bedrock, etc.). Direct provider-to-provider translation would require O(n²) translators.
8+
9+
**Alternatives considered:**
10+
11+
- **Direct translation** — Each pair needs a dedicated translator (n² complexity)
12+
- **Common intermediate format** — Translate to/from a canonical format (2n complexity)
13+
- **Protocol buffers** — Strong typing but heavy overhead for a proxy
14+
15+
## Decision
16+
17+
We use the **OpenAI Chat Completions format** as the canonical intermediate representation. All incoming requests are normalized to OpenAI format, processed, then translated to the target provider's format.
18+
19+
```
20+
Client → [any format] → OpenAI canonical → [target format] → Provider
21+
Provider → [response] → OpenAI canonical → [original format] → Client
22+
```
23+
24+
## Consequences
25+
26+
**Positive:**
27+
28+
- Only 2 translators per provider (inbound + outbound) instead of n² pairs
29+
- OpenAI format is the de facto standard — most clients already use it
30+
- Adding a new provider requires only implementing one translator pair
31+
- Streaming (SSE) works consistently through the canonical format
32+
33+
**Negative:**
34+
35+
- Some provider-specific features may be lost in translation
36+
- The double translation adds latency (typically < 5ms)
37+
- OpenAI format changes require updating the canonical representation
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# ADR-003: Dual Storage — SQLite Primary with JSON Migration Path
2+
3+
## Status: Accepted
4+
5+
## Context
6+
7+
OmniRoute originally used LowDB (JSON file) for all persistence. As the project grew, JSON-based storage became a bottleneck for concurrent access, querying, and data integrity.
8+
9+
**Alternatives considered:**
10+
11+
- **LowDB only** — Simple but no concurrent access, no ACID, no querying
12+
- **SQLite only** — Fast, ACID-compliant, but breaks existing deployments
13+
- **PostgreSQL** — Production-grade but requires external dependency
14+
- **Dual storage with migration** — SQLite primary + automatic JSON migration
15+
16+
## Decision
17+
18+
We migrated to **SQLite as the primary store** with an automatic one-time migration from `db.json`:
19+
20+
1. On startup, if `db.json` exists and SQLite is empty, auto-migrate all data
21+
2. All new reads/writes go through SQLite
22+
3. The `db.json` file is preserved but no longer written to
23+
24+
Settings remain in a hybrid model where LowDB handles simple key-value configuration for backward compatibility.
25+
26+
## Consequences
27+
28+
**Positive:**
29+
30+
- ACID transactions for provider connections, API keys, and usage data
31+
- Proper SQL queries for analytics and log filtering
32+
- Concurrent read/write safety via WAL mode
33+
- Zero-downtime migration from JSON — users upgrade transparently
34+
35+
**Negative:**
36+
37+
- Two storage engines to maintain (SQLite + LowDB for settings)
38+
- Migration code must handle edge cases and partial data
39+
- SQLite binary dependency needed in deployment environments

eslint.config.mjs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,33 @@ import nextVitals from "eslint-config-next/core-web-vitals";
33
/** @type {import("eslint").Linter.Config[]} */
44
const eslintConfig = [
55
...nextVitals,
6-
// FASE-02: Security rules
6+
// FASE-02: Security rules (strict everywhere)
77
{
88
rules: {
99
"no-eval": "error",
1010
"no-implied-eval": "error",
1111
"no-new-func": "error",
1212
},
1313
},
14-
// Global ignores
14+
// Relaxed rules for open-sse and tests (incremental adoption)
15+
{
16+
files: ["open-sse/**/*.ts", "tests/**/*.mjs", "tests/**/*.ts"],
17+
rules: {
18+
"@typescript-eslint/no-explicit-any": "warn",
19+
"@next/next/no-assign-module-variable": "off",
20+
"react-hooks/rules-of-hooks": "off",
21+
},
22+
},
23+
// Global ignores (open-sse and tests REMOVED — now linted)
1524
{
1625
ignores: [
1726
".next/**",
1827
"out/**",
1928
"build/**",
2029
"next-env.d.ts",
21-
"tests/**",
2230
"scripts/**",
2331
"bin/**",
2432
"node_modules/**",
25-
"open-sse/**",
2633
],
2734
},
2835
];
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
"use client";
2+
3+
export default function AnalyticsLoading() {
4+
return (
5+
<div className="space-y-6 animate-pulse p-6">
6+
<div className="h-8 bg-gray-200 dark:bg-gray-700 rounded w-40" />
7+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
8+
{[1, 2, 3, 4].map((i) => (
9+
<div key={i} className="h-24 bg-gray-200 dark:bg-gray-700 rounded-lg" />
10+
))}
11+
</div>
12+
<div className="h-64 bg-gray-200 dark:bg-gray-700 rounded-lg" />
13+
</div>
14+
);
15+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
"use client";
2+
3+
export default function ProvidersError({
4+
error,
5+
reset,
6+
}: {
7+
error: Error & { digest?: string };
8+
reset: () => void;
9+
}) {
10+
return (
11+
<div className="flex flex-col items-center justify-center min-h-[400px] p-6">
12+
<div className="text-center space-y-4">
13+
<h2 className="text-xl font-semibold text-red-600 dark:text-red-400">
14+
Failed to load providers
15+
</h2>
16+
<p className="text-gray-600 dark:text-gray-400 max-w-md">
17+
{error.message || "An unexpected error occurred while loading provider data."}
18+
</p>
19+
<button
20+
onClick={reset}
21+
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
22+
>
23+
Try Again
24+
</button>
25+
</div>
26+
</div>
27+
);
28+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
"use client";
2+
3+
export default function ProvidersLoading() {
4+
return (
5+
<div className="space-y-6 animate-pulse p-6">
6+
<div className="h-8 bg-gray-200 dark:bg-gray-700 rounded w-48" />
7+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
8+
{[1, 2, 3].map((i) => (
9+
<div key={i} className="h-40 bg-gray-200 dark:bg-gray-700 rounded-lg" />
10+
))}
11+
</div>
12+
</div>
13+
);
14+
}

0 commit comments

Comments
 (0)