Initialize Aidoit v0.1 monorepo
This commit is contained in:
@@ -0,0 +1,15 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.py]
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
[Makefile]
|
||||||
|
indent_style = tab
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
AIDOIT_ENV=development
|
||||||
|
AIDOIT_ADMIN_TOKEN=replace-with-a-long-random-token
|
||||||
|
|
||||||
|
POSTGRES_DB=aidoit
|
||||||
|
POSTGRES_USER=aidoit
|
||||||
|
POSTGRES_PASSWORD=replace-me
|
||||||
|
DATABASE_URL=postgresql+psycopg://aidoit:replace-me@localhost:5432/aidoit
|
||||||
|
|
||||||
|
REDIS_URL=redis://localhost:6379/0
|
||||||
|
|
||||||
|
MINIO_ROOT_USER=aidoit
|
||||||
|
MINIO_ROOT_PASSWORD=replace-with-a-long-password
|
||||||
|
MINIO_ENDPOINT=http://localhost:9000
|
||||||
|
MINIO_BUCKET=aidoit-artifacts
|
||||||
|
|
||||||
|
NEXT_PUBLIC_API_URL=http://localhost:8000
|
||||||
|
WEB_ORIGIN=http://localhost:3000
|
||||||
|
MSP_ORIGIN=http://localhost:3001
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ["main"]
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
frontend:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
version: 10
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
cache: pnpm
|
||||||
|
- run: pnpm install --no-frozen-lockfile
|
||||||
|
- run: pnpm typecheck
|
||||||
|
- run: pnpm build
|
||||||
|
|
||||||
|
api:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: "3.13"
|
||||||
|
- run: pip install -r apps/api/requirements-dev.txt
|
||||||
|
- run: pytest apps/api/tests
|
||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
.DS_Store
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
node_modules/
|
||||||
|
.pnpm-store/
|
||||||
|
.turbo/
|
||||||
|
.next/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
coverage/
|
||||||
|
*.log
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
.pytest_cache/
|
||||||
|
.ruff_cache/
|
||||||
|
.mypy_cache/
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
target/
|
||||||
|
*.db
|
||||||
|
*.sqlite3
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
.PHONY: install dev up down logs build lint typecheck test
|
||||||
|
|
||||||
|
install:
|
||||||
|
pnpm install
|
||||||
|
cd apps/api && python -m venv .venv && .venv/bin/pip install -r requirements-dev.txt
|
||||||
|
|
||||||
|
dev:
|
||||||
|
pnpm dev
|
||||||
|
|
||||||
|
up:
|
||||||
|
docker compose -f infrastructure/docker/compose.yml up -d
|
||||||
|
|
||||||
|
down:
|
||||||
|
docker compose -f infrastructure/docker/compose.yml down
|
||||||
|
|
||||||
|
logs:
|
||||||
|
docker compose -f infrastructure/docker/compose.yml logs -f
|
||||||
|
|
||||||
|
build:
|
||||||
|
pnpm build
|
||||||
|
|
||||||
|
lint:
|
||||||
|
pnpm lint
|
||||||
|
|
||||||
|
typecheck:
|
||||||
|
pnpm typecheck
|
||||||
|
|
||||||
|
test:
|
||||||
|
pnpm test
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
# Aidoit
|
||||||
|
|
||||||
|
Aidoit is a single-user personal AI operating system for building applications,
|
||||||
|
running AI teams, and managing infrastructure.
|
||||||
|
|
||||||
|
## Repository structure
|
||||||
|
|
||||||
|
```text
|
||||||
|
apps/
|
||||||
|
web/ Main web development dashboard
|
||||||
|
msp/ Infrastructure management dashboard
|
||||||
|
api/ Shared modular REST API
|
||||||
|
desktop/ Tauri desktop application workspace
|
||||||
|
|
||||||
|
packages/
|
||||||
|
ui/ Shared React components and design tokens
|
||||||
|
contracts/ Shared domain and API contracts
|
||||||
|
sdk/ Typed API client
|
||||||
|
config/ Shared TypeScript configuration
|
||||||
|
|
||||||
|
infrastructure/
|
||||||
|
docker/ Local Docker Compose stack
|
||||||
|
nginx/ Reverse-proxy examples
|
||||||
|
|
||||||
|
docs/
|
||||||
|
architecture/
|
||||||
|
decisions/
|
||||||
|
roadmap/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Node.js 22+
|
||||||
|
- pnpm 10+
|
||||||
|
- Python 3.13+
|
||||||
|
- Docker with Docker Compose
|
||||||
|
|
||||||
|
## Local development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
pnpm install
|
||||||
|
docker compose -f infrastructure/docker/compose.yml up -d postgres redis minio
|
||||||
|
pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Services:
|
||||||
|
|
||||||
|
- Web dashboard: http://localhost:3000
|
||||||
|
- MSP dashboard: http://localhost:3001
|
||||||
|
- API: http://localhost:8000
|
||||||
|
- OpenAPI: http://localhost:8000/docs
|
||||||
|
- MinIO console: http://localhost:9001
|
||||||
|
|
||||||
|
## First production targets
|
||||||
|
|
||||||
|
- `web.aidoit.top`
|
||||||
|
- `msp.aidoit.top`
|
||||||
|
- `api.aidoit.top`
|
||||||
|
- desktop application communicating with the shared API
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
This repository is the professional v0.1 foundation. It intentionally starts as a
|
||||||
|
modular monolith and leaves room for later extraction of workers or services when
|
||||||
|
real operational needs justify it.
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
FROM python:3.13-slim AS runtime
|
||||||
|
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY pyproject.toml requirements.txt ./
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
COPY app ./app
|
||||||
|
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
|
|
||||||
|
|
||||||
|
class Settings(BaseSettings):
|
||||||
|
aidoit_env: str = "development"
|
||||||
|
aidoit_admin_token: str = "replace-me"
|
||||||
|
database_url: str = "sqlite:///./aidoit.db"
|
||||||
|
redis_url: str = "redis://localhost:6379/0"
|
||||||
|
web_origin: str = "http://localhost:3000"
|
||||||
|
msp_origin: str = "http://localhost:3001"
|
||||||
|
|
||||||
|
model_config = SettingsConfigDict(
|
||||||
|
env_file=".env",
|
||||||
|
env_file_encoding="utf-8",
|
||||||
|
case_sensitive=False,
|
||||||
|
extra="ignore",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
settings = Settings()
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
from fastapi import FastAPI
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
|
||||||
|
from app.core.config import settings
|
||||||
|
from app.modules.health.router import router as health_router
|
||||||
|
from app.modules.projects.router import router as projects_router
|
||||||
|
|
||||||
|
app = FastAPI(
|
||||||
|
title="Aidoit API",
|
||||||
|
version="0.1.0",
|
||||||
|
description="Shared REST API for the Aidoit personal AI operating system.",
|
||||||
|
)
|
||||||
|
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=[settings.web_origin, settings.msp_origin],
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
app.include_router(health_router)
|
||||||
|
app.include_router(projects_router, prefix="/api/v1")
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
async def root() -> dict[str, str]:
|
||||||
|
return {
|
||||||
|
"name": "Aidoit API",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"docs": "/docs",
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
from fastapi import APIRouter
|
||||||
|
|
||||||
|
router = APIRouter(tags=["health"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/health")
|
||||||
|
async def health() -> dict[str, str]:
|
||||||
|
return {
|
||||||
|
"status": "ok",
|
||||||
|
"service": "aidoit-api",
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
from fastapi import APIRouter, HTTPException, status
|
||||||
|
|
||||||
|
from app.modules.projects.schemas import ProjectCreate, ProjectRead
|
||||||
|
from app.modules.projects.service import project_service
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/projects", tags=["projects"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("", response_model=list[ProjectRead])
|
||||||
|
async def list_projects() -> list[ProjectRead]:
|
||||||
|
return project_service.list()
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"",
|
||||||
|
response_model=ProjectRead,
|
||||||
|
status_code=status.HTTP_201_CREATED,
|
||||||
|
)
|
||||||
|
async def create_project(payload: ProjectCreate) -> ProjectRead:
|
||||||
|
return project_service.create(payload)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{project_id}", response_model=ProjectRead)
|
||||||
|
async def get_project(project_id: int) -> ProjectRead:
|
||||||
|
project = project_service.get(project_id)
|
||||||
|
if project is None:
|
||||||
|
raise HTTPException(status_code=404, detail="Project not found")
|
||||||
|
return project
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
from pydantic import BaseModel, ConfigDict, Field
|
||||||
|
|
||||||
|
ProductType = Literal["web", "desktop", "msp"]
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectCreate(BaseModel):
|
||||||
|
name: str = Field(min_length=1, max_length=120)
|
||||||
|
description: str = Field(default="", max_length=2000)
|
||||||
|
product: ProductType = "web"
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectRead(ProjectCreate):
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
id: int
|
||||||
|
created_at: datetime
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
from datetime import UTC, datetime
|
||||||
|
from itertools import count
|
||||||
|
|
||||||
|
from app.modules.projects.schemas import ProjectCreate, ProjectRead
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectService:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self._ids = count(1)
|
||||||
|
self._projects: list[ProjectRead] = []
|
||||||
|
|
||||||
|
def list(self) -> list[ProjectRead]:
|
||||||
|
return self._projects.copy()
|
||||||
|
|
||||||
|
def create(self, payload: ProjectCreate) -> ProjectRead:
|
||||||
|
project = ProjectRead(
|
||||||
|
id=next(self._ids),
|
||||||
|
created_at=datetime.now(UTC),
|
||||||
|
**payload.model_dump(),
|
||||||
|
)
|
||||||
|
self._projects.append(project)
|
||||||
|
return project
|
||||||
|
|
||||||
|
def get(self, project_id: int) -> ProjectRead | None:
|
||||||
|
return next(
|
||||||
|
(project for project in self._projects if project.id == project_id),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
project_service = ProjectService()
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
[project]
|
||||||
|
name = "aidoit-api"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Shared REST API for Aidoit"
|
||||||
|
requires-python = ">=3.13"
|
||||||
|
dependencies = [
|
||||||
|
"fastapi>=0.115",
|
||||||
|
"uvicorn[standard]>=0.34",
|
||||||
|
"pydantic-settings>=2.7",
|
||||||
|
"sqlalchemy>=2.0",
|
||||||
|
"psycopg[binary]>=3.2",
|
||||||
|
"alembic>=1.14",
|
||||||
|
"redis>=5.2",
|
||||||
|
"httpx>=0.28"
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
dev = [
|
||||||
|
"pytest>=8.3",
|
||||||
|
"pytest-asyncio>=0.25",
|
||||||
|
"ruff>=0.9",
|
||||||
|
"mypy>=1.14"
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
line-length = 100
|
||||||
|
target-version = "py313"
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
pythonpath = ["."]
|
||||||
|
testpaths = ["tests"]
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
-e .[dev]
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
-e .
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
from app.main import app
|
||||||
|
|
||||||
|
client = TestClient(app)
|
||||||
|
|
||||||
|
|
||||||
|
def test_health() -> None:
|
||||||
|
response = client.get("/health")
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json()["status"] == "ok"
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# Aidoit Desktop
|
||||||
|
|
||||||
|
This directory is reserved for the native Tauri application.
|
||||||
|
|
||||||
|
Planned capabilities:
|
||||||
|
|
||||||
|
- API authentication and synchronization
|
||||||
|
- local filesystem access
|
||||||
|
- shell execution
|
||||||
|
- Git operations
|
||||||
|
- Docker operations
|
||||||
|
- local Ollama integration
|
||||||
|
- native builds for macOS, Windows and Linux
|
||||||
|
|
||||||
|
Tauri initialization is intentionally deferred until the shared contracts and
|
||||||
|
desktop security boundaries are finalized.
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import "@aidoit/ui/styles.css";
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
title: "Aidoit MSP",
|
||||||
|
description: "Infrastructure management workspace",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
}: Readonly<{ children: React.ReactNode }>) {
|
||||||
|
return (
|
||||||
|
<html lang="en">
|
||||||
|
<body>{children}</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
export default function MspHomePage() {
|
||||||
|
return (
|
||||||
|
<main style={{ padding: "48px", maxWidth: "1000px", margin: "0 auto" }}>
|
||||||
|
<p style={{ color: "var(--accent)", fontWeight: 800 }}>AIDOIT MSP</p>
|
||||||
|
<h1>Infrastructure workspace</h1>
|
||||||
|
<p style={{ color: "var(--muted)" }}>
|
||||||
|
VPS, Proxmox, Docker, LXC, monitoring, backups and deployments will live here.
|
||||||
|
</p>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
|
const config: NextConfig = {
|
||||||
|
output: "standalone",
|
||||||
|
transpilePackages: ["@aidoit/ui", "@aidoit/contracts", "@aidoit/sdk"],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"name": "@aidoit/msp",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev --port 3001",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start --port 3001",
|
||||||
|
"lint": "next lint",
|
||||||
|
"typecheck": "tsc --noEmit",
|
||||||
|
"test": "echo \"No MSP tests yet\""
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@aidoit/contracts": "workspace:*",
|
||||||
|
"@aidoit/sdk": "workspace:*",
|
||||||
|
"@aidoit/ui": "workspace:*",
|
||||||
|
"next": "^15.2.0",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@aidoit/config": "workspace:*",
|
||||||
|
"@types/node": "^22.0.0",
|
||||||
|
"@types/react": "^19.0.0",
|
||||||
|
"@types/react-dom": "^19.0.0",
|
||||||
|
"typescript": "^5.8.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"extends": "@aidoit/config/nextjs.json",
|
||||||
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
.app-shell {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 250px 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
padding: 26px 20px;
|
||||||
|
border-right: 1px solid var(--border);
|
||||||
|
background: rgba(6, 14, 22, 0.9);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 34px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand strong,
|
||||||
|
.brand span {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand span {
|
||||||
|
margin-top: 3px;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-mark {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 13px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
color: #041019;
|
||||||
|
background: linear-gradient(135deg, #83dcff, #4ca3ff);
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav {
|
||||||
|
display: grid;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav a {
|
||||||
|
color: var(--muted);
|
||||||
|
padding: 11px 12px;
|
||||||
|
border-radius: 10px;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav a:hover,
|
||||||
|
nav a.active {
|
||||||
|
color: var(--text);
|
||||||
|
background: rgba(87, 199, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-status {
|
||||||
|
margin-top: auto;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 12px;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
padding-top: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-status span,
|
||||||
|
.feed-item > span {
|
||||||
|
display: inline-block;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
margin-right: 7px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #5be18a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace {
|
||||||
|
padding: 38px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero h1 {
|
||||||
|
font-size: clamp(30px, 4vw, 48px);
|
||||||
|
margin: 3px 0 10px;
|
||||||
|
letter-spacing: -0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero > div > p:last-child,
|
||||||
|
.empty-state p,
|
||||||
|
.feed-item p {
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero button {
|
||||||
|
border: 0;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 13px 18px;
|
||||||
|
background: var(--accent);
|
||||||
|
color: #041019;
|
||||||
|
font-weight: 800;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eyebrow {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--accent);
|
||||||
|
font-weight: 800;
|
||||||
|
font-size: 11px;
|
||||||
|
letter-spacing: 0.14em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1.4fr 0.8fr;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
min-height: 330px;
|
||||||
|
padding: 22px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: linear-gradient(180deg, rgba(16, 30, 44, 0.96), rgba(10, 22, 32, 0.96));
|
||||||
|
border-radius: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel h2 {
|
||||||
|
margin: 3px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-heading {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-heading a {
|
||||||
|
color: var(--accent);
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
min-height: 240px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
align-content: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state > div {
|
||||||
|
width: 54px;
|
||||||
|
height: 54px;
|
||||||
|
border-radius: 18px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
background: rgba(87, 199, 255, 0.1);
|
||||||
|
color: var(--accent);
|
||||||
|
font-size: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feed-item {
|
||||||
|
margin-top: 30px;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feed-item p {
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1000px) {
|
||||||
|
.app-shell {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-grid {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 620px) {
|
||||||
|
.workspace {
|
||||||
|
padding: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero {
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import "@aidoit/ui/styles.css";
|
||||||
|
import "./app.css";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Aidoit",
|
||||||
|
description: "Personal AI operating system",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
}: Readonly<{ children: React.ReactNode }>) {
|
||||||
|
return (
|
||||||
|
<html lang="en">
|
||||||
|
<body>{children}</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import { StatCard } from "@aidoit/ui";
|
||||||
|
|
||||||
|
const navigation = [
|
||||||
|
"Overview",
|
||||||
|
"Projects",
|
||||||
|
"Runs",
|
||||||
|
"Tasks",
|
||||||
|
"Teams",
|
||||||
|
"Tools",
|
||||||
|
"Deployments",
|
||||||
|
"Servers",
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function HomePage() {
|
||||||
|
return (
|
||||||
|
<main className="app-shell">
|
||||||
|
<aside className="sidebar">
|
||||||
|
<div className="brand">
|
||||||
|
<div className="brand-mark">A</div>
|
||||||
|
<div>
|
||||||
|
<strong>Aidoit</strong>
|
||||||
|
<span>Personal AI OS</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav>
|
||||||
|
{navigation.map((item, index) => (
|
||||||
|
<a className={index === 0 ? "active" : ""} href="#" key={item}>
|
||||||
|
{item}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div className="api-status">
|
||||||
|
<span />
|
||||||
|
Development environment
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<section className="workspace">
|
||||||
|
<header className="hero">
|
||||||
|
<div>
|
||||||
|
<p className="eyebrow">AIDOIT CONTROL CENTER</p>
|
||||||
|
<h1>Good evening, Pavel.</h1>
|
||||||
|
<p>
|
||||||
|
Build applications, orchestrate AI teams, and manage infrastructure
|
||||||
|
from one workspace.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button type="button">New project</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section className="stats-grid" aria-label="Workspace statistics">
|
||||||
|
<StatCard label="Projects" value="0" detail="Web, desktop and MSP" />
|
||||||
|
<StatCard label="Active runs" value="0" detail="Harness executions" />
|
||||||
|
<StatCard label="Pending tasks" value="0" detail="Waiting for a team" />
|
||||||
|
<StatCard label="Deployments" value="0" detail="Preview and production" />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="content-grid">
|
||||||
|
<article className="panel">
|
||||||
|
<div className="panel-heading">
|
||||||
|
<div>
|
||||||
|
<p className="eyebrow">WORKSPACE</p>
|
||||||
|
<h2>Recent projects</h2>
|
||||||
|
</div>
|
||||||
|
<a href="#">View all</a>
|
||||||
|
</div>
|
||||||
|
<div className="empty-state">
|
||||||
|
<div>+</div>
|
||||||
|
<h3>No projects yet</h3>
|
||||||
|
<p>Create the first project and assign it to an AI team.</p>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article className="panel">
|
||||||
|
<p className="eyebrow">ACTIVITY</p>
|
||||||
|
<h2>Harness feed</h2>
|
||||||
|
<div className="feed-item">
|
||||||
|
<span />
|
||||||
|
<div>
|
||||||
|
<strong>Platform initialized</strong>
|
||||||
|
<p>Professional monorepo foundation is ready.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
|
const config: NextConfig = {
|
||||||
|
output: "standalone",
|
||||||
|
transpilePackages: ["@aidoit/ui", "@aidoit/contracts", "@aidoit/sdk"],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"name": "@aidoit/web",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev --port 3000",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start --port 3000",
|
||||||
|
"lint": "next lint",
|
||||||
|
"typecheck": "tsc --noEmit",
|
||||||
|
"test": "echo \"No web tests yet\""
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@aidoit/contracts": "workspace:*",
|
||||||
|
"@aidoit/sdk": "workspace:*",
|
||||||
|
"@aidoit/ui": "workspace:*",
|
||||||
|
"next": "^15.2.0",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@aidoit/config": "workspace:*",
|
||||||
|
"@types/node": "^22.0.0",
|
||||||
|
"@types/react": "^19.0.0",
|
||||||
|
"@types/react-dom": "^19.0.0",
|
||||||
|
"typescript": "^5.8.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"extends": "@aidoit/config/nextjs.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
# Architecture overview
|
||||||
|
|
||||||
|
## Principles
|
||||||
|
|
||||||
|
1. Single user, but authentication is still mandatory.
|
||||||
|
2. One shared backend for all products.
|
||||||
|
3. Modular monolith before microservices.
|
||||||
|
4. AI harness remains independent from UI applications.
|
||||||
|
5. Tool access is explicitly permissioned per team.
|
||||||
|
6. Long-running work is executed asynchronously.
|
||||||
|
7. Generated artifacts are stored outside the relational database.
|
||||||
|
|
||||||
|
## Products
|
||||||
|
|
||||||
|
### Web
|
||||||
|
|
||||||
|
Application development dashboard for projects, prompts, runs, tasks, preview
|
||||||
|
environments, reviews and deployments.
|
||||||
|
|
||||||
|
### Desktop
|
||||||
|
|
||||||
|
Native Tauri application for local filesystem, shell, Git, Docker, Ollama and
|
||||||
|
desktop application builds.
|
||||||
|
|
||||||
|
### MSP
|
||||||
|
|
||||||
|
Infrastructure dashboard for VPS, Proxmox, LXC, VM, Docker, firewall, reverse
|
||||||
|
proxy, monitoring, updates and backups.
|
||||||
|
|
||||||
|
## Core backend modules
|
||||||
|
|
||||||
|
- authentication
|
||||||
|
- projects
|
||||||
|
- runs
|
||||||
|
- tasks
|
||||||
|
- teams
|
||||||
|
- tools
|
||||||
|
- artifacts
|
||||||
|
- builds
|
||||||
|
- deployments
|
||||||
|
- servers
|
||||||
|
- monitoring
|
||||||
|
- audit log
|
||||||
|
|
||||||
|
## Data systems
|
||||||
|
|
||||||
|
- PostgreSQL: relational source of truth
|
||||||
|
- Redis: queues, locks and ephemeral state
|
||||||
|
- MinIO: generated files, logs and build artifacts
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# ADR 0001: Start as a modular monolith
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Accepted
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
The first production version uses a single FastAPI deployment with explicit
|
||||||
|
module boundaries.
|
||||||
|
|
||||||
|
## Reasons
|
||||||
|
|
||||||
|
- one operator
|
||||||
|
- easier local development
|
||||||
|
- simpler deployment and backups
|
||||||
|
- no premature distributed systems complexity
|
||||||
|
- modules can be extracted later when justified by real load or isolation needs
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# ADR 0002: Single-user security model
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Accepted
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Aidoit is single-user software, but no sensitive endpoint is anonymously
|
||||||
|
accessible in production.
|
||||||
|
|
||||||
|
Initial access:
|
||||||
|
|
||||||
|
- Tailscale for private network access
|
||||||
|
- bearer token for development
|
||||||
|
- later PocketID or Authentik for browser login
|
||||||
|
- separate execution permissions for every AI team
|
||||||
|
- immutable audit records for tool calls and infrastructure changes
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# v0.1 roadmap
|
||||||
|
|
||||||
|
## Foundation
|
||||||
|
|
||||||
|
- monorepo and CI
|
||||||
|
- shared design system
|
||||||
|
- shared API contracts and SDK
|
||||||
|
- PostgreSQL migrations
|
||||||
|
- authentication
|
||||||
|
- project CRUD
|
||||||
|
|
||||||
|
## Harness
|
||||||
|
|
||||||
|
- team definitions
|
||||||
|
- tool registry
|
||||||
|
- task graph
|
||||||
|
- run execution
|
||||||
|
- event streaming
|
||||||
|
- logs and artifacts
|
||||||
|
|
||||||
|
## Development workflow
|
||||||
|
|
||||||
|
- Gitea integration
|
||||||
|
- repository checkout
|
||||||
|
- isolated workspaces
|
||||||
|
- preview Docker builds
|
||||||
|
- code review flow
|
||||||
|
- deployment approvals
|
||||||
|
|
||||||
|
## MSP
|
||||||
|
|
||||||
|
- server inventory
|
||||||
|
- SSH connection profiles
|
||||||
|
- Docker and Proxmox adapters
|
||||||
|
- monitoring integration
|
||||||
|
- backups and update plans
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:17-alpine
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: ${POSTGRES_DB}
|
||||||
|
POSTGRES_USER: ${POSTGRES_USER}
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "6379:6379"
|
||||||
|
volumes:
|
||||||
|
- redis_data:/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "redis-cli", "ping"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
minio:
|
||||||
|
image: minio/minio:latest
|
||||||
|
restart: unless-stopped
|
||||||
|
command: server /data --console-address ":9001"
|
||||||
|
environment:
|
||||||
|
MINIO_ROOT_USER: ${MINIO_ROOT_USER}
|
||||||
|
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
|
||||||
|
ports:
|
||||||
|
- "9000:9000"
|
||||||
|
- "9001:9001"
|
||||||
|
volumes:
|
||||||
|
- minio_data:/data
|
||||||
|
|
||||||
|
api:
|
||||||
|
build:
|
||||||
|
context: ../../apps/api
|
||||||
|
restart: unless-stopped
|
||||||
|
env_file:
|
||||||
|
- ../../.env
|
||||||
|
environment:
|
||||||
|
DATABASE_URL: postgresql+psycopg://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}
|
||||||
|
REDIS_URL: redis://redis:6379/0
|
||||||
|
MINIO_ENDPOINT: http://minio:9000
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
|
redis_data:
|
||||||
|
minio_data:
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# Reverse proxy
|
||||||
|
|
||||||
|
Production proxy hosts should route:
|
||||||
|
|
||||||
|
- `web.aidoit.top` to the web application
|
||||||
|
- `msp.aidoit.top` to the MSP application
|
||||||
|
- `api.aidoit.top` to the API
|
||||||
|
|
||||||
|
For the first deployment, Nginx Proxy Manager can be used. Keep the API and
|
||||||
|
databases on a private Docker network and expose only the reverse proxy.
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"name": "aidoit",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"packageManager": "pnpm@10.0.0",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "turbo dev",
|
||||||
|
"build": "turbo build",
|
||||||
|
"lint": "turbo lint",
|
||||||
|
"typecheck": "turbo typecheck",
|
||||||
|
"test": "turbo test",
|
||||||
|
"format": "prettier --write .",
|
||||||
|
"format:check": "prettier --check .",
|
||||||
|
"dev:web": "pnpm --filter @aidoit/web dev",
|
||||||
|
"dev:msp": "pnpm --filter @aidoit/msp dev",
|
||||||
|
"dev:api": "cd apps/api && python -m uvicorn app.main:app --reload --port 8000"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"prettier": "^3.5.0",
|
||||||
|
"turbo": "^2.5.0",
|
||||||
|
"typescript": "^5.8.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/tsconfig",
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"extends": "./base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"jsx": "preserve",
|
||||||
|
"incremental": true,
|
||||||
|
"plugins": [{ "name": "next" }]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"name": "@aidoit/config",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"files": [
|
||||||
|
"*.json"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"extends": "./base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"jsx": "react-jsx"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"name": "@aidoit/contracts",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"exports": {
|
||||||
|
".": "./src/index.ts"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc --noEmit",
|
||||||
|
"lint": "tsc --noEmit",
|
||||||
|
"typecheck": "tsc --noEmit",
|
||||||
|
"test": "echo \"No contract tests yet\""
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@aidoit/config": "workspace:*",
|
||||||
|
"typescript": "^5.8.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
export type ProductType = "web" | "desktop" | "msp";
|
||||||
|
|
||||||
|
export interface Project {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
product: ProductType;
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateProjectRequest {
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
product?: ProductType;
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"extends": "@aidoit/config/base.json",
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"name": "@aidoit/sdk",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"exports": {
|
||||||
|
".": "./src/index.ts"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc --noEmit",
|
||||||
|
"lint": "tsc --noEmit",
|
||||||
|
"typecheck": "tsc --noEmit",
|
||||||
|
"test": "echo \"No SDK tests yet\""
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@aidoit/contracts": "workspace:*"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@aidoit/config": "workspace:*",
|
||||||
|
"typescript": "^5.8.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import type { CreateProjectRequest, Project } from "@aidoit/contracts";
|
||||||
|
|
||||||
|
export class AidoitClient {
|
||||||
|
public constructor(
|
||||||
|
private readonly baseUrl: string,
|
||||||
|
private readonly token?: string,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
private async request<T>(path: string, init?: RequestInit): Promise<T> {
|
||||||
|
const response = await fetch(`${this.baseUrl}${path}`, {
|
||||||
|
...init,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
...(this.token ? { Authorization: `Bearer ${this.token}` } : {}),
|
||||||
|
...init?.headers,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Aidoit API request failed: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json() as Promise<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
public listProjects(): Promise<Project[]> {
|
||||||
|
return this.request<Project[]>("/api/v1/projects");
|
||||||
|
}
|
||||||
|
|
||||||
|
public createProject(payload: CreateProjectRequest): Promise<Project> {
|
||||||
|
return this.request<Project>("/api/v1/projects", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"extends": "@aidoit/config/base.json",
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"name": "@aidoit/ui",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"exports": {
|
||||||
|
".": "./src/index.ts",
|
||||||
|
"./styles.css": "./src/styles.css"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc --noEmit",
|
||||||
|
"lint": "tsc --noEmit",
|
||||||
|
"typecheck": "tsc --noEmit",
|
||||||
|
"test": "echo \"No UI tests yet\""
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^19.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@aidoit/config": "workspace:*",
|
||||||
|
"@types/react": "^19.0.0",
|
||||||
|
"typescript": "^5.8.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { StatCard } from "./stat-card";
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
type StatCardProps = {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
detail: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function StatCard({ label, value, detail }: StatCardProps) {
|
||||||
|
return (
|
||||||
|
<article className="aidoit-stat-card">
|
||||||
|
<span>{label}</span>
|
||||||
|
<strong>{value}</strong>
|
||||||
|
<p>{detail}</p>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
:root {
|
||||||
|
color-scheme: dark;
|
||||||
|
--background: #071019;
|
||||||
|
--surface: #0d1824;
|
||||||
|
--surface-soft: #101e2c;
|
||||||
|
--border: rgba(255, 255, 255, 0.08);
|
||||||
|
--text: #eef6ff;
|
||||||
|
--muted: #8ca0b5;
|
||||||
|
--accent: #57c7ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
background: var(--background);
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 80% 0%, rgba(45, 139, 196, 0.12), transparent 28%),
|
||||||
|
var(--background);
|
||||||
|
color: var(--text);
|
||||||
|
font-family:
|
||||||
|
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
|
||||||
|
sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aidoit-stat-card {
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: linear-gradient(180deg, rgba(16, 30, 44, 0.96), rgba(10, 22, 32, 0.96));
|
||||||
|
border-radius: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aidoit-stat-card span,
|
||||||
|
.aidoit-stat-card p {
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.aidoit-stat-card strong {
|
||||||
|
display: block;
|
||||||
|
font-size: 34px;
|
||||||
|
margin: 18px 0 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aidoit-stat-card p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"extends": "@aidoit/config/react-library.json",
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
packages:
|
||||||
|
- apps/web
|
||||||
|
- apps/msp
|
||||||
|
- packages/*
|
||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://turbo.build/schema.json",
|
||||||
|
"ui": "tui",
|
||||||
|
"tasks": {
|
||||||
|
"dev": {
|
||||||
|
"cache": false,
|
||||||
|
"persistent": true
|
||||||
|
},
|
||||||
|
"build": {
|
||||||
|
"dependsOn": ["^build"],
|
||||||
|
"outputs": [".next/**", "dist/**", "! .next/cache/**"]
|
||||||
|
},
|
||||||
|
"lint": {
|
||||||
|
"dependsOn": ["^lint"]
|
||||||
|
},
|
||||||
|
"typecheck": {
|
||||||
|
"dependsOn": ["^typecheck"]
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"dependsOn": ["^build"],
|
||||||
|
"outputs": ["coverage/**"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user