Merge pull request #3 from BlackRoad-OS/codex/analyze-and-update-blackroad-os-prism-console
Set up Prism Console app and deployment automation
This commit is contained in:
60
.github/workflows/deploy-console.yml
vendored
Normal file
60
.github/workflows/deploy-console.yml
vendored
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
name: Deploy Prism Console
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [dev, staging, main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
NODE_VERSION: '20'
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Select Railway environment
|
||||||
|
run: |
|
||||||
|
BRANCH=${GITHUB_REF##*/}
|
||||||
|
if [ "$BRANCH" = "main" ]; then
|
||||||
|
echo "RAILWAY_ENV=prod" >> $GITHUB_ENV
|
||||||
|
echo "CONSOLE_URL=https://console.blackroad.systems" >> $GITHUB_ENV
|
||||||
|
elif [ "$BRANCH" = "staging" ]; then
|
||||||
|
echo "RAILWAY_ENV=staging" >> $GITHUB_ENV
|
||||||
|
echo "CONSOLE_URL=https://staging.console.blackroad.systems" >> $GITHUB_ENV
|
||||||
|
else
|
||||||
|
echo "RAILWAY_ENV=dev" >> $GITHUB_ENV
|
||||||
|
echo "CONSOLE_URL=${DEV_CONSOLE_URL:-""}" >> $GITHUB_ENV
|
||||||
|
echo "SKIP_HEALTH_CHECK=$([ -z "${DEV_CONSOLE_URL:-}" ] && echo true || echo false)" >> $GITHUB_ENV
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Deploy to Railway
|
||||||
|
uses: railwayapp/railway-action@v2
|
||||||
|
with:
|
||||||
|
railwayToken: ${{ secrets.RAILWAY_TOKEN }}
|
||||||
|
service: prism-console-web
|
||||||
|
environment: ${{ env.RAILWAY_ENV }}
|
||||||
|
|
||||||
|
- name: Health check
|
||||||
|
run: |
|
||||||
|
if [ "${SKIP_HEALTH_CHECK:-false}" = "true" ]; then
|
||||||
|
echo "Skipping health check because CONSOLE_URL is not configured for dev deployments"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
if [ -z "$CONSOLE_URL" ]; then
|
||||||
|
echo "CONSOLE_URL is not set for this environment" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
curl --fail --location "$CONSOLE_URL/health"
|
||||||
37
README.md
37
README.md
@@ -36,3 +36,40 @@ Sample tasks:
|
|||||||
- Worker queue timeline
|
- Worker queue timeline
|
||||||
- Error heatmaps
|
- Error heatmaps
|
||||||
- Rail & Cloudflare integrated view
|
- Rail & Cloudflare integrated view
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
Prism Console deploys to the Railway project **`blackroad-prism-console`** as the service **`prism-console-web`**. It fronts environments published via Cloudflare:
|
||||||
|
|
||||||
|
- **Production:** `https://console.blackroad.systems`
|
||||||
|
- **Staging:** `https://staging.console.blackroad.systems`
|
||||||
|
- **Dev:** Railway-provided dev URL (optionally `https://dev.console.blackroad.systems`)
|
||||||
|
|
||||||
|
### Runtime commands
|
||||||
|
|
||||||
|
- Install: `npm ci`
|
||||||
|
- Build: `npm run build`
|
||||||
|
- Start: `npm run start`
|
||||||
|
|
||||||
|
### Environment variables
|
||||||
|
|
||||||
|
| Variable | Purpose | Prod | Staging | Dev |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| `NODE_ENV` | Runtime environment | `production` | `staging` | `development` |
|
||||||
|
| `CORE_API_URL` | Core backend base URL | `https://core.blackroad.systems` | `https://staging.core.blackroad.systems` | Core dev Railway URL |
|
||||||
|
| `AGENTS_API_URL` | Agents API base URL | `https://agents.blackroad.systems` | `https://staging.agents.blackroad.systems` | Agents dev Railway URL |
|
||||||
|
| `PUBLIC_CONSOLE_URL` | Console base URL | `https://console.blackroad.systems` | `https://staging.console.blackroad.systems` | Dev console URL |
|
||||||
|
| `NEXT_PUBLIC_CORE_API_URL` | Browser-safe core URL | `https://core.blackroad.systems` | `https://staging.core.blackroad.systems` | Core dev Railway URL |
|
||||||
|
| `NEXT_PUBLIC_AGENTS_API_URL` | Browser-safe agents URL | `https://agents.blackroad.systems` | `https://staging.agents.blackroad.systems` | Agents dev Railway URL |
|
||||||
|
| `NEXT_PUBLIC_CONSOLE_URL` | Browser-safe console URL | `https://console.blackroad.systems` | `https://staging.console.blackroad.systems` | Dev console URL |
|
||||||
|
| `DATABASE_URL` | (Optional) Metrics DB connection string | as provisioned | as provisioned | as provisioned |
|
||||||
|
| `SKIP_ENV_VALIDATION` | (Optional) Set to `true` to bypass env enforcement during local builds | optional | optional | `true` (in build script) |
|
||||||
|
|
||||||
|
### Health and status
|
||||||
|
|
||||||
|
- `GET /health` — lightweight liveness JSON suitable for Railway checks.
|
||||||
|
- `GET /status` — static status board that surfaces configured backend URLs; ready to wire to real health endpoints later.
|
||||||
|
|
||||||
|
### Railway definition
|
||||||
|
|
||||||
|
`railway.json` declares the project + service so `railway up` deploys with the right commands and port. Set `RAILWAY_TOKEN` in CI/CD and provide the above env vars per environment before deploying.
|
||||||
|
|||||||
24
eslint.config.mjs
Normal file
24
eslint.config.mjs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import nextPlugin from '@next/eslint-plugin-next';
|
||||||
|
import tsParser from '@typescript-eslint/parser';
|
||||||
|
|
||||||
|
export default [
|
||||||
|
{
|
||||||
|
ignores: ['.next/**/*', 'node_modules/**/*']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
plugins: {
|
||||||
|
'@next/next': nextPlugin
|
||||||
|
},
|
||||||
|
languageOptions: {
|
||||||
|
parser: tsParser,
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 2022,
|
||||||
|
sourceType: 'module'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...nextPlugin.configs['core-web-vitals'].rules,
|
||||||
|
'@next/next/no-html-link-for-pages': 'off'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
6
next-env.d.ts
vendored
Normal file
6
next-env.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/// <reference types="next" />
|
||||||
|
/// <reference types="next/image-types/global" />
|
||||||
|
import "./.next/types/routes.d.ts";
|
||||||
|
|
||||||
|
// NOTE: This file should not be edited
|
||||||
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||||
8
next.config.mjs
Normal file
8
next.config.mjs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {
|
||||||
|
output: 'standalone',
|
||||||
|
reactStrictMode: true,
|
||||||
|
typedRoutes: true
|
||||||
|
};
|
||||||
|
|
||||||
|
export default nextConfig;
|
||||||
5945
package-lock.json
generated
Normal file
5945
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
33
package.json
Normal file
33
package.json
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"name": "blackroad-os-prism-console",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Admin console for deployments, observability, environments, and system control.",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev",
|
||||||
|
"build": "SKIP_ENV_VALIDATION=true next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "eslint . --ext .ts,.tsx"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"prism",
|
||||||
|
"console",
|
||||||
|
"operations"
|
||||||
|
],
|
||||||
|
"author": "Prism Operations Group",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"next": "^16.0.3",
|
||||||
|
"react": "^19.2.0",
|
||||||
|
"react-dom": "^19.2.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^24.10.1",
|
||||||
|
"@types/react": "^19.2.6",
|
||||||
|
"@types/react-dom": "^19.2.3",
|
||||||
|
"@typescript-eslint/parser": "^8.47.0",
|
||||||
|
"eslint": "^9.39.1",
|
||||||
|
"eslint-config-next": "^16.0.3",
|
||||||
|
"typescript": "^5.9.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
23
railway.json
Normal file
23
railway.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://railway.app/railway.schema.json",
|
||||||
|
"project": "blackroad-prism-console",
|
||||||
|
"services": [
|
||||||
|
{
|
||||||
|
"name": "prism-console-web",
|
||||||
|
"source": "./",
|
||||||
|
"port": 3000,
|
||||||
|
"buildCommand": "npm run build",
|
||||||
|
"startCommand": "npm run start",
|
||||||
|
"envVariables": {
|
||||||
|
"NODE_ENV": "production",
|
||||||
|
"CORE_API_URL": "",
|
||||||
|
"AGENTS_API_URL": "",
|
||||||
|
"PUBLIC_CONSOLE_URL": "",
|
||||||
|
"NEXT_PUBLIC_CORE_API_URL": "",
|
||||||
|
"NEXT_PUBLIC_AGENTS_API_URL": "",
|
||||||
|
"NEXT_PUBLIC_CONSOLE_URL": "",
|
||||||
|
"DATABASE_URL": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
115
src/app/globals.css
Normal file
115
src/app/globals.css
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
:root {
|
||||||
|
color-scheme: light;
|
||||||
|
--bg: #0b1021;
|
||||||
|
--panel: #11162d;
|
||||||
|
--card: #151b34;
|
||||||
|
--text: #e8ecf7;
|
||||||
|
--muted: #9aa5c0;
|
||||||
|
--accent: #7dd3fc;
|
||||||
|
--accent-strong: #38bdf8;
|
||||||
|
--border: #1f2946;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||||
|
background: radial-gradient(circle at 20% 20%, rgba(56, 189, 248, 0.08), transparent 25%),
|
||||||
|
radial-gradient(circle at 80% 10%, rgba(99, 102, 241, 0.08), transparent 20%),
|
||||||
|
var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--accent-strong);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
padding: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav {
|
||||||
|
background: var(--panel);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 16px 32px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: var(--card);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
box-shadow: 0 15px 40px rgba(0, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 12px;
|
||||||
|
background: rgba(125, 211, 252, 0.1);
|
||||||
|
color: var(--accent-strong);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-top: 12px;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table th,
|
||||||
|
.table td {
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
padding: 8px 12px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.muted {
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-ok {
|
||||||
|
color: #22c55e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-warn {
|
||||||
|
color: #facc15;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-bad {
|
||||||
|
color: #f87171;
|
||||||
|
}
|
||||||
16
src/app/health/route.ts
Normal file
16
src/app/health/route.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
import { config } from '@/lib/config';
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
const payload = {
|
||||||
|
status: 'ok',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
environment: config.environment,
|
||||||
|
services: {
|
||||||
|
coreApiConfigured: Boolean(config.coreApiUrl),
|
||||||
|
agentsApiConfigured: Boolean(config.agentsApiUrl)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return NextResponse.json(payload);
|
||||||
|
}
|
||||||
22
src/app/layout.tsx
Normal file
22
src/app/layout.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import './globals.css';
|
||||||
|
import { ReactNode } from 'react';
|
||||||
|
import AppShell from '@/components/layout/AppShell';
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
title: 'Prism Console',
|
||||||
|
description: 'Operational console for BlackRoad OS'
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
children: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RootLayout({ children }: Props) {
|
||||||
|
return (
|
||||||
|
<html lang="en">
|
||||||
|
<body>
|
||||||
|
<AppShell>{children}</AppShell>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
66
src/app/page.tsx
Normal file
66
src/app/page.tsx
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { config, getStaticServiceHealth, publicConfig } from '@/lib/config';
|
||||||
|
|
||||||
|
const cards = [
|
||||||
|
{ title: 'Core API', key: 'coreApiUrl', description: 'Primary backend for Prism data.' },
|
||||||
|
{ title: 'Agents API', key: 'agentsApiUrl', description: 'Agent runtime surface area.' },
|
||||||
|
{ title: 'Console URL', key: 'consoleUrl', description: 'Public entrypoint for this console.' }
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
const serviceHealth = getStaticServiceHealth();
|
||||||
|
const resolvedValues = {
|
||||||
|
coreApiUrl: publicConfig.coreApiUrl || config.coreApiUrl,
|
||||||
|
agentsApiUrl: publicConfig.agentsApiUrl || config.agentsApiUrl,
|
||||||
|
consoleUrl: publicConfig.consoleUrl || config.consoleUrl
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid">
|
||||||
|
<div className="card">
|
||||||
|
<h3>Environment</h3>
|
||||||
|
<p className="muted">Aligns with Railway and Cloudflare mapping.</p>
|
||||||
|
<div className="badge">{config.environment}</div>
|
||||||
|
<table className="table">
|
||||||
|
<tbody>
|
||||||
|
{cards.map((card) => (
|
||||||
|
<tr key={card.key}>
|
||||||
|
<td>{card.title}</td>
|
||||||
|
<td className="muted">
|
||||||
|
{resolvedValues[card.key] || 'not set'}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<p className="muted" style={{ marginTop: 12 }}>
|
||||||
|
Add authentication here later — centralize auth checks before rendering protected pages.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="card">
|
||||||
|
<h3>Connectivity</h3>
|
||||||
|
<p className="muted">Configuration-based readiness of upstream services.</p>
|
||||||
|
<ul>
|
||||||
|
{serviceHealth.map((service) => (
|
||||||
|
<li key={service.name}>
|
||||||
|
{service.name}:{' '}
|
||||||
|
<span className={service.configured ? 'status-ok' : 'status-bad'}>
|
||||||
|
{service.configured ? 'Configured' : 'Missing'}
|
||||||
|
</span>{' '}
|
||||||
|
<span className="muted">{service.url || 'not set'}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="card">
|
||||||
|
<h3>Notes</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Use /status for a focused service status board.</li>
|
||||||
|
<li>Centralized fetch helper lives in <code>src/lib/api.ts</code>.</li>
|
||||||
|
<li>Health endpoint: <code>/health</code>.</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
30
src/app/status/page.tsx
Normal file
30
src/app/status/page.tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { getStaticServiceHealth, publicConfig } from '@/lib/config';
|
||||||
|
import { StatusCard } from '@/components/status/StatusCard';
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
title: 'System Status | Prism Console'
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function StatusPage() {
|
||||||
|
const services = getStaticServiceHealth();
|
||||||
|
const env = publicConfig.environment;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid">
|
||||||
|
<StatusCard
|
||||||
|
title="Prism Console"
|
||||||
|
description="Static overview of configured upstream services. Replace mock health with live pings when endpoints are ready."
|
||||||
|
environment={env}
|
||||||
|
services={services}
|
||||||
|
/>
|
||||||
|
<div className="card">
|
||||||
|
<h3>Next Steps</h3>
|
||||||
|
<ol>
|
||||||
|
<li>Wire status to lightweight read-only endpoints from core and agents.</li>
|
||||||
|
<li>Add authentication guard before exposing sensitive data.</li>
|
||||||
|
<li>Expand to deployment + observability widgets under /status.</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
37
src/components/layout/AppShell.tsx
Normal file
37
src/components/layout/AppShell.tsx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import Link from 'next/link';
|
||||||
|
import { ReactNode } from 'react';
|
||||||
|
import type { Route } from 'next';
|
||||||
|
import { config } from '@/lib/config';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
children: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const navLinks: { href: Route; label: string }[] = [
|
||||||
|
{ href: '/', label: 'Overview' },
|
||||||
|
{ href: '/status', label: 'Status' },
|
||||||
|
{ href: '/health', label: 'Health API' }
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function AppShell({ children }: Props) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<nav>
|
||||||
|
<div className="nav-links">
|
||||||
|
<Link href="/" className="logo">
|
||||||
|
Prism Console
|
||||||
|
</Link>
|
||||||
|
{navLinks.map((link) => (
|
||||||
|
<Link key={link.href} href={link.href}>
|
||||||
|
{link.label}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
<span className="badge" style={{ marginLeft: 'auto' }}>
|
||||||
|
Environment: {config.environment}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<main>{children}</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
43
src/components/status/StatusCard.tsx
Normal file
43
src/components/status/StatusCard.tsx
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { ServiceHealth } from '@/lib/config';
|
||||||
|
|
||||||
|
export type StatusCardProps = {
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
environment: string;
|
||||||
|
services: ServiceHealth[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function StatusCard({ title, description, environment, services }: StatusCardProps) {
|
||||||
|
return (
|
||||||
|
<div className="card">
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
|
<h3>{title}</h3>
|
||||||
|
<span className="badge">{environment}</span>
|
||||||
|
</div>
|
||||||
|
{description && <p className="muted">{description}</p>}
|
||||||
|
<table className="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Service</th>
|
||||||
|
<th>URL</th>
|
||||||
|
<th>Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{services.map((service) => (
|
||||||
|
<tr key={service.name}>
|
||||||
|
<td>{service.name}</td>
|
||||||
|
<td className="muted">{service.url || 'not set'}</td>
|
||||||
|
<td className={service.configured ? 'status-ok' : 'status-bad'}>
|
||||||
|
{service.configured ? 'Configured' : 'Missing'}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{!services.every((svc) => svc.configured) && (
|
||||||
|
<p className="muted">Backend URLs are required in staging/production.</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
41
src/lib/api.ts
Normal file
41
src/lib/api.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { config } from './config';
|
||||||
|
|
||||||
|
type FetchTarget = 'core' | 'agents';
|
||||||
|
|
||||||
|
type ApiClientOptions = {
|
||||||
|
target: FetchTarget;
|
||||||
|
path: string;
|
||||||
|
init?: RequestInit;
|
||||||
|
};
|
||||||
|
|
||||||
|
const baseMap: Record<FetchTarget, string> = {
|
||||||
|
core: config.coreApiUrl,
|
||||||
|
agents: config.agentsApiUrl
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Centralized fetch helper for backend calls.
|
||||||
|
* TODO: add authentication headers or cookies once auth is available.
|
||||||
|
*/
|
||||||
|
export async function apiClient<T>({ target, path, init }: ApiClientOptions): Promise<T> {
|
||||||
|
const baseUrl = baseMap[target];
|
||||||
|
|
||||||
|
if (!baseUrl) {
|
||||||
|
throw new Error(`${target} API URL is not configured`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = new URL(path, baseUrl);
|
||||||
|
const response = await fetch(url.toString(), {
|
||||||
|
...init,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...(init?.headers ?? {})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`${target} API responded with ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json() as Promise<T>;
|
||||||
|
}
|
||||||
72
src/lib/config.ts
Normal file
72
src/lib/config.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
export type RuntimeEnvironment = 'development' | 'staging' | 'production';
|
||||||
|
|
||||||
|
type ReadEnvOptions = {
|
||||||
|
optional?: boolean;
|
||||||
|
defaultValue?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const nodeEnvRaw = (process.env.NODE_ENV || 'development') as string;
|
||||||
|
const environment: RuntimeEnvironment =
|
||||||
|
nodeEnvRaw === 'production' ? 'production' : nodeEnvRaw === 'staging' ? 'staging' : 'development';
|
||||||
|
|
||||||
|
const isDev = environment === 'development';
|
||||||
|
const requireEnv = !isDev && process.env.SKIP_ENV_VALIDATION !== 'true';
|
||||||
|
|
||||||
|
function readEnv(variable: string, { optional, defaultValue }: ReadEnvOptions = {}): string {
|
||||||
|
const value = process.env[variable];
|
||||||
|
|
||||||
|
if (!value && !optional && requireEnv) {
|
||||||
|
throw new Error(`Missing required environment variable ${variable} for ${environment}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!value && defaultValue) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
environment,
|
||||||
|
nodeEnv: environment,
|
||||||
|
coreApiUrl: readEnv('CORE_API_URL', { optional: isDev }),
|
||||||
|
agentsApiUrl: readEnv('AGENTS_API_URL', { optional: isDev }),
|
||||||
|
consoleUrl: readEnv('PUBLIC_CONSOLE_URL', { optional: isDev }),
|
||||||
|
get isProduction() {
|
||||||
|
return environment === 'production';
|
||||||
|
},
|
||||||
|
get isStaging() {
|
||||||
|
return environment === 'staging';
|
||||||
|
},
|
||||||
|
get isDevelopment() {
|
||||||
|
return environment === 'development';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const publicConfig = {
|
||||||
|
environment,
|
||||||
|
consoleUrl: readEnv('NEXT_PUBLIC_CONSOLE_URL', { optional: isDev }),
|
||||||
|
coreApiUrl: readEnv('NEXT_PUBLIC_CORE_API_URL', { optional: isDev }),
|
||||||
|
agentsApiUrl: readEnv('NEXT_PUBLIC_AGENTS_API_URL', { optional: isDev })
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ServiceHealth = {
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
configured: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getStaticServiceHealth(): ServiceHealth[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'Core API',
|
||||||
|
url: publicConfig.coreApiUrl || config.coreApiUrl,
|
||||||
|
configured: Boolean(publicConfig.coreApiUrl || config.coreApiUrl)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Agents API',
|
||||||
|
url: publicConfig.agentsApiUrl || config.agentsApiUrl,
|
||||||
|
configured: Boolean(publicConfig.agentsApiUrl || config.agentsApiUrl)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
44
tsconfig.json
Normal file
44
tsconfig.json
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es5",
|
||||||
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"dom.iterable",
|
||||||
|
"esnext"
|
||||||
|
],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"incremental": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": [
|
||||||
|
"./src/*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"next-env.d.ts",
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
".next/types/**/*.ts",
|
||||||
|
".next/dev/types/**/*.ts"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user