From c9082cba81b30663b99d5ba02ab8cd2257f3d475 Mon Sep 17 00:00:00 2001 From: Hector Ros Date: Tue, 20 Jan 2026 01:20:34 +0100 Subject: [PATCH] Initial frontend implementation - React dashboard with Tailwind CSS v4 - Session-based authentication (Lucia patterns) - API client with axios - Project, Task, and Agent views - Bun.serve() with HMR and API proxy - Docker support Co-Authored-By: Claude Sonnet 4.5 (1M context) --- .../use-bun-instead-of-node-vite-npm-pnpm.mdc | 1 + .dockerignore | 9 + .gitignore | 34 +++ CLAUDE.md | 111 +++++++++ Dockerfile | 18 ++ README.md | 15 ++ bun.lock | 224 ++++++++++++++++++ index.ts | 1 + package.json | 29 +++ postcss.config.mjs | 5 + public/index.html | 12 + server.ts | 73 ++++++ src/App.tsx | 29 +++ src/api/client.ts | 29 +++ src/components/AgentStatus.tsx | 108 +++++++++ src/components/Layout.tsx | 45 ++++ src/components/ProjectList.tsx | 77 ++++++ src/components/TaskList.tsx | 125 ++++++++++ src/index.css | 1 + src/lib/auth.tsx | 80 +++++++ src/main.tsx | 15 ++ src/pages/Dashboard.tsx | 100 ++++++++ src/pages/Login.tsx | 87 +++++++ src/pages/Register.tsx | 126 ++++++++++ src/types/index.ts | 91 +++++++ tsconfig.json | 29 +++ 26 files changed, 1474 insertions(+) create mode 120000 .cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc create mode 100644 .dockerignore create mode 100644 .gitignore create mode 100644 CLAUDE.md create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 bun.lock create mode 100644 index.ts create mode 100644 package.json create mode 100644 postcss.config.mjs create mode 100644 public/index.html create mode 100644 server.ts create mode 100644 src/App.tsx create mode 100644 src/api/client.ts create mode 100644 src/components/AgentStatus.tsx create mode 100644 src/components/Layout.tsx create mode 100644 src/components/ProjectList.tsx create mode 100644 src/components/TaskList.tsx create mode 100644 src/index.css create mode 100644 src/lib/auth.tsx create mode 100644 src/main.tsx create mode 100644 src/pages/Dashboard.tsx create mode 100644 src/pages/Login.tsx create mode 100644 src/pages/Register.tsx create mode 100644 src/types/index.ts create mode 100644 tsconfig.json diff --git a/.cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc b/.cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc new file mode 120000 index 0000000..6100270 --- /dev/null +++ b/.cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc @@ -0,0 +1 @@ +../../CLAUDE.md \ No newline at end of file diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..d2e1a66 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,9 @@ +node_modules +.git +.gitignore +*.md +.env +.env.local +dist +.cache +*.log diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a14702c --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +# dependencies (bun install) +node_modules + +# output +out +dist +*.tgz + +# code coverage +coverage +*.lcov + +# logs +logs +_.log +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# caches +.eslintcache +.cache +*.tsbuildinfo + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..ebda995 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,111 @@ +--- +description: Use Bun instead of Node.js, npm, pnpm, or vite. +globs: "*.ts, *.tsx, *.html, *.css, *.js, *.jsx, package.json" +alwaysApply: false +--- + +Default to using Bun instead of Node.js. + +- Use `bun ` instead of `node ` or `ts-node ` +- Use `bun test` instead of `jest` or `vitest` +- Use `bun build ` instead of `webpack` or `esbuild` +- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install` +- Use `bun run + + +``` + +With the following `frontend.tsx`: + +```tsx#frontend.tsx +import React from "react"; +import { createRoot } from "react-dom/client"; + +// import .css files directly and it works +import './index.css'; + +const root = createRoot(document.body); + +export default function Frontend() { + return

