feat: add react api and shadcn foundation
This commit is contained in:
parent
f6d6d39a7b
commit
a71d66b1b8
21 changed files with 1524 additions and 36 deletions
25
components.json
Normal file
25
components.json
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
|
"style": "radix-nova",
|
||||||
|
"rsc": false,
|
||||||
|
"tsx": true,
|
||||||
|
"tailwind": {
|
||||||
|
"config": "",
|
||||||
|
"css": "src/app.css",
|
||||||
|
"baseColor": "neutral",
|
||||||
|
"cssVariables": true,
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"iconLibrary": "lucide",
|
||||||
|
"rtl": false,
|
||||||
|
"aliases": {
|
||||||
|
"components": "$lib/components",
|
||||||
|
"utils": "$lib/utils",
|
||||||
|
"ui": "$lib/components/ui",
|
||||||
|
"lib": "$lib/.",
|
||||||
|
"hooks": "$lib/hooks"
|
||||||
|
},
|
||||||
|
"menuColor": "default",
|
||||||
|
"menuAccent": "subtle",
|
||||||
|
"registries": {}
|
||||||
|
}
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
**Architecture:** The browser runtime becomes React mounted from `src/main.tsx`. A Bun server at `src/server/index.ts` serves the static React build and exposes JSON API routes for dashboard reads and agent dashboard mutations. Existing model, persistence, datasource, and agent-config modules remain framework-agnostic TypeScript with import path updates as needed.
|
**Architecture:** The browser runtime becomes React mounted from `src/main.tsx`. A Bun server at `src/server/index.ts` serves the static React build and exposes JSON API routes for dashboard reads and agent dashboard mutations. Existing model, persistence, datasource, and agent-config modules remain framework-agnostic TypeScript with import path updates as needed.
|
||||||
|
|
||||||
**Tech Stack:** Bun, Vite, React, React DOM, TypeScript, Vitest, Storybook React Vite, Playwright, Drizzle ORM, Bun SQLite, MSW, uPlot, Iconify React.
|
**Tech Stack:** Bun, Vite, React, React DOM, TypeScript, Tailwind CSS v4, shadcn/ui, Vitest, Storybook React Vite, Playwright, Drizzle ORM, Bun SQLite, MSW, uPlot, Iconify React.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -23,13 +23,16 @@
|
||||||
- Create `src/server/routes/agent-dashboard.test.ts`: agent API delegation/auth tests.
|
- Create `src/server/routes/agent-dashboard.test.ts`: agent API delegation/auth tests.
|
||||||
- Create `src/lib/ui/components/*.tsx`: React ports of current reusable Svelte components.
|
- Create `src/lib/ui/components/*.tsx`: React ports of current reusable Svelte components.
|
||||||
- Create `src/lib/ui/components/styles.css`: component CSS migrated from Svelte style blocks.
|
- Create `src/lib/ui/components/styles.css`: component CSS migrated from Svelte style blocks.
|
||||||
|
- Create `components.json`: shadcn/ui configuration for Vite, Radix, Nova, Tailwind v4, and `$lib` aliases.
|
||||||
|
- Create `src/lib/components/ui/*.tsx`: selected shadcn primitives, not the full registry.
|
||||||
|
- Create `src/lib/utils.ts`: `cn()` helper for shadcn and dashboard components.
|
||||||
- Replace `src/lib/ui/components/render.test.ts`: React `renderToString` component tests.
|
- Replace `src/lib/ui/components/render.test.ts`: React `renderToString` component tests.
|
||||||
- Replace `src/lib/ui/stories/*.stories.svelte`: React `.stories.tsx` stories.
|
- Replace `src/lib/ui/stories/*.stories.svelte`: React `.stories.tsx` stories.
|
||||||
- Modify `.storybook/main.ts`: use `@storybook/react-vite` and React story globs.
|
- Modify `.storybook/main.ts`: use `@storybook/react-vite` and React story globs.
|
||||||
- Modify `.storybook/preview.ts`: use React Storybook types and keep MSW setup/global CSS.
|
- Modify `.storybook/preview.ts`: use React Storybook types and keep MSW setup/global CSS.
|
||||||
- Modify `vite.config.ts`: use React plugin, alias `$lib` to `src/lib`, build client, and keep Vitest config.
|
- Modify `vite.config.ts`: use React and Tailwind plugins, alias `$lib` to `src/lib`, build client, and keep Vitest config.
|
||||||
- Modify `tsconfig.json`: remove `.svelte-kit` inheritance, enable JSX, and define path aliases.
|
- Modify `tsconfig.json`: remove `.svelte-kit` inheritance, enable JSX, and define path aliases.
|
||||||
- Modify `package.json`: swap Svelte/SvelteKit dependencies for React tooling and update scripts.
|
- Modify `package.json`: swap Svelte/SvelteKit dependencies for React/Tailwind/shadcn tooling and update scripts.
|
||||||
- Modify `playwright.config.ts`: build React client and Bun server before e2e.
|
- Modify `playwright.config.ts`: build React client and Bun server before e2e.
|
||||||
- Modify `Containerfile`: copy React/Bun build artifacts and keep `bun build/index.js` command.
|
- Modify `Containerfile`: copy React/Bun build artifacts and keep `bun build/index.js` command.
|
||||||
- Modify `README.md`: document React runtime, Bun server, scripts, QA gate, deployment.
|
- Modify `README.md`: document React runtime, Bun server, scripts, QA gate, deployment.
|
||||||
|
|
@ -104,11 +107,12 @@ Update `package.json` scripts and dependencies:
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Install React packages with Bun so `bun.lock` updates:
|
Install React, Tailwind, and shadcn packages with Bun so `bun.lock` updates:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
bun add @iconify/react @vitejs/plugin-react react react-dom
|
bun add @iconify/react @vitejs/plugin-react react react-dom
|
||||||
bun add -d @storybook/react-vite @types/react @types/react-dom
|
bun add class-variance-authority clsx lucide-react radix-ui tailwind-merge tw-animate-css @fontsource-variable/geist
|
||||||
|
bun add -d @storybook/react-vite @tailwindcss/vite @types/react @types/react-dom shadcn tailwindcss
|
||||||
```
|
```
|
||||||
|
|
||||||
Create `index.html`:
|
Create `index.html`:
|
||||||
|
|
@ -181,7 +185,16 @@ createRoot(root).render(
|
||||||
|
|
||||||
Update `tsconfig.json` with React JSX and path aliases.
|
Update `tsconfig.json` with React JSX and path aliases.
|
||||||
|
|
||||||
Update `vite.config.ts` to use `@vitejs/plugin-react` and `$lib` alias.
|
Update `vite.config.ts` to use `@vitejs/plugin-react`, `@tailwindcss/vite`,
|
||||||
|
and the `$lib` alias. Initialize shadcn with:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
bunx --bun shadcn@latest init --template vite --base radix --preset nova --yes --css-variables
|
||||||
|
bunx --bun shadcn@latest add badge card progress separator skeleton alert
|
||||||
|
```
|
||||||
|
|
||||||
|
Do not run `bunx --bun shadcn@latest add --all`; the dashboard should only
|
||||||
|
vendor primitives it actually uses.
|
||||||
|
|
||||||
- [ ] **Step 4: Run checks for the scaffold**
|
- [ ] **Step 4: Run checks for the scaffold**
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,10 @@ state is a React runtime and React component library.
|
||||||
|
|
||||||
Use Vite React for the browser app and a small Bun HTTP server for production
|
Use Vite React for the browser app and a small Bun HTTP server for production
|
||||||
runtime. This keeps the existing Bun SQLite persistence model and avoids adding
|
runtime. This keeps the existing Bun SQLite persistence model and avoids adding
|
||||||
a heavier React framework where the current route/API surface is small.
|
a heavier React framework where the current route/API surface is small. Use
|
||||||
|
Tailwind CSS v4 and shadcn/ui as the reusable primitive layer for React
|
||||||
|
components, but do not vendor the full shadcn registry. Add only primitives
|
||||||
|
that map to the dashboard surface.
|
||||||
|
|
||||||
The migrated app will have these boundaries:
|
The migrated app will have these boundaries:
|
||||||
|
|
||||||
|
|
@ -36,6 +39,10 @@ The migrated app will have these boundaries:
|
||||||
TypeScript business logic with minimal import-path updates.
|
TypeScript business logic with minimal import-path updates.
|
||||||
- `src/lib/ui/components/*.tsx` contains the reusable React component library.
|
- `src/lib/ui/components/*.tsx` contains the reusable React component library.
|
||||||
- `src/lib/ui/stories/*.stories.tsx` contains React Storybook stories.
|
- `src/lib/ui/stories/*.stories.tsx` contains React Storybook stories.
|
||||||
|
- `src/lib/components/ui/*.tsx` contains shadcn/ui primitives used by the
|
||||||
|
dashboard component library.
|
||||||
|
- `components.json` records the shadcn configuration with Vite, Radix, the Nova
|
||||||
|
preset, and `$lib` import aliases.
|
||||||
|
|
||||||
## Component Migration
|
## Component Migration
|
||||||
|
|
||||||
|
|
@ -76,10 +83,12 @@ data-model identifiers, severity attributes, focusable links, reduced-motion
|
||||||
behavior, and screenshot-tested dashboard layout must stay equivalent to the
|
behavior, and screenshot-tested dashboard layout must stay equivalent to the
|
||||||
current Svelte implementation.
|
current Svelte implementation.
|
||||||
|
|
||||||
CSS will remain in `src/lib/ui/tokens.css` and component-specific CSS files.
|
CSS will keep the existing design tokens in `src/lib/ui/tokens.css`.
|
||||||
React components should import their own CSS modules or regular CSS files in a
|
`src/app.css` imports Tailwind, shadcn CSS, font assets, and the existing
|
||||||
consistent local pattern. Shared UI types and `dashboardDocumentToUiDashboard`
|
Dimension Lab token file. shadcn semantic variables must be mapped to the dark
|
||||||
remain framework-agnostic TypeScript.
|
console palette so generated primitives fit the dashboard instead of resetting
|
||||||
|
the app to a light generic theme. Shared UI types and
|
||||||
|
`dashboardDocumentToUiDashboard` remain framework-agnostic TypeScript.
|
||||||
|
|
||||||
## Runtime And API Behavior
|
## Runtime And API Behavior
|
||||||
|
|
||||||
|
|
@ -101,8 +110,11 @@ owned by `src/lib/server/agent-config/index.ts`.
|
||||||
|
|
||||||
`package.json` will move from Svelte/SvelteKit dependencies to React tooling:
|
`package.json` will move from Svelte/SvelteKit dependencies to React tooling:
|
||||||
|
|
||||||
- Runtime dependencies include `@vitejs/plugin-react`, `react`, `react-dom`,
|
- Runtime dependencies include `react`, `react-dom`, `@iconify/react`,
|
||||||
`@iconify/react`, and existing non-Svelte libraries that still apply.
|
selected shadcn primitive dependencies, Tailwind merge helpers, and existing
|
||||||
|
non-Svelte libraries that still apply.
|
||||||
|
- Dev dependencies include `@vitejs/plugin-react`, `@tailwindcss/vite`,
|
||||||
|
`tailwindcss`, and the shadcn package needed by the generated CSS import.
|
||||||
- Storybook moves from `@storybook/sveltekit` and Svelte CSF to
|
- Storybook moves from `@storybook/sveltekit` and Svelte CSF to
|
||||||
`@storybook/react-vite`.
|
`@storybook/react-vite`.
|
||||||
- `svelte.config.js`, `src/app.html`, `src/routes/**`, and `.svelte` files are
|
- `svelte.config.js`, `src/app.html`, `src/routes/**`, and `.svelte` files are
|
||||||
|
|
@ -139,6 +151,8 @@ The migration is complete only when current evidence proves all of these:
|
||||||
|
|
||||||
- There are no `.svelte` app, component, route, or story files left.
|
- There are no `.svelte` app, component, route, or story files left.
|
||||||
- `package.json` has no Svelte, SvelteKit, or Svelte Storybook dependencies.
|
- `package.json` has no Svelte, SvelteKit, or Svelte Storybook dependencies.
|
||||||
|
- shadcn is configured for Vite/Radix with `$lib` aliases and only selected
|
||||||
|
primitives, not the full registry.
|
||||||
- `bun run check` passes.
|
- `bun run check` passes.
|
||||||
- `bun run test:unit` passes.
|
- `bun run test:unit` passes.
|
||||||
- `bun run build` produces the React client build and Bun server entry.
|
- `bun run build` produces the React client build and Bun server entry.
|
||||||
|
|
|
||||||
14
package.json
14
package.json
|
|
@ -5,8 +5,8 @@
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --host 0.0.0.0",
|
"dev": "vite --host 0.0.0.0",
|
||||||
"build": "vite build",
|
"build": "vite build && bun build src/server/index.ts --target bun --outdir build",
|
||||||
"preview": "vite preview --host 0.0.0.0",
|
"preview": "HOST=0.0.0.0 PORT=4173 bun build/index.js",
|
||||||
"storybook": "storybook dev -p 6006 --host 0.0.0.0",
|
"storybook": "storybook dev -p 6006 --host 0.0.0.0",
|
||||||
"build-storybook": "storybook build",
|
"build-storybook": "storybook build",
|
||||||
"check": "tsc --noEmit",
|
"check": "tsc --noEmit",
|
||||||
|
|
@ -18,14 +18,21 @@
|
||||||
"db:check": "drizzle-kit check"
|
"db:check": "drizzle-kit check"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@fontsource-variable/geist": "^5.2.9",
|
||||||
"@iconify/react": "^6.0.2",
|
"@iconify/react": "^6.0.2",
|
||||||
"@iconify/svelte": "^5.2.2",
|
"@iconify/svelte": "^5.2.2",
|
||||||
"@sinclair/typebox": "^0.34.49",
|
"@sinclair/typebox": "^0.34.49",
|
||||||
"ajv": "^8.20.0",
|
"ajv": "^8.20.0",
|
||||||
"ajv-formats": "^3.0.1",
|
"ajv-formats": "^3.0.1",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
"drizzle-orm": "^0.45.2",
|
"drizzle-orm": "^0.45.2",
|
||||||
|
"lucide-react": "^1.21.0",
|
||||||
|
"radix-ui": "^1.6.0",
|
||||||
"react": "^19.2.7",
|
"react": "^19.2.7",
|
||||||
"react-dom": "^19.2.7",
|
"react-dom": "^19.2.7",
|
||||||
|
"tailwind-merge": "^3.6.0",
|
||||||
|
"tw-animate-css": "^1.4.0",
|
||||||
"uplot": "^1.6.32"
|
"uplot": "^1.6.32"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
@ -39,6 +46,7 @@
|
||||||
"@sveltejs/adapter-node": "^5.5.4",
|
"@sveltejs/adapter-node": "^5.5.4",
|
||||||
"@sveltejs/kit": "^2.65.2",
|
"@sveltejs/kit": "^2.65.2",
|
||||||
"@sveltejs/vite-plugin-svelte": "^7.1.2",
|
"@sveltejs/vite-plugin-svelte": "^7.1.2",
|
||||||
|
"@tailwindcss/vite": "^4.3.1",
|
||||||
"@types/bun": "^1.3.14",
|
"@types/bun": "^1.3.14",
|
||||||
"@types/node": "^25.9.3",
|
"@types/node": "^25.9.3",
|
||||||
"@types/react": "^19.2.17",
|
"@types/react": "^19.2.17",
|
||||||
|
|
@ -46,9 +54,11 @@
|
||||||
"@vitejs/plugin-react": "^6.0.2",
|
"@vitejs/plugin-react": "^6.0.2",
|
||||||
"drizzle-kit": "^0.31.10",
|
"drizzle-kit": "^0.31.10",
|
||||||
"msw": "^2.14.6",
|
"msw": "^2.14.6",
|
||||||
|
"shadcn": "^4.11.0",
|
||||||
"storybook": "^10.4.6",
|
"storybook": "^10.4.6",
|
||||||
"svelte": "^5.56.3",
|
"svelte": "^5.56.3",
|
||||||
"svelte-check": "^4.6.0",
|
"svelte-check": "^4.6.0",
|
||||||
|
"tailwindcss": "^4.3.1",
|
||||||
"typescript": "^6.0.3",
|
"typescript": "^6.0.3",
|
||||||
"vite": "^8.0.16",
|
"vite": "^8.0.16",
|
||||||
"vitest": "^4.1.9"
|
"vitest": "^4.1.9"
|
||||||
|
|
|
||||||
59
src/App.tsx
59
src/App.tsx
|
|
@ -1,5 +1,13 @@
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
import type { DashboardRuntimeState } from "$lib/server/dashboard";
|
import type { DashboardRuntimeState } from "$lib/server/dashboard";
|
||||||
|
|
||||||
|
const loadingDashboardState: DashboardRuntimeState = {
|
||||||
|
state: "loading",
|
||||||
|
title: "Loading Dashboard",
|
||||||
|
subtitle: "Fetching active model",
|
||||||
|
message: "Waiting for the active dashboard document.",
|
||||||
|
};
|
||||||
|
|
||||||
export function AppStateView({
|
export function AppStateView({
|
||||||
dashboard,
|
dashboard,
|
||||||
}: {
|
}: {
|
||||||
|
|
@ -19,14 +27,45 @@ export function AppStateView({
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
return (
|
const [dashboard, setDashboard] =
|
||||||
<AppStateView
|
useState<DashboardRuntimeState>(loadingDashboardState);
|
||||||
dashboard={{
|
|
||||||
state: "loading",
|
useEffect(() => {
|
||||||
title: "Loading Dashboard",
|
let cancelled = false;
|
||||||
subtitle: "Fetching active model",
|
let refreshTimer: number | undefined;
|
||||||
message: "Waiting for the active dashboard document.",
|
|
||||||
}}
|
async function loadDashboard() {
|
||||||
/>
|
const response = await fetch("/api/dashboard");
|
||||||
);
|
const nextDashboard = (await response.json()) as DashboardRuntimeState;
|
||||||
|
|
||||||
|
if (cancelled) return;
|
||||||
|
setDashboard(nextDashboard);
|
||||||
|
|
||||||
|
if (refreshTimer) {
|
||||||
|
window.clearInterval(refreshTimer);
|
||||||
|
refreshTimer = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const refreshIntervalSeconds =
|
||||||
|
nextDashboard.state === "ready"
|
||||||
|
? nextDashboard.document.metadata.refreshIntervalSeconds
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
if (refreshIntervalSeconds) {
|
||||||
|
refreshTimer = window.setInterval(
|
||||||
|
() => void loadDashboard(),
|
||||||
|
refreshIntervalSeconds * 1000,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void loadDashboard();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
if (refreshTimer) window.clearInterval(refreshTimer);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return <AppStateView dashboard={dashboard} />;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
144
src/app.css
144
src/app.css
|
|
@ -1 +1,145 @@
|
||||||
|
@import "tailwindcss";
|
||||||
@import "./lib/ui/tokens.css";
|
@import "./lib/ui/tokens.css";
|
||||||
|
@import "tw-animate-css";
|
||||||
|
@import "shadcn/tailwind.css";
|
||||||
|
@import "@fontsource-variable/geist";
|
||||||
|
|
||||||
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
|
@theme inline {
|
||||||
|
--font-heading: var(--font-sans);
|
||||||
|
--font-sans: 'Geist Variable', sans-serif;
|
||||||
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
|
--color-sidebar-accent: var(--sidebar-accent);
|
||||||
|
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||||
|
--color-sidebar-primary: var(--sidebar-primary);
|
||||||
|
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||||
|
--color-sidebar: var(--sidebar);
|
||||||
|
--color-chart-5: var(--chart-5);
|
||||||
|
--color-chart-4: var(--chart-4);
|
||||||
|
--color-chart-3: var(--chart-3);
|
||||||
|
--color-chart-2: var(--chart-2);
|
||||||
|
--color-chart-1: var(--chart-1);
|
||||||
|
--color-ring: var(--ring);
|
||||||
|
--color-input: var(--input);
|
||||||
|
--color-border: var(--border);
|
||||||
|
--color-destructive: var(--destructive);
|
||||||
|
--color-accent-foreground: var(--accent-foreground);
|
||||||
|
--color-accent: var(--accent);
|
||||||
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
|
--color-muted: var(--muted);
|
||||||
|
--color-secondary-foreground: var(--secondary-foreground);
|
||||||
|
--color-secondary: var(--secondary);
|
||||||
|
--color-primary-foreground: var(--primary-foreground);
|
||||||
|
--color-primary: var(--primary);
|
||||||
|
--color-popover-foreground: var(--popover-foreground);
|
||||||
|
--color-popover: var(--popover);
|
||||||
|
--color-card-foreground: var(--card-foreground);
|
||||||
|
--color-card: var(--card);
|
||||||
|
--color-foreground: var(--foreground);
|
||||||
|
--color-background: var(--background);
|
||||||
|
--radius-sm: calc(var(--radius) * 0.6);
|
||||||
|
--radius-md: calc(var(--radius) * 0.8);
|
||||||
|
--radius-lg: var(--radius);
|
||||||
|
--radius-xl: calc(var(--radius) * 1.4);
|
||||||
|
--radius-2xl: calc(var(--radius) * 1.8);
|
||||||
|
--radius-3xl: calc(var(--radius) * 2.2);
|
||||||
|
--radius-4xl: calc(var(--radius) * 2.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root,
|
||||||
|
.dark {
|
||||||
|
--background: var(--ui-color-canvas);
|
||||||
|
--foreground: var(--ui-color-text);
|
||||||
|
--card: var(--ui-color-surface);
|
||||||
|
--card-foreground: var(--ui-color-text);
|
||||||
|
--popover: var(--ui-color-surface-raised);
|
||||||
|
--popover-foreground: var(--ui-color-text);
|
||||||
|
--primary: var(--ui-color-accent);
|
||||||
|
--primary-foreground: var(--ui-color-canvas);
|
||||||
|
--secondary: var(--ui-color-surface-raised);
|
||||||
|
--secondary-foreground: var(--ui-color-text);
|
||||||
|
--muted: var(--ui-color-surface-raised);
|
||||||
|
--muted-foreground: var(--ui-color-muted);
|
||||||
|
--accent: var(--ui-color-accent);
|
||||||
|
--accent-foreground: var(--ui-color-canvas);
|
||||||
|
--destructive: var(--ui-color-danger);
|
||||||
|
--border: rgba(244, 244, 244, 0.16);
|
||||||
|
--input: rgba(244, 244, 244, 0.18);
|
||||||
|
--ring: var(--ui-color-accent);
|
||||||
|
--chart-1: var(--ui-color-accent);
|
||||||
|
--chart-2: var(--ui-color-ok);
|
||||||
|
--chart-3: var(--ui-color-warning);
|
||||||
|
--chart-4: var(--ui-color-danger);
|
||||||
|
--chart-5: var(--ui-color-stale);
|
||||||
|
--radius: 0.5rem;
|
||||||
|
--sidebar: var(--ui-color-surface);
|
||||||
|
--sidebar-foreground: var(--ui-color-text);
|
||||||
|
--sidebar-primary: var(--ui-color-accent);
|
||||||
|
--sidebar-primary-foreground: var(--ui-color-canvas);
|
||||||
|
--sidebar-accent: var(--ui-color-surface-raised);
|
||||||
|
--sidebar-accent-foreground: var(--ui-color-text);
|
||||||
|
--sidebar-border: rgba(244, 244, 244, 0.16);
|
||||||
|
--sidebar-ring: var(--ui-color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border outline-ring/50;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
|
html {
|
||||||
|
@apply font-sans;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
background: var(--ui-color-canvas);
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
min-width: 320px;
|
||||||
|
min-height: 100vh;
|
||||||
|
margin: 0;
|
||||||
|
background:
|
||||||
|
linear-gradient(rgba(255, 255, 255, 0.035) 1px, transparent 1px),
|
||||||
|
linear-gradient(90deg, rgba(255, 255, 255, 0.035) 1px, transparent 1px),
|
||||||
|
var(--ui-color-canvas);
|
||||||
|
background-size: 48px 48px, 48px 48px, auto;
|
||||||
|
color: var(--ui-color-text);
|
||||||
|
font-family: var(--ui-font-mono);
|
||||||
|
text-rendering: geometricPrecision;
|
||||||
|
}
|
||||||
|
|
||||||
|
button,
|
||||||
|
input,
|
||||||
|
textarea,
|
||||||
|
select {
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:focus-visible,
|
||||||
|
a:focus-visible,
|
||||||
|
[tabindex]:focus-visible {
|
||||||
|
outline: 0;
|
||||||
|
box-shadow: var(--ui-focus-ring);
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
animation-duration: 0.01ms !important;
|
||||||
|
animation-iteration-count: 1 !important;
|
||||||
|
scroll-behavior: auto !important;
|
||||||
|
transition-duration: 0.01ms !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
76
src/lib/components/ui/alert.tsx
Normal file
76
src/lib/components/ui/alert.tsx
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "$lib/utils"
|
||||||
|
|
||||||
|
const alertVariants = cva(
|
||||||
|
"group/alert relative grid w-full gap-0.5 rounded-lg border px-2.5 py-2 text-left text-sm has-data-[slot=alert-action]:relative has-data-[slot=alert-action]:pr-18 has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2 *:[svg]:row-span-2 *:[svg]:translate-y-0.5 *:[svg]:text-current *:[svg:not([class*='size-'])]:size-4",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-card text-card-foreground",
|
||||||
|
destructive:
|
||||||
|
"bg-card text-destructive *:data-[slot=alert-description]:text-destructive/90 *:[svg]:text-current",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function Alert({
|
||||||
|
className,
|
||||||
|
variant,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="alert"
|
||||||
|
role="alert"
|
||||||
|
className={cn(alertVariants({ variant }), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="alert-title"
|
||||||
|
className={cn(
|
||||||
|
"font-medium group-has-[>svg]/alert:col-start-2 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDescription({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="alert-description"
|
||||||
|
className={cn(
|
||||||
|
"text-sm text-balance text-muted-foreground md:text-pretty [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground [&_p:not(:last-child)]:mb-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="alert-action"
|
||||||
|
className={cn("absolute top-2 right-2", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Alert, AlertTitle, AlertDescription, AlertAction }
|
||||||
49
src/lib/components/ui/badge.tsx
Normal file
49
src/lib/components/ui/badge.tsx
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
import { Slot } from "radix-ui"
|
||||||
|
|
||||||
|
import { cn } from "$lib/utils"
|
||||||
|
|
||||||
|
const badgeVariants = cva(
|
||||||
|
"group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
|
||||||
|
secondary:
|
||||||
|
"bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
|
||||||
|
destructive:
|
||||||
|
"bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20",
|
||||||
|
outline:
|
||||||
|
"border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
|
||||||
|
ghost:
|
||||||
|
"hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
|
||||||
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function Badge({
|
||||||
|
className,
|
||||||
|
variant = "default",
|
||||||
|
asChild = false,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"span"> &
|
||||||
|
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||||
|
const Comp = asChild ? Slot.Root : "span"
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
data-slot="badge"
|
||||||
|
data-variant={variant}
|
||||||
|
className={cn(badgeVariants({ variant }), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Badge, badgeVariants }
|
||||||
67
src/lib/components/ui/button.tsx
Normal file
67
src/lib/components/ui/button.tsx
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
import { Slot } from "radix-ui"
|
||||||
|
|
||||||
|
import { cn } from "$lib/utils"
|
||||||
|
|
||||||
|
const buttonVariants = cva(
|
||||||
|
"group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-primary text-primary-foreground hover:bg-primary/80",
|
||||||
|
outline:
|
||||||
|
"border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
|
||||||
|
secondary:
|
||||||
|
"bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
|
||||||
|
ghost:
|
||||||
|
"hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
|
||||||
|
destructive:
|
||||||
|
"bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
|
||||||
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default:
|
||||||
|
"h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
||||||
|
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
|
||||||
|
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
|
||||||
|
lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
||||||
|
icon: "size-8",
|
||||||
|
"icon-xs":
|
||||||
|
"size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
|
||||||
|
"icon-sm":
|
||||||
|
"size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
|
||||||
|
"icon-lg": "size-9",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function Button({
|
||||||
|
className,
|
||||||
|
variant = "default",
|
||||||
|
size = "default",
|
||||||
|
asChild = false,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"button"> &
|
||||||
|
VariantProps<typeof buttonVariants> & {
|
||||||
|
asChild?: boolean
|
||||||
|
}) {
|
||||||
|
const Comp = asChild ? Slot.Root : "button"
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
data-slot="button"
|
||||||
|
data-variant={variant}
|
||||||
|
data-size={size}
|
||||||
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Button, buttonVariants }
|
||||||
103
src/lib/components/ui/card.tsx
Normal file
103
src/lib/components/ui/card.tsx
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "$lib/utils"
|
||||||
|
|
||||||
|
function Card({
|
||||||
|
className,
|
||||||
|
size = "default",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div"> & { size?: "default" | "sm" }) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card"
|
||||||
|
data-size={size}
|
||||||
|
className={cn(
|
||||||
|
"group/card flex flex-col gap-(--card-spacing) overflow-hidden rounded-xl bg-card py-(--card-spacing) text-sm text-card-foreground ring-1 ring-foreground/10 [--card-spacing:--spacing(4)] has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:[--card-spacing:--spacing(3)] data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-header"
|
||||||
|
className={cn(
|
||||||
|
"group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-(--card-spacing) has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-(--card-spacing)",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-title"
|
||||||
|
className={cn(
|
||||||
|
"font-heading text-base leading-snug font-medium group-data-[size=sm]/card:text-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-description"
|
||||||
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-action"
|
||||||
|
className={cn(
|
||||||
|
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-content"
|
||||||
|
className={cn("px-(--card-spacing)", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-footer"
|
||||||
|
className={cn(
|
||||||
|
"flex items-center rounded-b-xl border-t bg-muted/50 p-(--card-spacing)",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Card,
|
||||||
|
CardHeader,
|
||||||
|
CardFooter,
|
||||||
|
CardTitle,
|
||||||
|
CardAction,
|
||||||
|
CardDescription,
|
||||||
|
CardContent,
|
||||||
|
}
|
||||||
29
src/lib/components/ui/progress.tsx
Normal file
29
src/lib/components/ui/progress.tsx
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import { Progress as ProgressPrimitive } from "radix-ui"
|
||||||
|
|
||||||
|
import { cn } from "$lib/utils"
|
||||||
|
|
||||||
|
function Progress({
|
||||||
|
className,
|
||||||
|
value,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ProgressPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<ProgressPrimitive.Root
|
||||||
|
data-slot="progress"
|
||||||
|
className={cn(
|
||||||
|
"relative flex h-1 w-full items-center overflow-x-hidden rounded-full bg-muted",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ProgressPrimitive.Indicator
|
||||||
|
data-slot="progress-indicator"
|
||||||
|
className="size-full flex-1 bg-primary transition-all"
|
||||||
|
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||||
|
/>
|
||||||
|
</ProgressPrimitive.Root>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Progress }
|
||||||
28
src/lib/components/ui/separator.tsx
Normal file
28
src/lib/components/ui/separator.tsx
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import { Separator as SeparatorPrimitive } from "radix-ui"
|
||||||
|
|
||||||
|
import { cn } from "$lib/utils"
|
||||||
|
|
||||||
|
function Separator({
|
||||||
|
className,
|
||||||
|
orientation = "horizontal",
|
||||||
|
decorative = true,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<SeparatorPrimitive.Root
|
||||||
|
data-slot="separator"
|
||||||
|
decorative={decorative}
|
||||||
|
orientation={orientation}
|
||||||
|
className={cn(
|
||||||
|
"shrink-0 bg-border data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Separator }
|
||||||
13
src/lib/components/ui/skeleton.tsx
Normal file
13
src/lib/components/ui/skeleton.tsx
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { cn } from "$lib/utils"
|
||||||
|
|
||||||
|
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="skeleton"
|
||||||
|
className={cn("animate-pulse rounded-md bg-muted", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Skeleton }
|
||||||
6
src/lib/utils.ts
Normal file
6
src/lib/utils.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { clsx, type ClassValue } from "clsx"
|
||||||
|
import { twMerge } from "tailwind-merge"
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs))
|
||||||
|
}
|
||||||
84
src/server/index.ts
Normal file
84
src/server/index.ts
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
import { extname, normalize } from "node:path";
|
||||||
|
import { handleAgentDashboardRoute } from "./routes/agent-dashboard";
|
||||||
|
import { handleDashboardRoute } from "./routes/dashboard";
|
||||||
|
|
||||||
|
const host = process.env.HOST || "0.0.0.0";
|
||||||
|
const port = Number(process.env.PORT || 3000);
|
||||||
|
const distRoot = `${process.cwd()}/dist`;
|
||||||
|
|
||||||
|
const contentTypes = new Map([
|
||||||
|
[".css", "text/css; charset=utf-8"],
|
||||||
|
[".html", "text/html; charset=utf-8"],
|
||||||
|
[".js", "text/javascript; charset=utf-8"],
|
||||||
|
[".json", "application/json; charset=utf-8"],
|
||||||
|
[".svg", "image/svg+xml"],
|
||||||
|
[".wasm", "application/wasm"],
|
||||||
|
]);
|
||||||
|
|
||||||
|
export async function handleRequest(request: Request): Promise<Response> {
|
||||||
|
const url = new URL(request.url);
|
||||||
|
|
||||||
|
if (url.pathname === "/api/dashboard") {
|
||||||
|
if (request.method !== "GET") return methodNotAllowed(["GET"]);
|
||||||
|
return handleDashboardRoute();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.pathname === "/api/agent/dashboard") {
|
||||||
|
if (request.method !== "POST") return methodNotAllowed(["POST"]);
|
||||||
|
return handleAgentDashboardRoute(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.pathname.startsWith("/api/")) {
|
||||||
|
return Response.json({ ok: false, message: "Not found" }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
return serveStaticAsset(url.pathname);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function serveStaticAsset(pathname: string): Promise<Response> {
|
||||||
|
const safePath = normalize(pathname).replace(/^(\.\.(\/|\\|$))+/, "");
|
||||||
|
const assetPath = safePath === "/" || safePath === "." ? "/index.html" : safePath;
|
||||||
|
const file = Bun.file(`${distRoot}${assetPath}`);
|
||||||
|
|
||||||
|
if (await file.exists()) {
|
||||||
|
return new Response(file, {
|
||||||
|
headers: contentTypeHeaders(assetPath),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const fallback = Bun.file(`${distRoot}/index.html`);
|
||||||
|
if (await fallback.exists()) {
|
||||||
|
return new Response(fallback, {
|
||||||
|
headers: contentTypeHeaders(".html"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response("Build output not found", { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
function methodNotAllowed(allowedMethods: string[]): Response {
|
||||||
|
return Response.json(
|
||||||
|
{ ok: false, message: "Method not allowed" },
|
||||||
|
{
|
||||||
|
status: 405,
|
||||||
|
headers: {
|
||||||
|
Allow: allowedMethods.join(", "),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function contentTypeHeaders(pathname: string): HeadersInit {
|
||||||
|
const contentType = contentTypes.get(extname(pathname));
|
||||||
|
return contentType ? { "Content-Type": contentType } : {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (import.meta.main) {
|
||||||
|
Bun.serve({
|
||||||
|
hostname: host,
|
||||||
|
port,
|
||||||
|
fetch: handleRequest,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.info(`Dimension Lab website listening on http://${host}:${port}`);
|
||||||
|
}
|
||||||
12
src/server/routes/agent-dashboard.test.ts
Normal file
12
src/server/routes/agent-dashboard.test.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { describe, expect, test } from "vitest";
|
||||||
|
import { handleAgentDashboardRoute } from "./agent-dashboard";
|
||||||
|
|
||||||
|
describe("agent dashboard API route", () => {
|
||||||
|
test("delegates unauthorized requests to the existing agent handler", async () => {
|
||||||
|
const response = await handleAgentDashboardRoute(
|
||||||
|
new Request("http://localhost/api/agent/dashboard", { method: "POST" }),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response.status).toBe(401);
|
||||||
|
});
|
||||||
|
});
|
||||||
5
src/server/routes/agent-dashboard.ts
Normal file
5
src/server/routes/agent-dashboard.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { handleAgentDashboardRequest } from "$lib/server/agent-config";
|
||||||
|
|
||||||
|
export function handleAgentDashboardRoute(request: Request): Promise<Response> {
|
||||||
|
return handleAgentDashboardRequest(request);
|
||||||
|
}
|
||||||
19
src/server/routes/dashboard.test.ts
Normal file
19
src/server/routes/dashboard.test.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { describe, expect, test } from "vitest";
|
||||||
|
import { dimensionLabDashboardFixture } from "$lib/model/fixtures/dimensionlab";
|
||||||
|
import { loadDashboardResponse } from "./dashboard";
|
||||||
|
|
||||||
|
describe("dashboard API route", () => {
|
||||||
|
test("returns ready dashboard runtime state from the existing model loader", async () => {
|
||||||
|
const response = await loadDashboardResponse({
|
||||||
|
disableLiveDatasources: true,
|
||||||
|
refreshSeedDocument: true,
|
||||||
|
seedIfEmpty: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.state).toBe("ready");
|
||||||
|
if (response.state !== "ready") throw new Error("expected ready dashboard");
|
||||||
|
expect(response.document.metadata.title).toBe(
|
||||||
|
dimensionLabDashboardFixture.metadata.title,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
41
src/server/routes/dashboard.ts
Normal file
41
src/server/routes/dashboard.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
import {
|
||||||
|
loadDashboardRuntime,
|
||||||
|
type DashboardRuntimeOptions,
|
||||||
|
type DashboardRuntimeState,
|
||||||
|
} from "$lib/server/dashboard";
|
||||||
|
import { resolveDashboardDatasources } from "$lib/server/datasources";
|
||||||
|
|
||||||
|
export interface LoadDashboardResponseOptions
|
||||||
|
extends Pick<
|
||||||
|
DashboardRuntimeOptions,
|
||||||
|
"refreshSeedDocument" | "seedIfEmpty" | "seedDocument"
|
||||||
|
> {
|
||||||
|
disableLiveDatasources?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadDashboardResponse(
|
||||||
|
options: LoadDashboardResponseOptions = {},
|
||||||
|
): Promise<DashboardRuntimeState> {
|
||||||
|
const dashboard = loadDashboardRuntime(undefined, {
|
||||||
|
refreshSeedDocument: options.refreshSeedDocument ?? true,
|
||||||
|
seedIfEmpty: options.seedIfEmpty ?? true,
|
||||||
|
seedDocument: options.seedDocument,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (dashboard.state !== "ready") {
|
||||||
|
return dashboard;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.disableLiveDatasources || process.env.DISABLE_LIVE_DATASOURCES === "1") {
|
||||||
|
return dashboard;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...dashboard,
|
||||||
|
document: await resolveDashboardDatasources(dashboard.document),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handleDashboardRoute(): Promise<Response> {
|
||||||
|
return Response.json(await loadDashboardResponse());
|
||||||
|
}
|
||||||
|
|
@ -1,16 +1,17 @@
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
import react from "@vitejs/plugin-react";
|
import react from "@vitejs/plugin-react";
|
||||||
import { svelte } from "@sveltejs/vite-plugin-svelte";
|
import { svelte } from "@sveltejs/vite-plugin-svelte";
|
||||||
import { defineConfig, configDefaults } from "vitest/config";
|
import { defineConfig, configDefaults } from "vitest/config";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react(), svelte()],
|
plugins: [react(), tailwindcss(), svelte()],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
$lib: fileURLToPath(new URL("./src/lib", import.meta.url)),
|
$lib: fileURLToPath(new URL("./src/lib", import.meta.url)),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
test: {
|
test: {
|
||||||
exclude: [...configDefaults.exclude, "tests/e2e/**"],
|
exclude: [...configDefaults.exclude, "tests/e2e/**", "src/routes/**"],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue