Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
12 changes: 12 additions & 0 deletions .workflows/build/compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
# ci - Lint + unit tests + build (gate before packaging)
# api - Production API image
# website - Production Website image (nginx + static)
# dashboard - Production Dashboard image (nginx + SPA)
# worker - Production Worker image (background job processor)
#
# Usage:
Expand Down Expand Up @@ -35,6 +36,17 @@ services:
args:
- VITE_WEBSITE_BASE_URL=${VITE_WEBSITE_BASE_URL:-}
- VITE_API_BASE_URL=${VITE_API_BASE_URL:-}
- VITE_DASHBOARD_BASE_URL=${VITE_DASHBOARD_BASE_URL:-}

dashboard:
image: arrhes-dashboard:${ARRHES_VERSION:-dev}
build:
context: ../..
dockerfile: .workflows/build/packages/dashboard/Dockerfile
args:
- VITE_API_BASE_URL=${VITE_API_BASE_URL:-}
- VITE_WEBSITE_BASE_URL=${VITE_WEBSITE_BASE_URL:-}


worker:
image: arrhes-worker:${ARRHES_VERSION:-dev}
Expand Down
4 changes: 2 additions & 2 deletions .workflows/build/packages/ci/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ RUN pnpm install
# Build all packages (must run before tests so workspace deps are compiled)
RUN pnpm run build

# Auto-format, then run Biome check (lint)
RUN pnpm format:fix && pnpm check
# Auto-fix formatting + imports, then run Biome check (lint)
RUN pnpm check:fix && pnpm check

# Run unit tests
RUN pnpm --recursive --if-present --filter='./packages/**' run test:unit
17 changes: 17 additions & 0 deletions .workflows/build/packages/dashboard/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Include any files or directories that you don't want to be copied to your
# container here (e.g., local build artifacts, temporary files, etc.).
#
# For more help, visit the .dockerignore file reference guide at
# https://docs.docker.com/go/build-context-dockerignore/

**/.dockerignore
**/.env
**/.vscode
**/.git
**/.gitignore
**/docker-compose*
**/compose*
**/Dockerfile*
**/node_modules
**/build
README.md
32 changes: 32 additions & 0 deletions .workflows/build/packages/dashboard/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Use an official Node.js image to build our image from
FROM node:25.2.1-alpine AS base
RUN npm install -g pnpm@10.26.1
ENV NODE_OPTIONS="--max-old-space-size=4096"

# Build the repo
FROM base AS build

# Build arguments for Vite environment variables
ARG VITE_API_BASE_URL
ARG VITE_WEBSITE_BASE_URL

WORKDIR /root
COPY . .
RUN pnpm install

# Write VITE_* build args to .env so Vite can read them during build.
# Vite reads import.meta.env from .env files, not from process.env.
RUN printf "VITE_API_BASE_URL=%s\nVITE_WEBSITE_BASE_URL=%s\n" \
"$VITE_API_BASE_URL" "$VITE_WEBSITE_BASE_URL" \
> packages/dashboard/.env

RUN pnpm --filter @arrhes/application-dashboard... run build