Hello, world!

; +} + +root.render(); +``` + +Then, run index.ts + +```sh +bun --hot ./index.ts +``` + +For more information, read the Bun API docs in `node_modules/bun-types/docs/**.mdx`. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e5356df --- /dev/null +++ b/Dockerfile @@ -0,0 +1,18 @@ +FROM oven/bun:1.3.6-alpine + +WORKDIR /app + +# Copy package files +COPY package.json bun.lockb ./ + +# Install dependencies +RUN bun install --frozen-lockfile --production + +# Copy source code +COPY . . + +# Expose port +EXPOSE 3001 + +# Start server +CMD ["bun", "run", "start"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..5dad4ec --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# frontend + +To install dependencies: + +```bash +bun install +``` + +To run: + +```bash +bun run index.ts +``` + +This project was created using `bun init` in bun v1.3.6. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime. diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..3151aba --- /dev/null +++ b/bun.lock @@ -0,0 +1,224 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "frontend", + "dependencies": { + "@tailwindcss/postcss": "^4.1.18", + "axios": "^1.13.2", + "react": "^19.2.3", + "react-dom": "^19.2.3", + "react-router-dom": "^7.12.0", + "tailwindcss": "^4.1.18", + }, + "devDependencies": { + "@types/bun": "latest", + "@types/react": "^19.2.8", + "@types/react-dom": "^19.2.3", + "autoprefixer": "^10.4.23", + "postcss": "^8.5.6", + }, + "peerDependencies": { + "typescript": "^5", + }, + }, + }, + "packages": { + "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@tailwindcss/node": ["@tailwindcss/node@4.1.18", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.1.18" } }, "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ=="], + + "@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.18", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.18", "@tailwindcss/oxide-darwin-arm64": "4.1.18", "@tailwindcss/oxide-darwin-x64": "4.1.18", "@tailwindcss/oxide-freebsd-x64": "4.1.18", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", "@tailwindcss/oxide-linux-x64-musl": "4.1.18", "@tailwindcss/oxide-wasm32-wasi": "4.1.18", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" } }, "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A=="], + + "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.18", "", { "os": "android", "cpu": "arm64" }, "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q=="], + + "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.18", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A=="], + + "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.18", "", { "os": "darwin", "cpu": "x64" }, "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw=="], + + "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.18", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA=="], + + "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18", "", { "os": "linux", "cpu": "arm" }, "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA=="], + + "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.18", "", { "os": "linux", "cpu": "arm64" }, "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw=="], + + "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.18", "", { "os": "linux", "cpu": "arm64" }, "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg=="], + + "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.18", "", { "os": "linux", "cpu": "x64" }, "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g=="], + + "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.18", "", { "os": "linux", "cpu": "x64" }, "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ=="], + + "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.18", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.0", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.4.0" }, "cpu": "none" }, "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA=="], + + "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.18", "", { "os": "win32", "cpu": "arm64" }, "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA=="], + + "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.18", "", { "os": "win32", "cpu": "x64" }, "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q=="], + + "@tailwindcss/postcss": ["@tailwindcss/postcss@4.1.18", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.1.18", "@tailwindcss/oxide": "4.1.18", "postcss": "^8.4.41", "tailwindcss": "4.1.18" } }, "sha512-Ce0GFnzAOuPyfV5SxjXGn0CubwGcuDB0zcdaPuCSzAa/2vII24JTkH+I6jcbXLb1ctjZMZZI6OjDaLPJQL1S0g=="], + + "@types/bun": ["@types/bun@1.3.6", "", { "dependencies": { "bun-types": "1.3.6" } }, "sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA=="], + + "@types/node": ["@types/node@25.0.9", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-/rpCXHlCWeqClNBwUhDcusJxXYDjZTyE8v5oTO7WbL8eij2nKhUeU89/6xgjU7N4/Vh3He0BtyhJdQbDyhiXAw=="], + + "@types/react": ["@types/react@19.2.8", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg=="], + + "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], + + "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], + + "autoprefixer": ["autoprefixer@10.4.23", "", { "dependencies": { "browserslist": "^4.28.1", "caniuse-lite": "^1.0.30001760", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA=="], + + "axios": ["axios@1.13.2", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA=="], + + "baseline-browser-mapping": ["baseline-browser-mapping@2.9.15", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-kX8h7K2srmDyYnXRIppo4AH/wYgzWVCs+eKr3RusRSQ5PvRYoEFmR/I0PbdTjKFAoKqp5+kbxnNTFO9jOfSVJg=="], + + "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], + + "bun-types": ["bun-types@1.3.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="], + + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + + "caniuse-lite": ["caniuse-lite@1.0.30001765", "", {}, "sha512-LWcNtSyZrakjECqmpP4qdg0MMGdN368D7X8XvvAqOcqMv0RxnlqVKZl2V6/mBR68oYMxOZPLw/gO7DuisMHUvQ=="], + + "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], + + "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], + + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + + "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], + + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + + "electron-to-chromium": ["electron-to-chromium@1.5.267", "", {}, "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw=="], + + "enhanced-resolve": ["enhanced-resolve@5.18.4", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q=="], + + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + + "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="], + + "form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="], + + "fraction.js": ["fraction.js@5.3.4", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], + + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + + "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], + + "lightningcss": ["lightningcss@1.30.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.30.2", "lightningcss-darwin-arm64": "1.30.2", "lightningcss-darwin-x64": "1.30.2", "lightningcss-freebsd-x64": "1.30.2", "lightningcss-linux-arm-gnueabihf": "1.30.2", "lightningcss-linux-arm64-gnu": "1.30.2", "lightningcss-linux-arm64-musl": "1.30.2", "lightningcss-linux-x64-gnu": "1.30.2", "lightningcss-linux-x64-musl": "1.30.2", "lightningcss-win32-arm64-msvc": "1.30.2", "lightningcss-win32-x64-msvc": "1.30.2" } }, "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ=="], + + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.30.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A=="], + + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA=="], + + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ=="], + + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA=="], + + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.2", "", { "os": "linux", "cpu": "arm" }, "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA=="], + + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A=="], + + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA=="], + + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w=="], + + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA=="], + + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ=="], + + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw=="], + + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + + "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + + "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + + "node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], + + "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="], + + "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], + + "react": ["react@19.2.3", "", {}, "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA=="], + + "react-dom": ["react-dom@19.2.3", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.3" } }, "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg=="], + + "react-router": ["react-router@7.12.0", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-kTPDYPFzDVGIIGNLS5VJykK0HfHLY5MF3b+xj0/tTyNYL1gF1qs7u67Z9jEhQk2sQ98SUaHxlG31g1JtF7IfVw=="], + + "react-router-dom": ["react-router-dom@7.12.0", "", { "dependencies": { "react-router": "7.12.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-pfO9fiBcpEfX4Tx+iTYKDtPbrSLLCbwJ5EqP+SPYQu1VYCXdy79GSj0wttR0U4cikVdlImZuEZ/9ZNCgoaxwBA=="], + + "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + + "set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="], + + "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="], + + "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="], + + "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], + + "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + } +} diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..f67b2c6 --- /dev/null +++ b/index.ts @@ -0,0 +1 @@ +console.log("Hello via Bun!"); \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..a419798 --- /dev/null +++ b/package.json @@ -0,0 +1,29 @@ +{ + "name": "frontend", + "module": "index.ts", + "type": "module", + "private": true, + "scripts": { + "dev": "bun --hot server.ts", + "start": "bun server.ts", + "build": "bun build public/index.html --outdir ./dist --target browser" + }, + "devDependencies": { + "@types/bun": "latest", + "@types/react": "^19.2.8", + "@types/react-dom": "^19.2.3", + "autoprefixer": "^10.4.23", + "postcss": "^8.5.6" + }, + "peerDependencies": { + "typescript": "^5" + }, + "dependencies": { + "@tailwindcss/postcss": "^4.1.18", + "axios": "^1.13.2", + "react": "^19.2.3", + "react-dom": "^19.2.3", + "react-router-dom": "^7.12.0", + "tailwindcss": "^4.1.18" + } +} diff --git a/postcss.config.mjs b/postcss.config.mjs new file mode 100644 index 0000000..7e0ba62 --- /dev/null +++ b/postcss.config.mjs @@ -0,0 +1,5 @@ +export default { + plugins: { + "@tailwindcss/postcss": {}, + } +} diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..94b8419 --- /dev/null +++ b/public/index.html @@ -0,0 +1,12 @@ + + + + + + AiWorker - AI Agent Orchestration Platform + + +
+ + + diff --git a/server.ts b/server.ts new file mode 100644 index 0000000..243278d --- /dev/null +++ b/server.ts @@ -0,0 +1,73 @@ +import index from './public/index.html' + +const PORT = process.env.PORT || 3001 +const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:3000' + +Bun.serve({ + port: PORT, + async fetch(req) { + const url = new URL(req.url) + + // API proxy to backend + if (url.pathname.startsWith('/api/')) { + const backendUrl = `${BACKEND_URL}${url.pathname}${url.search}` + const headers = new Headers(req.headers) + + // Forward cookies + const cookie = req.headers.get('cookie') + if (cookie) { + headers.set('cookie', cookie) + } + + const backendReq = new Request(backendUrl, { + method: req.method, + headers, + body: req.method !== 'GET' && req.method !== 'HEAD' ? await req.text() : undefined, + }) + + const backendRes = await fetch(backendReq) + + // Forward Set-Cookie headers + const setCookies = backendRes.headers.get('set-cookie') + const responseHeaders = new Headers(backendRes.headers) + + if (setCookies) { + responseHeaders.set('set-cookie', setCookies) + } + + return new Response(backendRes.body, { + status: backendRes.status, + statusText: backendRes.statusText, + headers: responseHeaders, + }) + } + + // Serve static files from public directory + if (url.pathname !== '/' && !url.pathname.includes('.')) { + // SPA routing - serve index.html for all non-file routes + return new Response(index, { + headers: { + 'Content-Type': 'text/html', + }, + }) + } + + // Serve index.html for root + if (url.pathname === '/') { + return new Response(index, { + headers: { + 'Content-Type': 'text/html', + }, + }) + } + + // Let Bun handle other static files + return new Response('Not Found', { status: 404 }) + }, + development: { + hmr: true, + }, +}) + +console.log(`Frontend server running on http://localhost:${PORT}`) +console.log(`Proxying API requests to ${BACKEND_URL}`) diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..42fa680 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,29 @@ +import React from 'react' +import { Routes, Route, Navigate } from 'react-router-dom' +import { AuthProvider, ProtectedRoute } from './lib/auth' +import Layout from './components/Layout' +import Login from './pages/Login' +import Register from './pages/Register' +import Dashboard from './pages/Dashboard' + +export default function App() { + return ( + + + } /> + } /> + + + + + + } + /> + } /> + + + ) +} diff --git a/src/api/client.ts b/src/api/client.ts new file mode 100644 index 0000000..3aa0e2e --- /dev/null +++ b/src/api/client.ts @@ -0,0 +1,29 @@ +import axios from 'axios' + +// Determine API URL based on environment +const API_URL = import.meta.env.PROD + ? 'https://api.fuq.tv/api' + : 'http://localhost:3000/api' + +// Create axios instance with default config +export const apiClient = axios.create({ + baseURL: API_URL, + withCredentials: true, // Important: send cookies with requests + headers: { + 'Content-Type': 'application/json', + }, +}) + +// Response interceptor for error handling +apiClient.interceptors.response.use( + (response) => response, + (error) => { + if (error.response?.status === 401) { + // Unauthorized - redirect to login + window.location.href = '/login' + } + return Promise.reject(error) + } +) + +export default apiClient diff --git a/src/components/AgentStatus.tsx b/src/components/AgentStatus.tsx new file mode 100644 index 0000000..d2f2f27 --- /dev/null +++ b/src/components/AgentStatus.tsx @@ -0,0 +1,108 @@ +import React from 'react' +import type { Agent } from '../types' + +interface AgentStatusProps { + agents: Agent[] + onRefresh: () => void +} + +export default function AgentStatus({ agents, onRefresh }: AgentStatusProps) { + const getStatusColor = (status: string) => { + switch (status) { + case 'idle': + return 'bg-green-100 text-green-800' + case 'busy': + return 'bg-blue-100 text-blue-800' + case 'error': + return 'bg-red-100 text-red-800' + case 'offline': + return 'bg-gray-100 text-gray-800' + default: + return 'bg-gray-100 text-gray-800' + } + } + + const formatDate = (dateString: string) => { + const date = new Date(dateString) + const now = new Date() + const diffMs = now.getTime() - date.getTime() + const diffMins = Math.floor(diffMs / 60000) + + if (diffMins < 1) return 'Just now' + if (diffMins < 60) return `${diffMins}m ago` + const diffHours = Math.floor(diffMins / 60) + if (diffHours < 24) return `${diffHours}h ago` + const diffDays = Math.floor(diffHours / 24) + return `${diffDays}d ago` + } + + if (agents.length === 0) { + return ( +
+

No agents running. Deploy your first agent to get started.

+
+ ) + } + + return ( +
+ {agents.map((agent) => ( +
+
+
+

{agent.name}

+

{agent.k8sPodName}

+
+ + {agent.status} + +
+
+
+ Namespace:{' '} + {agent.k8sNamespace} +
+
+ Tasks completed:{' '} + {agent.totalTasksCompleted} +
+ {agent.averageTaskTimeMinutes && ( +
+ Avg. time:{' '} + {agent.averageTaskTimeMinutes}m +
+ )} +
+ Last heartbeat:{' '} + {formatDate(agent.lastHeartbeat)} +
+ {agent.errorCount > 0 && ( +
+ Errors:{' '} + {agent.errorCount} +
+ )} +
+ {agent.capabilities && agent.capabilities.length > 0 && ( +
+
+ {agent.capabilities.map((capability) => ( + + {capability} + + ))} +
+
+ )} +
+ ))} +
+ ) +} diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx new file mode 100644 index 0000000..225f1e5 --- /dev/null +++ b/src/components/Layout.tsx @@ -0,0 +1,45 @@ +import React, { ReactNode } from 'react' +import { useAuth } from '../lib/auth' +import { useNavigate } from 'react-router-dom' + +interface LayoutProps { + children: ReactNode +} + +export default function Layout({ children }: LayoutProps) { + const { user, logout } = useAuth() + const navigate = useNavigate() + + const handleLogout = async () => { + await logout() + navigate('/login') + } + + if (!user) { + return <>{children} + } + + return ( +
+ +
{children}
+
+ ) +} diff --git a/src/components/ProjectList.tsx b/src/components/ProjectList.tsx new file mode 100644 index 0000000..6345012 --- /dev/null +++ b/src/components/ProjectList.tsx @@ -0,0 +1,77 @@ +import React from 'react' +import type { Project } from '../types' + +interface ProjectListProps { + projects: Project[] + onRefresh: () => void +} + +export default function ProjectList({ projects, onRefresh }: ProjectListProps) { + const getStatusColor = (status: string) => { + switch (status) { + case 'active': + return 'bg-green-100 text-green-800' + case 'paused': + return 'bg-yellow-100 text-yellow-800' + case 'archived': + return 'bg-gray-100 text-gray-800' + default: + return 'bg-gray-100 text-gray-800' + } + } + + if (projects.length === 0) { + return ( +
+

No projects yet. Create your first project to get started.

+
+ ) + } + + return ( +
+ {projects.map((project) => ( +
+
+
+

{project.name}

+

{project.description}

+
+ + {project.status} + +
+
+ {project.giteaRepoUrl && ( + + )} +
+ Namespace:{' '} + {project.k8sNamespace} +
+
+ Resources:{' '} + + {project.replicas} replica(s), {project.cpuLimit} CPU, {project.memoryLimit} RAM + +
+
+
+ ))} +
+ ) +} diff --git a/src/components/TaskList.tsx b/src/components/TaskList.tsx new file mode 100644 index 0000000..515a9f4 --- /dev/null +++ b/src/components/TaskList.tsx @@ -0,0 +1,125 @@ +import React from 'react' +import type { Task } from '../types' + +interface TaskListProps { + tasks: Task[] + onRefresh: () => void +} + +export default function TaskList({ tasks, onRefresh }: TaskListProps) { + const getStateColor = (state: string) => { + switch (state) { + case 'backlog': + return 'bg-gray-100 text-gray-800' + case 'in_progress': + return 'bg-blue-100 text-blue-800' + case 'needs_input': + return 'bg-yellow-100 text-yellow-800' + case 'ready_to_test': + return 'bg-purple-100 text-purple-800' + case 'approved': + return 'bg-green-100 text-green-800' + case 'staging': + return 'bg-indigo-100 text-indigo-800' + case 'production': + return 'bg-emerald-100 text-emerald-800' + default: + return 'bg-gray-100 text-gray-800' + } + } + + const getPriorityColor = (priority: string) => { + switch (priority) { + case 'urgent': + return 'text-red-600' + case 'high': + return 'text-orange-600' + case 'medium': + return 'text-yellow-600' + case 'low': + return 'text-gray-600' + default: + return 'text-gray-600' + } + } + + if (tasks.length === 0) { + return ( +
+

No tasks yet. Create your first task to get started.

+
+ ) + } + + return ( +
+ {tasks.map((task) => ( +
+
+
+
+

{task.title}

+ + {task.state.replace('_', ' ')} + + + {task.priority.toUpperCase()} + +
+

{task.description}

+
+
+
+ {task.giteaPrUrl && ( +
+ Pull Request:{' '} + + #{task.giteaPrNumber} + +
+ )} + {task.previewUrl && ( +
+ Preview:{' '} + + View + +
+ )} + {task.estimatedComplexity && ( +
+ Complexity:{' '} + {task.estimatedComplexity} +
+ )} + {task.actualTimeMinutes && ( +
+ Time:{' '} + {task.actualTimeMinutes}m +
+ )} +
+ {task.errorMessage && ( +
+ {task.errorMessage} +
+ )} +
+ ))} +
+ ) +} diff --git a/src/index.css b/src/index.css new file mode 100644 index 0000000..f1d8c73 --- /dev/null +++ b/src/index.css @@ -0,0 +1 @@ +@import "tailwindcss"; diff --git a/src/lib/auth.tsx b/src/lib/auth.tsx new file mode 100644 index 0000000..70a8f30 --- /dev/null +++ b/src/lib/auth.tsx @@ -0,0 +1,80 @@ +import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react' +import apiClient from '../api/client' +import type { User } from '../types' + +interface AuthContextType { + user: User | null + loading: boolean + login: (email: string, password: string) => Promise + register: (email: string, username: string, password: string) => Promise + logout: () => Promise + refreshUser: () => Promise +} + +const AuthContext = createContext(undefined) + +export function AuthProvider({ children }: { children: ReactNode }) { + const [user, setUser] = useState(null) + const [loading, setLoading] = useState(true) + + // Check if user is authenticated on mount + useEffect(() => { + refreshUser() + }, []) + + const refreshUser = async () => { + try { + const response = await apiClient.get('/auth/me') + setUser(response.data.data) + } catch (error) { + setUser(null) + } finally { + setLoading(false) + } + } + + const login = async (email: string, password: string) => { + const response = await apiClient.post('/auth/login', { email, password }) + setUser(response.data.data.user) + } + + const register = async (email: string, username: string, password: string) => { + const response = await apiClient.post('/auth/register', { email, username, password }) + setUser(response.data.data.user) + } + + const logout = async () => { + await apiClient.post('/auth/logout') + setUser(null) + } + + return ( + + {children} + + ) +} + +export function useAuth() { + const context = useContext(AuthContext) + if (context === undefined) { + throw new Error('useAuth must be used within an AuthProvider') + } + return context +} + +// Protected route wrapper component +export function ProtectedRoute({ children }: { children: ReactNode }) { + const { user, loading } = useAuth() + + if (loading) { + return
Loading...
+ } + + if (!user) { + window.location.href = '/login' + return null + } + + return <>{children} +} diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..5c14fda --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,15 @@ +import React from 'react' +import { createRoot } from 'react-dom/client' +import { BrowserRouter } from 'react-router-dom' +import App from './App' +import './index.css' + +const root = createRoot(document.getElementById('root')!) + +root.render( + + + + + +) diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx new file mode 100644 index 0000000..0b78224 --- /dev/null +++ b/src/pages/Dashboard.tsx @@ -0,0 +1,100 @@ +import React, { useState, useEffect } from 'react' +import apiClient from '../api/client' +import type { Project, Task, Agent } from '../types' +import ProjectList from '../components/ProjectList' +import TaskList from '../components/TaskList' +import AgentStatus from '../components/AgentStatus' + +export default function Dashboard() { + const [projects, setProjects] = useState([]) + const [tasks, setTasks] = useState([]) + const [agents, setAgents] = useState([]) + const [loading, setLoading] = useState(true) + const [activeTab, setActiveTab] = useState<'projects' | 'tasks' | 'agents'>('projects') + + useEffect(() => { + loadData() + // Refresh data every 10 seconds + const interval = setInterval(loadData, 10000) + return () => clearInterval(interval) + }, []) + + const loadData = async () => { + try { + const [projectsRes, tasksRes, agentsRes] = await Promise.all([ + apiClient.get('/projects'), + apiClient.get('/tasks'), + apiClient.get('/agents'), + ]) + setProjects(projectsRes.data.data) + setTasks(tasksRes.data.data) + setAgents(agentsRes.data.data) + } catch (error) { + console.error('Failed to load dashboard data:', error) + } finally { + setLoading(false) + } + } + + if (loading) { + return ( +
+
Loading dashboard...
+
+ ) + } + + return ( +
+
+
+

AiWorker Dashboard

+

Manage your AI agents and development tasks

+
+ + {/* Tabs */} +
+ +
+ + {/* Content */} +
+ {activeTab === 'projects' && } + {activeTab === 'tasks' && } + {activeTab === 'agents' && } +
+
+
+ ) +} diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx new file mode 100644 index 0000000..380d473 --- /dev/null +++ b/src/pages/Login.tsx @@ -0,0 +1,87 @@ +import React, { useState } from 'react' +import { useNavigate, Link } from 'react-router-dom' +import { useAuth } from '../lib/auth' + +export default function Login() { + const [email, setEmail] = useState('') + const [password, setPassword] = useState('') + const [error, setError] = useState('') + const [loading, setLoading] = useState(false) + const { login } = useAuth() + const navigate = useNavigate() + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setError('') + setLoading(true) + + try { + await login(email, password) + navigate('/dashboard') + } catch (err: any) { + setError(err.response?.data?.message || 'Login failed') + } finally { + setLoading(false) + } + } + + return ( +
+
+
+

AiWorker

+

Sign in to your account

+
+
+ {error && ( +
+ {error} +
+ )} +
+
+ + setEmail(e.target.value)} + className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500" + /> +
+
+ + setPassword(e.target.value)} + className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500" + /> +
+
+ + + +
+ + Don't have an account? Register + +
+
+
+
+ ) +} diff --git a/src/pages/Register.tsx b/src/pages/Register.tsx new file mode 100644 index 0000000..ddec09e --- /dev/null +++ b/src/pages/Register.tsx @@ -0,0 +1,126 @@ +import React, { useState } from 'react' +import { useNavigate, Link } from 'react-router-dom' +import { useAuth } from '../lib/auth' + +export default function Register() { + const [email, setEmail] = useState('') + const [username, setUsername] = useState('') + const [password, setPassword] = useState('') + const [confirmPassword, setConfirmPassword] = useState('') + const [error, setError] = useState('') + const [loading, setLoading] = useState(false) + const { register } = useAuth() + const navigate = useNavigate() + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setError('') + + if (password !== confirmPassword) { + setError('Passwords do not match') + return + } + + if (password.length < 8) { + setError('Password must be at least 8 characters') + return + } + + setLoading(true) + + try { + await register(email, username, password) + navigate('/dashboard') + } catch (err: any) { + setError(err.response?.data?.message || 'Registration failed') + } finally { + setLoading(false) + } + } + + return ( +
+
+
+

AiWorker

+

Create your account

+
+
+ {error && ( +
+ {error} +
+ )} +
+
+ + setEmail(e.target.value)} + className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500" + /> +
+
+ + setUsername(e.target.value)} + className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500" + /> +
+
+ + setPassword(e.target.value)} + className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500" + /> +
+
+ + setConfirmPassword(e.target.value)} + className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500" + /> +
+
+ + + +
+ + Already have an account? Sign in + +
+
+
+
+ ) +} diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 0000000..cd595e4 --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,91 @@ +// Database entities +export interface Project { + id: string + name: string + description: string | null + giteaRepoId: number | null + giteaRepoUrl: string | null + giteaOwner: string | null + giteaRepoName: string | null + defaultBranch: string + k8sNamespace: string + dockerImage: string | null + envVars: Record | null + replicas: number + cpuLimit: string + memoryLimit: string + status: 'active' | 'paused' | 'archived' + createdAt: string + updatedAt: string +} + +export interface Task { + id: string + projectId: string + title: string + description: string + state: 'backlog' | 'in_progress' | 'needs_input' | 'ready_to_test' | 'approved' | 'staging' | 'production' + priority: 'low' | 'medium' | 'high' | 'urgent' + assignedAgentId: string | null + giteaBranchName: string | null + giteaPrNumber: number | null + giteaPrUrl: string | null + previewUrl: string | null + estimatedComplexity: 'simple' | 'moderate' | 'complex' | 'epic' | null + actualTimeMinutes: number | null + errorMessage: string | null + createdAt: string + updatedAt: string + startedAt: string | null + completedAt: string | null + project?: Project + agent?: Agent +} + +export interface Agent { + id: string + name: string + status: 'idle' | 'busy' | 'error' | 'offline' + currentTaskId: string | null + k8sPodName: string + k8sNamespace: string + capabilities: string[] + lastHeartbeat: string + totalTasksCompleted: number + averageTaskTimeMinutes: number | null + errorCount: number + createdAt: string + updatedAt: string + currentTask?: Task +} + +export interface User { + id: string + email: string + username: string + createdAt: string +} + +export interface Session { + id: string + userId: string + expiresAt: string +} + +// API Response types +export interface ApiResponse { + success: boolean + data: T + count?: number + message?: string + error?: string +} + +export interface PaginatedResponse { + success: boolean + data: T[] + count: number + page: number + pageSize: number + totalPages: number +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..bfa0fea --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + // Environment setup & latest features + "lib": ["ESNext"], + "target": "ESNext", + "module": "Preserve", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +}