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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ on:

permissions:
contents: write
packages: write

jobs:
ci:
Expand Down Expand Up @@ -41,6 +42,45 @@ jobs:
- name: Test
run: npx turbo test

docker:
name: Publish Docker Image
needs: ci
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=raw,value=latest

- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
file: Dockerfile.aio
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

release:
name: Create GitHub Release
needs: ci
Expand Down
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,29 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.6.3] — 2026-04-14

### Added

- **GHCR Docker image publishing** — release workflow now builds `Dockerfile.aio` and pushes to `ghcr.io` on every version tag with semver tags (`x.y.z`, `x.y`) and `latest`

## [0.6.2] — 2026-04-14

### Added

- **All-in-One Docker image** — single-container deployment via `Dockerfile.aio` with Nginx reverse proxy, bundled PostgreSQL/SQLite, and MinIO; ideal for quick self-hosting and evaluation
- **SQLite support** — AiO container can run with SQLite instead of PostgreSQL for lightweight single-node deployments (`DATABASE_PROVIDER=sqlite`)
- **Agent streaming** — new `message:delta` WebSocket event forwards LLM token deltas from agent to frontend in real-time; gateway relays agent-emitted events to channel subscribers
- **Progress / thinking indicator** — new `message:progress` WebSocket event for ephemeral tool-hint and thinking status; frontend shows spinner + context text while agent processes
- **Agent status lifecycle** — agents now report `busy` / `active` / `error` status via `POST /api/v1/status/ping`; status transitions triggered automatically on message processing start, completion, and failure

### Changed

- `events.gateway.ts` — added `@SubscribeMessage('message:delta')` and `@SubscribeMessage('message:progress')` handlers that forward agent events to `channel:{id}` room subscribers
- `use-messages` hook — accumulates `streamingContent` from delta events, tracks `progress` state, auto-clears both on `message:created`
- `MessageList` — renders streaming bubble (partial response in real-time) and thinking indicator (spinner + tool hint text) below message list
- `ChatThreadPage` — passes `streamingContent` and `progress` props through to `MessageList`

## [0.6.1] — 2026-04-07

### Changed
Expand Down
123 changes: 123 additions & 0 deletions Dockerfile.aio
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# ─────────────────────────────────────────────────────────────────────────────
# Placet — All-in-One Image
# ─────────────────────────────────────────────────────────────────────────────
# Bundles nginx, backend, frontend, MCP server and MinIO into a single
# container behind a single port (8080).
#
# Supports both PostgreSQL (external) and SQLite (embedded, default for AIO).
# Set DB_PROVIDER=sqlite for a fully self-contained setup.
#
# Build: docker build -f Dockerfile.aio -t placet-aio .
# Run: docker compose -f docker-compose.aio.yml up
# ─────────────────────────────────────────────────────────────────────────────

# ── Base ──────────────────────────────────────────────────────────────────────
FROM node:22-alpine AS base
WORKDIR /app

# ── Install ALL workspace dependencies ────────────────────────────────────────
FROM base AS deps
COPY package.json package-lock.json ./
COPY apps/backend/package.json ./apps/backend/
COPY apps/frontend/package.json ./apps/frontend/
COPY packages/shared/package.json ./packages/shared/
COPY packages/mcp-server/package.json ./packages/mcp-server/
RUN npm ci

# ── Generate Prisma client ────────────────────────────────────────────────────
FROM deps AS prisma
COPY apps/backend/prisma ./apps/backend/prisma/
COPY apps/backend/prisma.config.ts ./apps/backend/
RUN cd apps/backend && npx prisma generate

# ── Build all workspaces ──────────────────────────────────────────────────────
FROM prisma AS build
COPY tsconfig.base.json ./
COPY packages/shared/ ./packages/shared/
COPY packages/plugins/ ./packages/plugins/
RUN npm run build --workspace=@placet/shared

# Backend
COPY apps/backend/ ./apps/backend/
RUN npm run build --workspace=@placet/backend

# Frontend
COPY apps/frontend/ ./apps/frontend/
ENV NEXT_TELEMETRY_DISABLED=1
RUN npm run build --workspace=@placet/frontend

# MCP server
COPY packages/mcp-server/ ./packages/mcp-server/
RUN npm run build --workspace=@placet/mcp

# ── All-in-One Runner ─────────────────────────────────────────────────────────
FROM base AS runner

ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1

# ── System packages (nginx + tini for proper PID 1) ────────────────────────
RUN apk add --no-cache nginx tini \
&& rm -rf /var/cache/apk/*

# ── Non-root user ──────────────────────────────────────────────────────────
RUN addgroup -g 1001 -S placet \
&& adduser -u 1001 -S placet -G placet -h /app -s /sbin/nologin

# ── MinIO binaries ─────────────────────────────────────────────────────────
COPY --from=minio/minio:latest /usr/bin/minio /usr/local/bin/minio
COPY --from=minio/mc:latest /usr/bin/mc /usr/local/bin/mc

# ── Backend (monorepo layout so Prisma + workspace symlinks resolve) ───────
COPY --from=build /app/package.json ./package.json
COPY --from=build /app/package-lock.json ./package-lock.json
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/packages/shared ./packages/shared
COPY --from=build /app/packages/plugins ./packages/plugins
COPY --from=build /app/apps/backend/dist ./apps/backend/dist
COPY --from=build /app/apps/backend/package.json ./apps/backend/
COPY --from=build /app/apps/backend/node_modules ./apps/backend/node_modules
COPY --from=build /app/apps/backend/prisma ./apps/backend/prisma
COPY --from=build /app/apps/backend/prisma.config.ts ./apps/backend/

# SQLite support — install into /tmp first, then merge to avoid npm pruning
RUN cd /tmp && npm init -y > /dev/null 2>&1 \
&& npm install --no-save \
@prisma/adapter-better-sqlite3@7.5.0 \
better-sqlite3 \
&& cp -r /tmp/node_modules/* /app/node_modules/ 2>/dev/null || true \
&& cp -r /tmp/node_modules/@prisma/* /app/node_modules/@prisma/ 2>/dev/null || true \
&& rm -rf /tmp/node_modules /tmp/package.json /tmp/package-lock.json

# ── Frontend (Next.js standalone) ──────────────────────────────────────────
COPY --from=build /app/apps/frontend/.next/standalone ./frontend/
COPY --from=build /app/apps/frontend/public ./frontend/apps/frontend/public
COPY --from=build /app/apps/frontend/.next/static ./frontend/apps/frontend/.next/static

# ── MCP server ─────────────────────────────────────────────────────────────
COPY --from=build /app/packages/mcp-server/dist ./packages/mcp-server/dist
COPY --from=build /app/packages/mcp-server/package.json ./packages/mcp-server/

# ── Nginx config ───────────────────────────────────────────────────────────
COPY nginx-aio.conf /etc/nginx/nginx.conf

# ── Entrypoint ─────────────────────────────────────────────────────────────
COPY docker-entrypoint-aio.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

# ── Data directories + permissions ─────────────────────────────────────────
RUN mkdir -p /data/minio /data/db /tmp/nginx-body /tmp/nginx-proxy \
/tmp/nginx-fastcgi /tmp/nginx-uwsgi /tmp/nginx-scgi /run/nginx \
&& chown -R placet:placet /app /data /tmp/nginx-* /run/nginx \
&& chmod 700 /data/db

VOLUME /data

# Single port for all traffic
EXPOSE 8080

HEALTHCHECK --interval=30s --timeout=5s --start-period=60s --retries=3 \
CMD node -e "const h=require('http');h.get('http://127.0.0.1:8080/health',r=>process.exit(r.statusCode===200?0:1)).on('error',()=>process.exit(1))"

# Use tini as PID 1 for proper signal handling
ENTRYPOINT ["/sbin/tini", "--", "/entrypoint.sh"]
46 changes: 46 additions & 0 deletions apps/backend/src/modules/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
import { createHash } from 'crypto';
import { hashPassword, verifyPassword } from '../../common/crypto';
import { PrismaService } from '../../prisma/prisma.service';

Expand All @@ -27,6 +28,7 @@ export class AuthService implements OnModuleInit {

async onModuleInit() {
await this.seedInitialUser();
await this.ensureApiKey();
}

private async seedInitialUser() {
Expand Down Expand Up @@ -56,6 +58,50 @@ export class AuthService implements OnModuleInit {
this.logger.log(`Initial owner user created: ${email}`);
}

private async ensureApiKey() {
const rawKey = this.config.get<string>('PLACET_API_KEY');
if (!rawKey) return;

if (!rawKey.startsWith('hp_') || rawKey.length < 10) {
this.logger.warn(
'PLACET_API_KEY is set but invalid (must start with hp_ and be >= 10 chars)',
);
return;
}

const keyHash = createHash('sha256').update(rawKey).digest('hex');
const existing = await this.prisma.apiKey.findUnique({
where: { keyHash },
});
if (existing) return;

const email = this.config.get<string>(
'INITIAL_USER_EMAIL',
'admin@placet.local',
);
const user = await this.prisma.user.findUnique({ where: { email } });
if (!user) {
this.logger.warn(
'PLACET_API_KEY set but owner user not found — skipping',
);
return;
}

const label = this.config.get<string>('PLACET_API_KEY_LABEL', 'Docker');
const keyPrefix = rawKey.substring(0, 11);

await this.prisma.apiKey.create({
data: {
userId: user.id,
label,
keyHash,
keyPrefix,
},
});

this.logger.log(`API key ${keyPrefix}... provisioned for ${email}`);
}

async login(email: string, password: string) {
const user = await this.prisma.user.findUnique({ where: { email } });
if (!user) {
Expand Down
25 changes: 25 additions & 0 deletions apps/backend/src/modules/events/events.gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,31 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect {
client.emit('pong');
}

// ── Agent-initiated events (forwarded to channel subscribers) ──

@SubscribeMessage('message:delta')
handleMessageDelta(
client: Socket,
data: {
channelId: string;
delta: string;
streamId?: string;
streamEnd?: boolean;
},
) {
if (!data?.channelId) return;
this.server.to(`channel:${data.channelId}`).emit('message:delta', data);
}

@SubscribeMessage('message:progress')
handleMessageProgress(
client: Socket,
data: { channelId: string; content: string; toolHint?: boolean },
) {
if (!data?.channelId) return;
this.server.to(`channel:${data.channelId}`).emit('message:progress', data);
}

// ── Server-side emit helpers ───────────────────────────────

emitToChannel(channelId: string, event: string, data: unknown) {
Expand Down
Loading
Loading