# Start application
FROM nginx:alpine AS deploy
WORKDIR /
COPY .workflows/build/packages/dashboard/nginx/default.conf /etc/nginx/nginx.conf
RUN rm -rf /usr/share/nginx/html/*
COPY --from=build /root/packages/dashboard/build /usr/share/nginx/html
EXPOSE 80
CMD ["nginx"]
40 changes: 40 additions & 0 deletions .workflows/build/packages/dashboard/nginx/default.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# default.conf
worker_processes auto;

daemon off;

events {
worker_connections 1024;
}

http {
include mime.types;

server {
listen 80;

gzip on;
gzip_http_version 1.1;
gzip_disable "MSIE6";
gzip_min_length 256;
gzip_vary on;
gzip_proxied any;
gzip_types text/plain text/css application/json application/javascript application/x-javascript text/xml application/xml application/xml+rss text/javascript font/woff2;
gzip_comp_level 9;

root /usr/share/nginx/html;
index index.html;

# Immutable hashed assets (JS, CSS, fonts, images) - cache for 1 year
location ~* \.(?:js|css|woff2?|ttf|eot|svg|png|jpg|jpeg|gif|webp|webmanifest)$ {
add_header Cache-Control "public, max-age=31536000, immutable";
try_files $uri =404;
}

# SPA fallback - all routes served by index.html
location / {
add_header Cache-Control "no-cache";
try_files $uri $uri/ /index.html =404;
}
}
}
12 changes: 10 additions & 2 deletions .workflows/build/packages/website/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,32 @@ FROM base AS build
# Build arguments for Vite environment variables
ARG VITE_WEBSITE_BASE_URL
ARG VITE_API_BASE_URL
ARG VITE_DASHBOARD_BASE_URL

WORKDIR /root
COPY . .
RUN pnpm install

# Write VITE_* build args to .env so Vite can read them during build.
# Vite reads import.meta.env from .env files, not from process.env.
RUN printf "VITE_WEBSITE_BASE_URL=%s\nVITE_API_BASE_URL=%s\n" \
"$VITE_WEBSITE_BASE_URL" "$VITE_API_BASE_URL" \
RUN printf "VITE_WEBSITE_BASE_URL=%s\nVITE_API_BASE_URL=%s\nVITE_DASHBOARD_BASE_URL=%s\n" \
"$VITE_WEBSITE_BASE_URL" "$VITE_API_BASE_URL" "$VITE_DASHBOARD_BASE_URL" \
> packages/website/.env

RUN pnpm --filter @arrhes/application-website... run build

# Stamp CLI files from VERSION
RUN VER=$(cat VERSION | tr -d 'v[:space:]') && \
sed -i "s/^VERSION=\".*\"/VERSION=\"$VER\"/" packages/cli/arrhes.sh && \
printf '%s\n' "$VER" > packages/cli/version

# Start application
FROM nginx:alpine AS deploy
WORKDIR /
COPY .workflows/build/packages/website/nginx/default.conf /etc/nginx/nginx.conf
RUN rm -rf /usr/share/nginx/html/*
COPY --from=build /root/packages/website/build /usr/share/nginx/html
COPY --from=build /root/packages/cli/install.sh /usr/share/nginx/html/cli/install.sh
COPY --from=build /root/packages/cli/version /usr/share/nginx/html/cli/version
EXPOSE 80
CMD ["nginx"]
13 changes: 12 additions & 1 deletion .workflows/build/packages/website/nginx/default.conf
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ http {
location = /og.png { add_header Cache-Control "public, max-age=86400"; try_files $uri =404; }
location = /og.webp { add_header Cache-Control "public, max-age=86400"; try_files $uri =404; }

# CLI install scripts and version file - serve as plain text, no caching
location ~* ^/cli/(.*\.sh|version)$ {
add_header Cache-Control "no-cache";
add_header Content-Type "text/plain; charset=utf-8";
try_files $uri =404;
}

# Immutable hashed assets (JS, CSS, fonts, images) - cache for 1 year
location ~* \.(?:js|css|woff2?|ttf|eot|svg|png|jpg|jpeg|gif|webp|webmanifest)$ {
add_header Cache-Control "public, max-age=31536000, immutable";
Expand All @@ -39,7 +46,11 @@ http {
# HTML and other mutable files - always revalidate
location / {
add_header Cache-Control "no-cache";
try_files $uri $uri/ /index.html =404;
# Use a blank SPA shell (__app.html) as the fallback for routes that
# have no prerendered file (e.g. /dashboard). index.html contains
# the prerendered home page and must not be served as a catch-all,
# otherwise it flashes on every non-home route refresh.
try_files $uri $uri/ /__app.html =404;
}
}
}
29 changes: 28 additions & 1 deletion .workflows/dev/compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -128,11 +128,12 @@ services:
ENV: development
VERBOSE: "true"
PORT: "${API_HOST_PORT}"
CORS_ORIGIN: "http://localhost:${WEBSITE_HOST_PORT}"
CORS_ORIGIN: localhost
COOKIES_DOMAIN: localhost
COOKIES_KEY: development-secret-key-change-in-production-min-32-chars
API_BASE_URL: "http://localhost:${API_HOST_PORT}"
WEBSITE_BASE_URL: "http://localhost:${WEBSITE_HOST_PORT}"
DASHBOARD_BASE_URL: "http://localhost:${DASHBOARD_HOST_PORT}"
SQL_DATABASE_URL: "postgres://postgres:admin@localhost:${POSTGRES_HOST_PORT}/default"
STORAGE_ENDPOINT: "http://localhost:${STORAGE_HOST_PORT}"
STORAGE_BUCKET_NAME: arrhes-files
Expand Down Expand Up @@ -192,6 +193,32 @@ services:
VITE_ENV: development
VITE_API_BASE_URL: "http://localhost:${API_HOST_PORT}"
VITE_WEBSITE_BASE_URL: "http://localhost:${WEBSITE_HOST_PORT}"
VITE_DASHBOARD_BASE_URL: "http://localhost:${DASHBOARD_HOST_PORT}"

# Dashboard - SPA dashboard interface (React + Vite)
dashboard:
container_name: arrhes-dashboard
image: arrhes-dev-dashboard:latest
build:
context: ../..
dockerfile: .workflows/dev/packages/dashboard/Dockerfile
args:
NODE_VERSION: "25.2.1"
PNPM_VERSION: "10.26.1"
networks:
default:
aliases:
- dashboard.arrhes.localhost
volumes:
- ../..:/workspace:cached
ports:
- "127.0.0.1:${DASHBOARD_HOST_PORT}:5174"
environment:
# Vite reads VITE_* from .env files, not process.env.
# The entrypoint writes these to packages/dashboard/.env at startup.
VITE_ENV: development
VITE_API_BASE_URL: "http://localhost:${API_HOST_PORT}"
VITE_WEBSITE_BASE_URL: "http://localhost:${WEBSITE_HOST_PORT}"

# Worker - Background job processor (Bull queue + Redis pub/sub)
worker:
Expand Down
25 changes: 25 additions & 0 deletions .workflows/dev/packages/dashboard/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# ==============================================================================
# Dashboard Development Dockerfile
# ==============================================================================
# Minimal Node.js + PNPM image for running the dashboard dev server.
# The full workspace (including node_modules) is bind-mounted at runtime.
# Dependencies must be installed on the host before starting.
#
# Environment variables are set in compose.yml and written to .env at startup
# so Vite can read them (Vite reads VITE_* from .env files, not process.env).
# ==============================================================================

ARG NODE_VERSION="25.2.1"
ARG PNPM_VERSION="10.26.1"

FROM node:${NODE_VERSION}-bullseye-slim

ARG PNPM_VERSION

# Install PNPM
RUN npm install -g "pnpm@${PNPM_VERSION}"

WORKDIR /workspace/packages/dashboard

# Generate .env from compose environment variables, run Panda codegen, then start Vite
CMD ["sh", "-c", "printenv | grep '^VITE_' > .env && cd /workspace/packages/ui && pnpm panda codegen && cd /workspace/packages/dashboard && pnpm panda codegen && pnpm dev -- --host 0.0.0.0"]
5 changes: 5 additions & 0 deletions .workflows/dev/up.sh
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ mailpit_ui_host_port=$(_allocate_port_for_key MAILPIT_UI_HOST_PORT)
mailpit_smtp_host_port=$(_allocate_port_for_key MAILPIT_SMTP_HOST_PORT)
postgres_host_port=$(_allocate_port_for_key POSTGRES_HOST_PORT)
redis_host_port=$(_allocate_port_for_key REDIS_HOST_PORT)
dashboard_host_port=$(_allocate_port_for_key DASHBOARD_HOST_PORT)

cat > "$PORTS_FILE" <<EOF
WEBSITE_HOST_PORT=$website_host_port
Expand All @@ -82,6 +83,7 @@ MAILPIT_UI_HOST_PORT=$mailpit_ui_host_port
MAILPIT_SMTP_HOST_PORT=$mailpit_smtp_host_port
POSTGRES_HOST_PORT=$postgres_host_port
REDIS_HOST_PORT=$redis_host_port
DASHBOARD_HOST_PORT=$dashboard_host_port
EOF

if ! WEBSITE_HOST_PORT="$website_host_port" \
Expand All @@ -92,6 +94,7 @@ if ! WEBSITE_HOST_PORT="$website_host_port" \
MAILPIT_SMTP_HOST_PORT="$mailpit_smtp_host_port" \
POSTGRES_HOST_PORT="$postgres_host_port" \
REDIS_HOST_PORT="$redis_host_port" \
DASHBOARD_HOST_PORT="$dashboard_host_port" \
"${DC[@]}" up --detach --build --force-recreate --wait; then
echo ""
echo "=============================================="
Expand All @@ -113,6 +116,7 @@ echo " Arrhes Development Environment"
echo "=============================================="
echo ""
echo " Services:"
echo " Dashboard: http://localhost:$dashboard_host_port"
echo " Website: http://localhost:$website_host_port"
echo " API: http://localhost:$api_host_port"
echo ""
Expand All @@ -134,3 +138,4 @@ echo " Password: admin"
echo ""
echo " Logs: docker compose -f $COMPOSE_FILE logs -f"
echo "=============================================="
echo ""
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v1.3.4
v1.3.5
7 changes: 7 additions & 0 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,13 @@ COMPOSE_START := "docker compose -f .workflows/build/compose.start.yml --project
build cmd:
@just build-{{cmd}}

# Stamp packages/cli/arrhes.sh and packages/cli/version from the VERSION file
build-cli:
@VER=$(cat VERSION | tr -d 'v[:space:]') && \
sed -i "s/^VERSION=\".*\"/VERSION=\"$VER\"/" packages/cli/arrhes.sh && \
printf '%s\n' "$VER" > packages/cli/version && \
echo "CLI stamped: $VER"

# Run CI gate: lint + typecheck + unit tests + build
build-ci:
@echo "=============================================="
Expand Down
3 changes: 0 additions & 3 deletions packages/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,12 @@
"@aws-sdk/s3-request-presigner": "3.1043.0",
"@mollie/api-client": "4.5.0",
"@tanstack/ai": "0.14.0",
"@tanstack/ai-ollama": "0.6.10",
"@tanstack/ai-openai": "0.8.2",
"@valibot/to-json-schema": "1.7.0",
"bull": "4.16.5",
"drizzle-orm": "0.45.2",
"hono": "4.12.18",
"ioredis": "5.10.1",
"nodemailer": "8.0.7",
"openai": "6.36.0",
"postgres": "3.4.9",
"valibot": "1.4.0"
}
Expand Down
10 changes: 9 additions & 1 deletion packages/api/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,16 @@ export async function api(parameters: {

// Set CORS
.use("/*", async (c, next) => {
const allowedDomain = c.var.env.CORS_ORIGIN
const corsMiddlewareHandler = cors({
origin: c.var.env.CORS_ORIGIN.split(",") ?? "*",
origin: (origin) => {
if (!origin) return null
const host = origin.replace(/^https?:\/\//, "").split(":")[0]
if (host === allowedDomain || host.endsWith(`.${allowedDomain}`)) {
return origin
}
return null
},
allowHeaders: [
"Content-Type",
"Authorization",
Expand Down
31 changes: 30 additions & 1 deletion packages/api/src/middlewares/validateBody.middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,36 @@ export async function validateBodyMiddleware<TSchema extends v.GenericSchema<unk
schema: TSchema
}) {
try {
const rawBody = await parameters.context.req.json()
let rawBody: Record<string, unknown>
if (parameters.context.req.method === "GET") {
const queries = parameters.context.req.queries()
rawBody = {}
for (const [key, values] of Object.entries(queries)) {
rawBody[key] = values.length === 1 ? values[0] : values
}
} else {
const contentLength = parameters.context.req.header("content-length")
const hasBody =
contentLength !== undefined ? Number(contentLength) > 0 : parameters.context.req.raw.body !== null
try {
rawBody = hasBody ? await parameters.context.req.json() : {}
} catch {
// Body was declared but is empty or could not be parsed
// (e.g. DELETE requests with Content-Type: application/json but no body).
// Fall back to empty object; schema validation will reject truly missing fields.
rawBody = {}
}
}

// Merge URL path params as fallback values (body/query already-set values take precedence).
// This supports REST-style clients that place idXxx only in the URL path.
const pathParams = parameters.context.req.param()
for (const [key, value] of Object.entries(pathParams)) {
if (!(key in rawBody)) {
rawBody[key] = value
}
}

const validatedBody = validate({
schema: parameters.schema,
data: rawBody,
Expand Down
Loading
Loading