feat: add control-plane, web, and docker-compose

- Add control-plane service with Dockerfile and internal packages
- Add web application (Next.js)
- Add docker-compose.yml for orchestration
- Update .gitignore and README.md
This commit is contained in:
toki 2026-05-18 06:21:48 +09:00
parent 05a7491300
commit ed882cf6a4
27 changed files with 2491 additions and 14 deletions

7
.gitignore vendored
View file

@ -5,3 +5,10 @@ agent-ops/rules/private/
# Local runtime/test artifacts
/iop.db
/*.log
# Web Portal artifacts
apps/web/node_modules/
apps/web/.next/
apps/web/out/
apps/web/.env*.local
apps/web/*.tsbuildinfo

View file

@ -206,14 +206,17 @@ NomadCode
| 경로 | 현재 의미 |
|---|---|
| `apps/web` | IOP 전체 Web Portal을 위한 Next.js 스캐폴드 |
| `apps/node` | Edge에 연결되어 adapter execution을 수행하는 Node agent |
| `apps/edge` | Node registry, 설정 전달, routing, stream relay를 담당하는 Edge skeleton |
| `apps/control-plane` | 향후 여러 Edge를 연결하고 운영 화면을 제공할 중앙 관리 계층 |
| `apps/control-plane` | 여러 Edge를 연결하고 Portal과 통신할 Go 기반 운영 제어 서버 스캐폴드 |
| `apps/worker` | 현재 placeholder이며, Worker 구조는 우선 각 Go 서비스 내부 공통 모듈 방향으로 둔다 |
| `packages` | 설정, 인증, 정책, 작업, 관측성, 버전 등 공통 패키지 |
| `proto` | 앱 간 메시지 계약 원본과 생성물 |
| `configs` | 현재 개발용 설정 예시 |
`apps/web`은 IOP 전체 Web Portal이며, `apps/control-plane`은 Go 기반 운영 제어 서버다. 주요 통신은 edge-node에서 사용 중인 protobuf-socket을 IOP Wire Protocol 기준으로 Portal-Control Plane, Control Plane-Edge, Edge-Node 방향으로 확장한다. `net/http`는 health/readiness/bootstrap 같은 보조 endpoint 용도로 유지한다.
## Roadmap
### Phase 1. Edge-Node execution skeleton

View file

@ -0,0 +1,29 @@
# syntax=docker/dockerfile:1
FROM golang:1.24-alpine AS builder
WORKDIR /workspace/go-iop
RUN apk add --no-cache git
# docker-compose builds this Dockerfile with /config/workspace as context so
# the local ../proto-socket/go replace in go.mod is available.
COPY go-iop/go.mod go-iop/go.sum ./
COPY proto-socket/go /workspace/proto-socket/go
RUN go mod download
COPY go-iop/ ./
RUN CGO_ENABLED=0 GOFLAGS=-trimpath go build -o /out/control-plane ./apps/control-plane/cmd/control-plane
FROM alpine:3.22
RUN addgroup -S iop && adduser -S iop -G iop
WORKDIR /app
COPY --from=builder /out/control-plane /usr/local/bin/control-plane
COPY --from=builder /workspace/go-iop/configs/control-plane.yaml /app/configs/control-plane.yaml
EXPOSE 9080 19080
USER iop
ENTRYPOINT ["control-plane"]
CMD ["serve", "--config", "/app/configs/control-plane.yaml"]

View file

@ -2,11 +2,17 @@
여러 Edge를 연결하고 관찰하며, Edge 설정 변경과 명령 전달을 담당할 Go 기반 중앙 관리 서버다.
**현재 상태: Placeholder**
**현재 상태: Scaffold**
현재 Control Plane은 구현 전 단계다. Node를 직접 등록하거나 직접 스케줄링하는 계층으로 확정하지 않는다. Control Plane은 Edge를 통해 시스템을 제어하고, Edge가 자신의 로컬 런타임 상태와 Node registry를 운영하는 방향을 따른다.
현재 Control Plane은 실행 가능한 최소 서버 스캐폴드 단계다. Node를 직접 등록하거나 직접 스케줄링하는 계층으로 확정하지 않는다. Control Plane은 Edge를 통해 시스템을 제어하고, Edge가 자신의 로컬 런타임 상태와 Node registry를 운영하는 방향을 따른다.
IOP의 프론트엔드는 Control Plane 내부 UI가 아니라 별도 React + Next.js 기반 Web Portal로 둔다. Portal은 별도 Docker 컨테이너로 운영하며, 초기에는 Control Plane API를 중심으로 연동한다.
IOP의 프론트엔드는 Control Plane 내부 UI가 아니라 `apps/web`의 React + Next.js 기반 Web Portal로 둔다. Portal은 별도 Docker 컨테이너로 운영하며, Control Plane과의 주요 통신은 edge-node에서 사용 중인 protobuf-socket을 IOP Wire Protocol 기준으로 확장한다. `net/http`는 health/readiness/bootstrap 같은 보조 endpoint 용도로만 둔다.
## 현재 실행 표면
- `GET /healthz`
- `GET /readyz`
- protobuf-socket wire endpoint 예약: `server.wire_listen`
## 계획된 기능

View file

@ -1,12 +1,34 @@
package main
import (
"context"
"fmt"
"net/http"
"os"
"os/signal"
"time"
"github.com/spf13/cobra"
"go.uber.org/zap"
"gopkg.in/yaml.v3"
"iop/apps/control-plane/internal/wire"
"iop/packages/observability"
)
var cfgFile string
type controlPlaneConfig struct {
Server struct {
Listen string `yaml:"listen"`
WireListen string `yaml:"wire_listen"`
} `yaml:"server"`
Logging struct {
Level string `yaml:"level"`
Pretty bool `yaml:"pretty"`
} `yaml:"logging"`
}
func main() {
if err := rootCmd().Execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
@ -17,19 +39,106 @@ func main() {
func rootCmd() *cobra.Command {
root := &cobra.Command{
Use: "control-plane",
Short: "IOP Control Plane — multi-edge management placeholder",
Short: "IOP Control Plane - multi-edge management server",
Long: `control-plane will manage Edge connections, observe Edge state,
adjust Edge configuration, deliver commands to Edge, and receive Edge events.
This component is a placeholder. It should control the system through Edge,
not by connecting to or scheduling Node directly.`,
It controls the system through Edge, not by connecting to or scheduling Node
directly. The main IOP communication layer is protobuf-socket; net/http is kept
for health, readiness, and bootstrap endpoints.`,
}
root.AddCommand(&cobra.Command{
Use: "serve",
Short: "Start the control-plane server",
Run: func(_ *cobra.Command, _ []string) {
fmt.Println("control-plane serve: not yet implemented")
},
})
root.PersistentFlags().StringVarP(&cfgFile, "config", "c", "configs/control-plane.yaml", "config file path")
root.AddCommand(serveCmd())
return root
}
func serveCmd() *cobra.Command {
return &cobra.Command{
Use: "serve",
Short: "Start the control-plane server",
RunE: func(_ *cobra.Command, _ []string) error {
cfg, err := loadConfig(cfgFile)
if err != nil {
return fmt.Errorf("load config: %w", err)
}
logger, err := observability.NewLogger(cfg.Logging.Level, cfg.Logging.Pretty)
if err != nil {
return fmt.Errorf("logger: %w", err)
}
defer func() { _ = logger.Sync() }()
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()
return run(ctx, cfg, logger)
},
}
}
func defaultConfig() controlPlaneConfig {
var cfg controlPlaneConfig
cfg.Server.Listen = "0.0.0.0:9080"
cfg.Server.WireListen = "0.0.0.0:19080"
cfg.Logging.Level = "info"
return cfg
}
func loadConfig(path string) (controlPlaneConfig, error) {
cfg := defaultConfig()
if path == "" {
return cfg, nil
}
b, err := os.ReadFile(path)
if err != nil {
return cfg, err
}
if err := yaml.Unmarshal(b, &cfg); err != nil {
return cfg, err
}
return cfg, nil
}
func run(ctx context.Context, cfg controlPlaneConfig, logger *zap.Logger) error {
mux := http.NewServeMux()
mux.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("ok\n"))
})
mux.HandleFunc("/readyz", func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("ready\n"))
})
wireEndpoint := wire.Endpoint{Listen: cfg.Server.WireListen}
logger.Info("control-plane wire endpoint reserved",
zap.String("protocol", wire.Protocol),
zap.String("listen", wireEndpoint.Listen),
)
// TODO(control-plane): start the protobuf-socket listener here and use it
// for Portal-Control Plane and Control Plane-Edge message sessions.
// Keep net/http limited to health, readiness, and bootstrap endpoints.
server := &http.Server{
Addr: cfg.Server.Listen,
Handler: mux,
ReadHeaderTimeout: 5 * time.Second,
}
errCh := make(chan error, 1)
go func() {
logger.Info("control-plane http endpoint listening", zap.String("listen", cfg.Server.Listen))
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
errCh <- err
return
}
errCh <- nil
}()
select {
case <-ctx.Done():
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
return server.Shutdown(shutdownCtx)
case err := <-errCh:
return err
}
}

View file

@ -0,0 +1,13 @@
package wire
// Protocol names the IOP standard communication layer. The current Edge-Node
// transport already uses common-proto-socket; Control Plane endpoints should
// extend that same wire protocol instead of introducing another RPC framework.
const Protocol = "protobuf-socket"
// Endpoint is the reserved Control Plane wire endpoint configuration.
// TODO(control-plane): replace this scaffold with a protobuf-socket server
// once Portal-Control Plane and Control Plane-Edge message contracts settle.
type Endpoint struct {
Listen string
}

6
apps/web/.dockerignore Normal file
View file

@ -0,0 +1,6 @@
node_modules
.next
out
*.tsbuildinfo
.env*.local
npm-debug.log*

34
apps/web/Dockerfile Normal file
View file

@ -0,0 +1,34 @@
# syntax=docker/dockerfile:1
FROM node:22-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
FROM node:22-alpine AS builder
WORKDIR /app
ENV NEXT_TELEMETRY_DISABLED=1
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
ENV HOSTNAME=0.0.0.0
ENV PORT=3000
RUN addgroup -S iop && adduser -S iop -G iop
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
USER iop
EXPOSE 3000
CMD ["node", "server.js"]

20
apps/web/README.md Normal file
View file

@ -0,0 +1,20 @@
# web - IOP Web Portal
`apps/web`은 IOP 전체 Web Portal을 위한 Next.js 스캐폴드다. 현재는 root dummy 화면만 제공하며, `/nodes`, `/models`, `/jobs` 같은 도메인 페이지는 아직 만들지 않는다.
Control Plane과의 주요 통신은 edge-node에서 사용 중인 protobuf-socket을 IOP Wire Protocol 기준으로 확장한다. 현재 `src/lib/iop-client/`에는 TypeScript 클라이언트 연결 위치와 TODO만 둔다. 브라우저 직접 WebSocket transport가 확정되기 전까지는 Next.js server-side bridge가 필요할 수 있다.
## Local
```bash
npm install
npm run dev
```
## Docker
루트에서 실행한다.
```bash
docker compose up --build
```

21
apps/web/components.json Normal file
View file

@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}

6
apps/web/next-env.d.ts vendored Normal file
View file

@ -0,0 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

7
apps/web/next.config.ts Normal file
View file

@ -0,0 +1,7 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
output: "standalone",
};
export default nextConfig;

1808
apps/web/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

30
apps/web/package.json Normal file
View file

@ -0,0 +1,30 @@
{
"name": "@iop/web",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "node .next/standalone/server.js",
"check": "tsc --noEmit"
},
"dependencies": {
"@radix-ui/react-slot": "1.2.4",
"class-variance-authority": "0.7.1",
"clsx": "2.1.1",
"lucide-react": "1.16.0",
"next": "16.2.6",
"react": "19.2.6",
"react-dom": "19.2.6",
"tailwind-merge": "3.6.0"
},
"devDependencies": {
"@tailwindcss/postcss": "4.3.0",
"@types/node": "25.8.0",
"@types/react": "19.2.14",
"@types/react-dom": "19.2.3",
"tailwindcss": "4.3.0",
"typescript": "6.0.3"
}
}

View file

@ -0,0 +1,7 @@
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;

1
apps/web/public/.gitkeep Normal file
View file

@ -0,0 +1 @@

View file

@ -0,0 +1,86 @@
@import "tailwindcss";
@custom-variant dark (&:is(.dark *));
:root {
--radius: 0.5rem;
--background: oklch(0.985 0.004 106);
--foreground: oklch(0.18 0.015 250);
--card: oklch(1 0 0);
--card-foreground: oklch(0.18 0.015 250);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.18 0.015 250);
--primary: oklch(0.47 0.13 220);
--primary-foreground: oklch(0.99 0.005 210);
--secondary: oklch(0.92 0.016 190);
--secondary-foreground: oklch(0.23 0.026 230);
--muted: oklch(0.94 0.01 220);
--muted-foreground: oklch(0.46 0.025 235);
--accent: oklch(0.88 0.09 165);
--accent-foreground: oklch(0.2 0.04 175);
--destructive: oklch(0.57 0.22 25);
--border: oklch(0.87 0.012 230);
--input: oklch(0.87 0.012 230);
--ring: oklch(0.58 0.14 215);
}
.dark {
--background: oklch(0.16 0.017 245);
--foreground: oklch(0.96 0.008 220);
--card: oklch(0.2 0.02 245);
--card-foreground: oklch(0.96 0.008 220);
--popover: oklch(0.2 0.02 245);
--popover-foreground: oklch(0.96 0.008 220);
--primary: oklch(0.72 0.13 210);
--primary-foreground: oklch(0.17 0.02 245);
--secondary: oklch(0.27 0.025 235);
--secondary-foreground: oklch(0.94 0.008 220);
--muted: oklch(0.26 0.022 235);
--muted-foreground: oklch(0.72 0.025 225);
--accent: oklch(0.72 0.11 165);
--accent-foreground: oklch(0.17 0.02 245);
--destructive: oklch(0.66 0.2 25);
--border: oklch(1 0 0 / 12%);
--input: oklch(1 0 0 / 16%);
--ring: oklch(0.72 0.13 210);
}
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
}
* {
border-color: var(--border);
}
html {
background: var(--background);
}
body {
min-height: 100vh;
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
}

View file

@ -0,0 +1,20 @@
import type { Metadata } from "next";
import "./globals.css";
export const metadata: Metadata = {
title: "IOP Portal",
description: "IOP Web Portal scaffold",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}

96
apps/web/src/app/page.tsx Normal file
View file

@ -0,0 +1,96 @@
import { Activity, Cable, Server, ShieldCheck } from "lucide-react";
import { Button } from "@/components/ui/button";
import { getControlPlaneHealth, getIopClientConfig } from "@/lib/iop-client/client";
export const dynamic = "force-dynamic";
export default async function Home() {
const config = getIopClientConfig();
const health = await getControlPlaneHealth(config);
return (
<main className="min-h-screen bg-background">
<div className="mx-auto flex min-h-screen w-full max-w-6xl flex-col px-6 py-5">
<header className="flex items-center justify-between border-b border-border pb-4">
<div className="flex items-center gap-3">
<div className="flex size-9 items-center justify-center rounded-lg bg-primary text-primary-foreground">
<Activity className="size-5" aria-hidden="true" />
</div>
<div>
<p className="text-sm font-medium text-muted-foreground">Inference Operations Platform</p>
<h1 className="text-2xl font-semibold tracking-normal">IOP Portal</h1>
</div>
</div>
<Button variant="outline" size="sm">
<ShieldCheck className="size-4" aria-hidden="true" />
Scaffold
</Button>
</header>
<section className="grid flex-1 content-center gap-6 py-10 md:grid-cols-[1.35fr_0.65fr]">
<div className="space-y-6">
<div className="space-y-4">
<div className="inline-flex items-center gap-2 rounded-lg border border-border bg-card px-3 py-1.5 text-sm text-muted-foreground">
<span
className={`size-2 rounded-full ${health.ok ? "bg-emerald-500" : "bg-amber-500"}`}
aria-hidden="true"
/>
Control Plane {health.label}
</div>
<div className="max-w-3xl space-y-3">
<h2 className="text-4xl font-semibold tracking-normal md:text-5xl"> </h2>
<p className="text-lg leading-8 text-muted-foreground">
IOP Web Portal을 Next.js . .
</p>
</div>
</div>
<div className="grid gap-3 sm:grid-cols-3">
<StatusCard icon={Server} label="Server" value="Go Control Plane" />
<StatusCard icon={Cable} label="Wire" value="protobuf-socket" />
<StatusCard icon={Activity} label="Portal" value="Next.js" />
</div>
</div>
<aside className="self-center rounded-lg border border-border bg-card p-5 shadow-sm">
<div className="space-y-4">
<div>
<p className="text-sm font-medium text-muted-foreground">Control Plane HTTP</p>
<p className="mt-1 break-all text-sm font-semibold">{config.publicHttpUrl}</p>
</div>
<div>
<p className="text-sm font-medium text-muted-foreground">IOP Wire Endpoint</p>
<p className="mt-1 break-all text-sm font-semibold">{config.wireUrl}</p>
</div>
<div className="rounded-lg bg-secondary p-3 text-sm text-secondary-foreground">
<p className="font-medium">Connection scaffold</p>
<p className="mt-1 text-muted-foreground">
protobuf-socket TS client entrypoints live under <code>src/lib/iop-client</code>.
</p>
</div>
</div>
</aside>
</section>
</div>
</main>
);
}
function StatusCard({
icon: Icon,
label,
value,
}: {
icon: typeof Server;
label: string;
value: string;
}) {
return (
<div className="rounded-lg border border-border bg-card p-4 shadow-sm">
<Icon className="size-5 text-primary" aria-hidden="true" />
<p className="mt-4 text-sm text-muted-foreground">{label}</p>
<p className="mt-1 text-base font-semibold">{value}</p>
</div>
);
}

View file

@ -0,0 +1,45 @@
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex h-9 items-center justify-center gap-2 whitespace-nowrap rounded-lg px-4 py-2 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
outline: "border border-border bg-background hover:bg-secondary hover:text-secondary-foreground",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-secondary hover:text-secondary-foreground",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 px-3 text-sm",
icon: "size-9 px-0",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
function Button({
className,
variant,
size,
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean;
}) {
const Comp = asChild ? Slot : "button";
return <Comp data-slot="button" className={cn(buttonVariants({ variant, size, className }))} {...props} />;
}
export { Button, buttonVariants };

View file

@ -0,0 +1,24 @@
import type { IopClientConfig } from "./types";
export function getIopClientConfig(): IopClientConfig {
const publicHttpUrl = process.env.NEXT_PUBLIC_CONTROL_PLANE_HTTP_URL ?? "http://localhost:9080";
return {
publicHttpUrl,
internalHttpUrl: process.env.CONTROL_PLANE_INTERNAL_HTTP_URL ?? publicHttpUrl,
wireUrl: process.env.NEXT_PUBLIC_CONTROL_PLANE_WIRE_URL ?? "tcp://localhost:19080",
protocol: "protobuf-socket",
};
}
export async function getControlPlaneHealth(config: IopClientConfig): Promise<{ ok: boolean; label: string }> {
try {
const res = await fetch(`${config.internalHttpUrl}/healthz`, {
cache: "no-store",
signal: AbortSignal.timeout(1500),
});
return { ok: res.ok, label: res.ok ? "ready" : "unavailable" };
} catch {
return { ok: false, label: "pending" };
}
}

View file

@ -0,0 +1,10 @@
import type { IopConnection, IopConnectionOptions } from "./types";
export async function connectIopWire(options: IopConnectionOptions): Promise<IopConnection> {
void options;
// TODO(web): wire this to the TypeScript protobuf-socket client once the
// browser transport is confirmed. The current checked-in TS implementation
// uses Node.js TCP/ws helpers; if that remains true, add a Next.js
// server-side bridge and keep browser code talking to the bridge.
throw new Error("IOP protobuf-socket client is not connected yet.");
}

View file

@ -0,0 +1,21 @@
export type IopWireProtocol = "protobuf-socket";
export type IopWireTransport = "tcp" | "websocket" | "server-bridge";
export interface IopClientConfig {
publicHttpUrl: string;
internalHttpUrl: string;
wireUrl: string;
protocol: IopWireProtocol;
}
export interface IopConnectionOptions {
url: string;
transport: IopWireTransport;
}
export interface IopConnection {
protocol: IopWireProtocol;
transport: IopWireTransport;
close(): Promise<void>;
}

View 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));
}

33
apps/web/tsconfig.json Normal file
View file

@ -0,0 +1,33 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": false,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
"exclude": ["node_modules"]
}

View file

@ -1,11 +1,13 @@
server:
listen: "0.0.0.0:9080"
wire_listen: "0.0.0.0:19080"
tls:
enabled: false
logging:
level: "info"
pretty: true
metrics:
port: 9093

27
docker-compose.yml Normal file
View file

@ -0,0 +1,27 @@
services:
control-plane:
build:
context: ..
dockerfile: go-iop/apps/control-plane/Dockerfile
ports:
- "9080:9080"
- "19080:19080"
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:9080/healthz"]
interval: 10s
timeout: 3s
retries: 5
web:
build:
context: ./apps/web
dockerfile: Dockerfile
environment:
CONTROL_PLANE_INTERNAL_HTTP_URL: "http://control-plane:9080"
NEXT_PUBLIC_CONTROL_PLANE_HTTP_URL: "http://localhost:9080"
NEXT_PUBLIC_CONTROL_PLANE_WIRE_URL: "tcp://localhost:19080"
ports:
- "3000:3000"
depends_on:
control-plane:
condition: service_healthy