Task 001: dockerized monorepo foundation

This commit is contained in:
2026-05-21 16:57:58 +03:00
commit d42f0c4392
83 changed files with 4236 additions and 0 deletions
+15
View File
@@ -0,0 +1,15 @@
FROM python:3.12-slim
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
WORKDIR /app
COPY apps/backend/requirements.txt ./requirements.txt
RUN pip install --no-cache-dir -r requirements.txt
COPY apps/backend/src ./src
EXPOSE 8000
CMD ["uvicorn", "src.presentation.main:app", "--host", "0.0.0.0", "--port", "8000"]
+5
View File
@@ -0,0 +1,5 @@
fastapi==0.115.6
uvicorn[standard]==0.34.0
psycopg[binary]==3.2.3
redis==5.2.1
boto3==1.35.90
+1
View File
@@ -0,0 +1 @@
"""Backend service package."""
+3
View File
@@ -0,0 +1,3 @@
# Application
Application use cases and workflow orchestration belong in this layer.
+1
View File
@@ -0,0 +1 @@
"""Application layer for use cases and orchestration."""
+3
View File
@@ -0,0 +1,3 @@
# Domain
Business entities, value objects, and domain rules belong in this layer.
+1
View File
@@ -0,0 +1 @@
"""Domain layer for business concepts and invariants."""
@@ -0,0 +1,4 @@
# Infrastructure
Database, cache, object storage, queue, and other external adapters belong in
this layer.
@@ -0,0 +1 @@
"""Infrastructure adapters for external services."""
@@ -0,0 +1,74 @@
from __future__ import annotations
import os
from collections.abc import Callable
import boto3
import psycopg
from botocore.config import Config
from redis import Redis
DependencyStatus = dict[str, str]
def check_dependencies() -> DependencyStatus:
checks: dict[str, Callable[[], None]] = {
"postgres": _check_postgres,
"redis": _check_redis,
"object_storage": _check_object_storage,
}
results: DependencyStatus = {}
for name, check in checks.items():
try:
check()
except Exception:
results[name] = "error"
else:
results[name] = "ok"
return results
def _check_postgres() -> None:
dsn = _required_env("POSTGRES_DSN")
with psycopg.connect(dsn, connect_timeout=3) as connection:
with connection.cursor() as cursor:
cursor.execute("SELECT 1")
cursor.fetchone()
def _check_redis() -> None:
redis_url = _required_env("REDIS_URL")
client = Redis.from_url(redis_url, socket_connect_timeout=3, socket_timeout=3)
try:
if client.ping() is not True:
raise RuntimeError("Redis ping failed")
finally:
client.close()
def _check_object_storage() -> None:
endpoint = _required_env("OBJECT_STORAGE_ENDPOINT")
access_key_id = _required_env("OBJECT_STORAGE_ACCESS_KEY_ID")
secret_access_key = _required_env("OBJECT_STORAGE_SECRET_ACCESS_KEY")
bucket = _required_env("OBJECT_STORAGE_BUCKET")
region = os.environ.get("OBJECT_STORAGE_REGION", "us-east-1")
client = boto3.client(
"s3",
endpoint_url=endpoint,
aws_access_key_id=access_key_id,
aws_secret_access_key=secret_access_key,
region_name=region,
config=Config(signature_version="s3v4", connect_timeout=3, read_timeout=3),
)
client.head_bucket(Bucket=bucket)
def _required_env(name: str) -> str:
value = os.environ.get(name)
if not value:
raise RuntimeError(f"Missing required environment variable: {name}")
return value
+3
View File
@@ -0,0 +1,3 @@
# Presentation
HTTP routes, request/response DTOs, and API wiring belong in this layer.
@@ -0,0 +1 @@
"""Presentation layer for HTTP APIs."""
+27
View File
@@ -0,0 +1,27 @@
from fastapi import FastAPI
from fastapi.responses import JSONResponse
from src.infrastructure.dependencies import check_dependencies
app = FastAPI(title="AI Content Pipeline Backend")
@app.get("/health")
def health() -> dict[str, str]:
return {"service": "backend", "status": "ok"}
@app.get("/health/dependencies")
def dependency_health() -> JSONResponse:
dependencies = check_dependencies()
is_ok = all(status == "ok" for status in dependencies.values())
return JSONResponse(
status_code=200 if is_ok else 503,
content={
"service": "backend",
"status": "ok" if is_ok else "error",
"dependencies": dependencies,
},
)
+4
View File
@@ -0,0 +1,4 @@
# Shared
Cross-layer backend primitives belong here. Keep this layer small and avoid
placing domain rules in shared code.
+1
View File
@@ -0,0 +1 @@
"""Shared backend primitives."""
+17
View File
@@ -0,0 +1,17 @@
# Frontend Architecture
This app uses Next.js App Router in `src/app/` for routing. Feature-Sliced
Design layers live next to it under `src/`:
```text
src/app/
src/processes/
src/pages/
src/widgets/
src/features/
src/entities/
src/shared/
```
`src/pages` is an FSD page-composition layer. It is not the legacy Next.js Pages
Router; the App Router is `src/app`.
+12
View File
@@ -0,0 +1,12 @@
FROM node:22-alpine
WORKDIR /app
COPY apps/frontend/package.json ./package.json
RUN npm install
COPY apps/frontend ./
EXPOSE 3000
CMD ["npm", "run", "dev", "--", "--hostname", "0.0.0.0"]
+5
View File
@@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// This file is generated by Next.js and kept here so TypeScript works before
// the first local `next dev` run.
+6
View File
@@ -0,0 +1,6 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
};
export default nextConfig;
+23
View File
@@ -0,0 +1,23 @@
{
"name": "@pipeline/frontend",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
},
"dependencies": {
"next": "16.2.6",
"react": "19.0.0",
"react-dom": "19.0.0"
},
"devDependencies": {
"@types/node": "22.10.5",
"@types/react": "19.0.2",
"@types/react-dom": "19.0.2",
"typescript": "5.7.2"
},
"overrides": {
"postcss": "8.5.14"
}
}
+21
View File
@@ -0,0 +1,21 @@
:root {
color-scheme: light;
font-family:
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
sans-serif;
background: #f6f7f9;
color: #1d2733;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
}
main {
min-height: 100vh;
padding: 48px;
}
+5
View File
@@ -0,0 +1,5 @@
export const dynamic = "force-dynamic";
export async function GET() {
return Response.json({ service: "frontend", status: "ok" });
}
+19
View File
@@ -0,0 +1,19 @@
import "./globals.css";
import type { ReactNode } from "react";
export const metadata = {
title: "AI Content Pipeline",
description: "Internal editorial workflow foundation",
};
export default function RootLayout({
children,
}: Readonly<{
children: ReactNode;
}>) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
+8
View File
@@ -0,0 +1,8 @@
export default function HomePage() {
return (
<main>
<h1>AI Content Pipeline</h1>
<p>Dockerized monorepo foundation is running.</p>
</main>
);
}
+3
View File
@@ -0,0 +1,3 @@
# Entities
FSD layer for domain entity UI and client-side models.
+3
View File
@@ -0,0 +1,3 @@
# Features
FSD layer for user-facing actions and feature logic.
+4
View File
@@ -0,0 +1,4 @@
# Pages
FSD page-composition layer. This is separate from the Next.js App Router
`app/` route layer and does not enable the legacy Pages Router.
+4
View File
@@ -0,0 +1,4 @@
# Processes
FSD layer for cross-page user workflows. Placeholder for upcoming article
workflow processes.
+3
View File
@@ -0,0 +1,3 @@
# Shared
FSD layer for reusable frontend primitives, API clients, and UI foundations.
+3
View File
@@ -0,0 +1,3 @@
# Widgets
FSD layer for composed UI blocks used by pages.
+27
View File
@@ -0,0 +1,27 @@
{
"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": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
+12
View File
@@ -0,0 +1,12 @@
FROM python:3.12-slim
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
WORKDIR /app
COPY apps/runner/src ./src
EXPOSE 8010
CMD ["python", "-m", "src.presentation.main"]
View File
+1
View File
@@ -0,0 +1 @@
"""Runner service package."""
+3
View File
@@ -0,0 +1,3 @@
# Application
Runner use cases, job orchestration, and command policies belong in this layer.
+1
View File
@@ -0,0 +1 @@
"""Application layer for runner use cases."""
+3
View File
@@ -0,0 +1,3 @@
# Domain
Runner job entities and invariants belong in this layer.
+1
View File
@@ -0,0 +1 @@
"""Domain layer for runner job concepts."""
+4
View File
@@ -0,0 +1,4 @@
# Infrastructure
CLI execution, workspace, filesystem, and artifact storage adapters belong in
this layer.
@@ -0,0 +1 @@
"""Infrastructure adapters for runner execution."""
+3
View File
@@ -0,0 +1,3 @@
# Presentation
Runner HTTP routes and API wiring belong in this layer.
+1
View File
@@ -0,0 +1 @@
"""Presentation layer for runner HTTP APIs."""
+33
View File
@@ -0,0 +1,33 @@
from __future__ import annotations
import json
import os
from http import HTTPStatus
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
class HealthHandler(BaseHTTPRequestHandler):
def do_GET(self) -> None:
if self.path != "/health":
self.send_error(HTTPStatus.NOT_FOUND)
return
body = json.dumps({"service": "runner", "status": "ok"}).encode("utf-8")
self.send_response(HTTPStatus.OK)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def log_message(self, format: str, *args: object) -> None:
return
def run() -> None:
port = int(os.environ.get("PORT", "8010"))
server = ThreadingHTTPServer(("0.0.0.0", port), HealthHandler)
server.serve_forever()
if __name__ == "__main__":
run()
+3
View File
@@ -0,0 +1,3 @@
# Shared
Cross-layer runner primitives belong here. Keep this layer small.
+1
View File
@@ -0,0 +1 @@
"""Shared runner primitives."""