feat(build): embed web UI in backend binary + simplify Docker variants (#620)

- Add internal/webui/ package with //go:build embedui tag for optional
  SPA embedding (handler.go serves static files with SPA fallback)
- Add internal/version/ shared semver comparison (DRY: extracted from
  gateway/update_check.go and updater/updater.go)
- Enhance UpdateChecker: release notes, ETag caching, filter lite-v* tags
- Add web UI build stage to Dockerfile with ENABLE_EMBEDUI build arg
- Simplify CI: 7 Docker variants → 4 (base, latest, full, otel)
- Add SHA256 checksums job to release workflow
- Add Makefile build-full target (embeds web UI in Go binary)
- Default make up now embeds web UI (no separate nginx needed)
- Add WITH_WEB_NGINX=1 flag for optional nginx reverse proxy
- Update README + 30 translated READMEs: make up, port 18790
- Update docker-compose comments and prepare-env.sh
- About dialog: show release notes with markdown rendering
- Health card: amber badge for available updates

BREAKING: Default Docker setup no longer requires selfservice overlay.
Web dashboard served at :18790 (same port as API).
This commit is contained in:
Viet Tran authored and GitHub committed 2026-04-01 15:25:59 +07:00
1 parent 39bb90bd60
commit 52c67d6d92
56 files changed
+531 -381

No files matched your search

+3 -1
View File
@@ -16,6 +16,8 @@ tmp/
.claude/
.vscode/
.idea/
ui/
ui/desktop/
ui/simple-saas/
!ui/web/
plans/
skills-store/
+15 -47
View File
@@ -19,63 +19,33 @@ jobs:
strategy:
matrix:
include:
# ── Runtime variants ──
# Base: backend API-only, no web UI, no runtimes
- variant: base
suffix: "-base"
enable_otel: "false"
enable_embedui: "false"
enable_python: "false"
enable_full_skills: "false"
# Latest: backend + embedded web UI + Python
- variant: latest
suffix: ""
enable_otel: "false"
enable_tsnet: "false"
enable_redis: "false"
enable_node: "false"
enable_python: "false"
enable_full_skills: "false"
- variant: node
suffix: "-node"
enable_otel: "false"
enable_tsnet: "false"
enable_redis: "false"
enable_node: "true"
enable_python: "false"
enable_full_skills: "false"
- variant: python
suffix: "-python"
enable_otel: "false"
enable_tsnet: "false"
enable_redis: "false"
enable_node: "false"
enable_embedui: "true"
enable_python: "true"
enable_full_skills: "false"
# Full: all runtimes + skills pre-installed
- variant: full
suffix: "-full"
enable_otel: "false"
enable_tsnet: "false"
enable_redis: "false"
enable_node: "true"
enable_embedui: "true"
enable_python: "true"
enable_full_skills: "true"
# ── Build-tag variants ──
# OTel: latest + OpenTelemetry tracing
- variant: otel
suffix: "-otel"
enable_otel: "true"
enable_tsnet: "false"
enable_redis: "false"
enable_node: "false"
enable_python: "false"
enable_full_skills: "false"
- variant: tsnet
suffix: "-tsnet"
enable_otel: "false"
enable_tsnet: "true"
enable_redis: "false"
enable_node: "false"
enable_python: "false"
enable_full_skills: "false"
- variant: redis
suffix: "-redis"
enable_otel: "false"
enable_tsnet: "false"
enable_redis: "true"
enable_node: "false"
enable_python: "false"
enable_embedui: "true"
enable_python: "true"
enable_full_skills: "false"
steps:
@@ -124,9 +94,7 @@ jobs:
labels: ${{ steps.meta.outputs.labels }}
build-args: |
ENABLE_OTEL=${{ matrix.enable_otel }}
ENABLE_TSNET=${{ matrix.enable_tsnet }}
ENABLE_REDIS=${{ matrix.enable_redis }}
ENABLE_NODE=${{ matrix.enable_node }}
ENABLE_EMBEDUI=${{ matrix.enable_embedui }}
ENABLE_PYTHON=${{ matrix.enable_python }}
ENABLE_FULL_SKILLS=${{ matrix.enable_full_skills }}
VERSION=${{ github.ref_name }}
+59 -46
View File
@@ -53,13 +53,24 @@ jobs:
go-version-file: go.mod
cache-dependency-path: go.sum
- uses: actions/setup-node@v4
with:
node-version: 22
- name: Build web UI
run: |
corepack enable && corepack prepare pnpm@10.28.2 --activate
cd ui/web && pnpm install --frozen-lockfile && pnpm build && cd ../..
mkdir -p internal/webui/dist
cp -r ui/web/dist/* internal/webui/dist/
- name: Build binary
env:
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
VERSION: v${{ needs.release.outputs.version }}
run: |
CGO_ENABLED=0 go build \
CGO_ENABLED=0 go build -tags embedui \
-ldflags="-s -w -X github.com/nextlevelbuilder/goclaw/cmd.Version=${VERSION}" \
-o goclaw .
tar -czf "goclaw-${{ needs.release.outputs.version }}-${{ matrix.goos }}-${{ matrix.goarch }}.tar.gz" goclaw migrations/
@@ -72,6 +83,38 @@ jobs:
"goclaw-${{ needs.release.outputs.version }}-${{ matrix.goos }}-${{ matrix.goarch }}.tar.gz" \
--clobber
# Generate SHA256 checksums for all binary assets
checksums:
needs: [release, build-binaries]
if: needs.release.outputs.released == 'true'
runs-on: ubuntu-latest
steps:
- name: Download all binary assets
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
mkdir -p assets
gh release download "v${{ needs.release.outputs.version }}" \
--repo "${{ github.repository }}" \
--pattern "goclaw-*.tar.gz" \
--dir assets
- name: Generate checksums
run: |
cd assets
sha256sum goclaw-*.tar.gz > CHECKSUMS.sha256
echo "Generated checksums:"
cat CHECKSUMS.sha256
- name: Upload checksums to release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release upload "v${{ needs.release.outputs.version }}" \
assets/CHECKSUMS.sha256 \
--repo "${{ github.repository }}" \
--clobber
# Build and push Docker images to GHCR + Docker Hub
docker-images:
needs: release
@@ -80,61 +123,33 @@ jobs:
strategy:
matrix:
include:
# Base: backend API-only, no web UI, no runtimes
- variant: base
suffix: "-base"
enable_otel: "false"
enable_embedui: "false"
enable_python: "false"
enable_full_skills: "false"
# Latest: backend + embedded web UI + Python
- variant: latest
suffix: ""
enable_otel: "false"
enable_tsnet: "false"
enable_redis: "false"
enable_node: "false"
enable_python: "false"
enable_full_skills: "false"
- variant: node
suffix: "-node"
enable_otel: "false"
enable_tsnet: "false"
enable_redis: "false"
enable_node: "true"
enable_python: "false"
enable_full_skills: "false"
- variant: python
suffix: "-python"
enable_otel: "false"
enable_tsnet: "false"
enable_redis: "false"
enable_node: "false"
enable_embedui: "true"
enable_python: "true"
enable_full_skills: "false"
# Full: all runtimes + skills pre-installed
- variant: full
suffix: "-full"
enable_otel: "false"
enable_tsnet: "false"
enable_redis: "false"
enable_node: "true"
enable_embedui: "true"
enable_python: "true"
enable_full_skills: "true"
# OTel: latest + OpenTelemetry tracing
- variant: otel
suffix: "-otel"
enable_otel: "true"
enable_tsnet: "false"
enable_redis: "false"
enable_node: "false"
enable_python: "false"
enable_full_skills: "false"
- variant: tsnet
suffix: "-tsnet"
enable_otel: "false"
enable_tsnet: "true"
enable_redis: "false"
enable_node: "false"
enable_python: "false"
enable_full_skills: "false"
- variant: redis
suffix: "-redis"
enable_otel: "false"
enable_tsnet: "false"
enable_redis: "true"
enable_node: "false"
enable_python: "false"
enable_embedui: "true"
enable_python: "true"
enable_full_skills: "false"
steps:
- uses: actions/checkout@v4
@@ -178,9 +193,7 @@ jobs:
labels: ${{ steps.meta.outputs.labels }}
build-args: |
ENABLE_OTEL=${{ matrix.enable_otel }}
ENABLE_TSNET=${{ matrix.enable_tsnet }}
ENABLE_REDIS=${{ matrix.enable_redis }}
ENABLE_NODE=${{ matrix.enable_node }}
ENABLE_EMBEDUI=${{ matrix.enable_embedui }}
ENABLE_PYTHON=${{ matrix.enable_python }}
ENABLE_FULL_SKILLS=${{ matrix.enable_full_skills }}
VERSION=v${{ needs.release.outputs.version }}
+1 -1
View File
@@ -75,7 +75,7 @@ goclaw.db-shm
.opencode/
# Auto-generated by Makefile (git describe)
VERSION
/VERSION
ui/desktop/frontend/dist/index.html
# Local compose modules
+18 -2
View File
@@ -1,6 +1,15 @@
# syntax=docker/dockerfile:1
# ── Stage 1: Build ──
# ── Stage 0: Build Web UI (conditional) ──
FROM node:22-alpine AS web-builder
RUN corepack enable && corepack prepare pnpm@10.28.2 --activate
WORKDIR /app
COPY ui/web/package.json ui/web/pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
COPY ui/web/ .
RUN pnpm build
# ── Stage 1: Build Go ──
FROM golang:1.26-bookworm AS builder
WORKDIR /src
@@ -16,15 +25,22 @@ COPY . .
ARG ENABLE_OTEL=false
ARG ENABLE_TSNET=false
ARG ENABLE_REDIS=false
ARG ENABLE_EMBEDUI=false
ARG VERSION=
# Copy web UI dist for embedding (only used when ENABLE_EMBEDUI=true)
COPY --from=web-builder /app/dist /src/internal/webui/dist
# Build static binary (CGO disabled for scratch/alpine compatibility)
# Version: build arg > VERSION file (auto-generated by Makefile) > "dev"
RUN set -eux; \
if [ -z "$VERSION" ] && [ -f VERSION ]; then VERSION=$(cat VERSION); fi; \
if [ -z "$VERSION" ]; then VERSION="dev"; fi; \
TAGS=""; \
if [ "$ENABLE_OTEL" = "true" ]; then TAGS="otel"; fi; \
if [ "$ENABLE_EMBEDUI" = "true" ]; then TAGS="embedui"; fi; \
if [ "$ENABLE_OTEL" = "true" ]; then \
if [ -n "$TAGS" ]; then TAGS="$TAGS,otel"; else TAGS="otel"; fi; \
fi; \
if [ "$ENABLE_TSNET" = "true" ]; then \
if [ -n "$TAGS" ]; then TAGS="$TAGS,tsnet"; else TAGS="tsnet"; fi; \
fi; \
+18 -3
View File
@@ -2,21 +2,36 @@ VERSION ?= $(shell git describe --tags --abbrev=0 --match "v[0-9]*" 2>/dev/null
LDFLAGS = -s -w -X github.com/nextlevelbuilder/goclaw/cmd.Version=$(VERSION)
BINARY = goclaw
.PHONY: build run clean version up down logs reset test vet check-web dev migrate setup ci desktop-dev desktop-build desktop-dmg
.PHONY: build build-full run clean version up down logs reset test vet check-web dev migrate setup ci desktop-dev desktop-build desktop-dmg
# Build backend only (API-only, no embedded web UI)
build:
CGO_ENABLED=0 go build -ldflags="$(LDFLAGS)" -o $(BINARY) .
# Build with embedded web UI (recommended for production)
build-full: check-web
rm -rf internal/webui/dist && mkdir -p internal/webui/dist
cp -r ui/web/dist/* internal/webui/dist/
CGO_ENABLED=0 go build -tags embedui -ldflags="$(LDFLAGS)" -o $(BINARY) .
run: build
./$(BINARY)
clean:
rm -f $(BINARY)
rm -rf internal/webui/dist
version:
@echo $(VERSION)
COMPOSE_BASE = docker compose -f docker-compose.yml -f docker-compose.postgres.yml -f docker-compose.selfservice.yml
# ── Docker Compose ──
# Default: backend (with embedded web UI) + Postgres. No separate nginx needed.
# Add WITH_WEB_NGINX=1 for separate nginx on :3000 (custom SSL, reverse proxy).
COMPOSE_BASE = docker compose -f docker-compose.yml -f docker-compose.postgres.yml
ifdef WITH_WEB_NGINX
COMPOSE_BASE += -f docker-compose.selfservice.yml
export ENABLE_EMBEDUI=false
endif
COMPOSE_EXTRA =
ifdef WITH_BROWSER
COMPOSE_EXTRA += -f docker-compose.browser.yml
@@ -43,7 +58,7 @@ version-file:
@echo $(VERSION) > VERSION
up: version-file
$(COMPOSE) up -d --build
GOCLAW_VERSION=$(VERSION) $(COMPOSE) up -d --build
$(UPGRADE) run --rm upgrade
down:
+29 -3
View File
@@ -185,8 +185,11 @@ chmod +x prepare-env.sh && ./prepare-env.sh
# Add at least one GOCLAW_*_API_KEY to .env, then:
make up
# Web Dashboard at http://localhost:3000
# Web Dashboard at http://localhost:18790 (built-in)
# Health check: curl http://localhost:18790/health
# Optional: separate nginx for custom SSL/reverse proxy
# make up WITH_WEB_NGINX=1 → Dashboard at http://localhost:3000
```
`make up` creates a Docker network, embeds the correct version from git tags, builds and starts all services, and runs database migrations automatically.
@@ -222,9 +225,32 @@ make down WITH_BROWSER=1 WITH_OTEL=1
When `GOCLAW_*_API_KEY` environment variables are set, the gateway auto-onboards without interactive prompts — detects provider, runs migrations, and seeds default data.
> `ghcr.io/nextlevelbuilder/goclaw:latest` is the minimal runtime image and does **not** include Python or Node.js by default. If you need those runtimes preinstalled, use the `python`, `node`, or `full` image variant, run `./scripts/setup-docker.sh --variant <python|node|full>`, or rebuild locally with `ENABLE_PYTHON=true`, `ENABLE_NODE=true`, or `ENABLE_FULL_SKILLS=true`.
> **Docker image variants:**
> | Image | Description |
> |-------|-------------|
> | `latest` | Backend + embedded web UI + Python (**recommended**) |
> | `latest-base` | Backend API-only, no web UI, no runtimes |
> | `latest-full` | All runtimes + skill dependencies pre-installed |
> | `latest-otel` | Latest + OpenTelemetry tracing |
> | `goclaw-web` | Standalone nginx + React SPA (for custom reverse proxy) |
>
> For custom builds (Tailscale, Redis): `docker build --build-arg ENABLE_TSNET=true ...`
> See the [Deployment Guide](https://docs.goclaw.sh/#deploy-docker-compose) for details.
> For build variants (OTel, Tailscale, Redis), Docker image tags, and compose overlays, see the [Deployment Guide](https://docs.goclaw.sh/#deploy-docker-compose).
## Updating
### Docker
```bash
docker compose pull && docker compose up -d
```
### Binary (with embedded web UI)
```bash
goclaw update --apply # Downloads, verifies SHA256, swaps binary, restarts
```
### Web Dashboard
Open **About** dialog → click **Update Now** (admin only). The update includes both backend and web dashboard when using the default `latest` image.
## Multi-Agent Orchestration
+2 -3
View File
@@ -133,10 +133,9 @@ source .env.local && ./goclaw
chmod +x prepare-env.sh && ./prepare-env.sh
# أضف على الأقل مفتاح GOCLAW_*_API_KEY إلى .env، ثم:
docker compose -f docker-compose.yml -f docker-compose.postgres.yml \
-f docker-compose.selfservice.yml up -d
make up
# لوحة تحكم الويب على http://localhost:3000
# لوحة تحكم الويب على http://localhost:18790
# فحص الصحة: curl http://localhost:18790/health
```
+2 -3
View File
@@ -133,10 +133,9 @@ source .env.local && ./goclaw
chmod +x prepare-env.sh && ./prepare-env.sh
# Add at least one GOCLAW_*_API_KEY to .env, then:
docker compose -f docker-compose.yml -f docker-compose.postgres.yml \
-f docker-compose.selfservice.yml up -d
make up
# Web Dashboard at http://localhost:3000
# Web Dashboard at http://localhost:18790
# Health check: curl http://localhost:18790/health
```
+2 -3
View File
@@ -133,10 +133,9 @@ source .env.local && ./goclaw
chmod +x prepare-env.sh && ./prepare-env.sh
# Přidat alespoň jeden GOCLAW_*_API_KEY do .env, poté:
docker compose -f docker-compose.yml -f docker-compose.postgres.yml \
-f docker-compose.selfservice.yml up -d
make up
# Webový dashboard na http://localhost:3000
# Webový dashboard na http://localhost:18790
# Kontrola stavu: curl http://localhost:18790/health
```
+2 -3
View File
@@ -133,10 +133,9 @@ source .env.local && ./goclaw
chmod +x prepare-env.sh && ./prepare-env.sh
# Tilføj mindst én GOCLAW_*_API_KEY til .env, derefter:
docker compose -f docker-compose.yml -f docker-compose.postgres.yml \
-f docker-compose.selfservice.yml up -d
make up
# Web Dashboard på http://localhost:3000
# Web Dashboard på http://localhost:18790
# Sundhedstjek: curl http://localhost:18790/health
```
+2 -3
View File
@@ -111,10 +111,9 @@ source .env.local && ./goclaw
chmod +x prepare-env.sh && ./prepare-env.sh
# Mindestens einen GOCLAW_*_API_KEY in .env hinzufügen, dann:
docker compose -f docker-compose.yml -f docker-compose.postgres.yml \
-f docker-compose.selfservice.yml up -d
make up
# Web-Dashboard unter http://localhost:3000
# Web-Dashboard unter http://localhost:18790
# Health-Check: curl http://localhost:18790/health
```
+2 -3
View File
@@ -133,10 +133,9 @@ source .env.local && ./goclaw
chmod +x prepare-env.sh && ./prepare-env.sh
# Add at least one GOCLAW_*_API_KEY to .env, then:
docker compose -f docker-compose.yml -f docker-compose.postgres.yml \
-f docker-compose.selfservice.yml up -d
make up
# Web Dashboard at http://localhost:3000
# Web Dashboard at http://localhost:18790
# Health check: curl http://localhost:18790/health
```
+2 -3
View File
@@ -111,10 +111,9 @@ source .env.local && ./goclaw
chmod +x prepare-env.sh && ./prepare-env.sh
# Añade al menos una GOCLAW_*_API_KEY al .env, luego:
docker compose -f docker-compose.yml -f docker-compose.postgres.yml \
-f docker-compose.selfservice.yml up -d
make up
# Panel web en http://localhost:3000
# Panel web en http://localhost:18790
# Verificación de estado: curl http://localhost:18790/health
```
+2 -3
View File
@@ -133,10 +133,9 @@ source .env.local && ./goclaw
chmod +x prepare-env.sh && ./prepare-env.sh
# Lisää vähintään yksi GOCLAW_*_API_KEY tiedostoon .env, sitten:
docker compose -f docker-compose.yml -f docker-compose.postgres.yml \
-f docker-compose.selfservice.yml up -d
make up
# Web-koontinäyttö osoitteessa http://localhost:3000
# Web-koontinäyttö osoitteessa http://localhost:18790
# Terveystarkistus: curl http://localhost:18790/health
```
+2 -3
View File
@@ -111,10 +111,9 @@ source .env.local && ./goclaw
chmod +x prepare-env.sh && ./prepare-env.sh
# Ajouter au moins une GOCLAW_*_API_KEY dans .env, puis :
docker compose -f docker-compose.yml -f docker-compose.postgres.yml \
-f docker-compose.selfservice.yml up -d
make up
# Tableau de bord web sur http://localhost:3000
# Tableau de bord web sur http://localhost:18790
# Vérification de santé : curl http://localhost:18790/health
```
+2 -3
View File
@@ -133,10 +133,9 @@ source .env.local && ./goclaw
chmod +x prepare-env.sh && ./prepare-env.sh
# הוסף לפחות GOCLAW_*_API_KEY אחד ל-.env, ואז:
docker compose -f docker-compose.yml -f docker-compose.postgres.yml \
-f docker-compose.selfservice.yml up -d
make up
# לוח הבקרה הווב בכתובת http://localhost:3000
# לוח הבקרה הווב בכתובת http://localhost:18790
# בדיקת תקינות: curl http://localhost:18790/health
```
+2 -3
View File
@@ -133,10 +133,9 @@ source .env.local && ./goclaw
chmod +x prepare-env.sh && ./prepare-env.sh
# Add at least one GOCLAW_*_API_KEY to .env, then:
docker compose -f docker-compose.yml -f docker-compose.postgres.yml \
-f docker-compose.selfservice.yml up -d
make up
# Web Dashboard at http://localhost:3000
# Web Dashboard at http://localhost:18790
# Health check: curl http://localhost:18790/health
```
+2 -3
View File
@@ -133,10 +133,9 @@ source .env.local && ./goclaw
chmod +x prepare-env.sh && ./prepare-env.sh
# Adj meg legalább egy GOCLAW_*_API_KEY értéket a .env fájlban, majd:
docker compose -f docker-compose.yml -f docker-compose.postgres.yml \
-f docker-compose.selfservice.yml up -d
make up
# Webes irányítópult: http://localhost:3000
# Webes irányítópult: http://localhost:18790
# Állapotfelmérés: curl http://localhost:18790/health
```
+2 -3
View File
@@ -133,10 +133,9 @@ source .env.local && ./goclaw
chmod +x prepare-env.sh && ./prepare-env.sh
# Tambahkan minimal satu GOCLAW_*_API_KEY ke .env, lalu:
docker compose -f docker-compose.yml -f docker-compose.postgres.yml \
-f docker-compose.selfservice.yml up -d
make up
# Dasbor Web di http://localhost:3000
# Dasbor Web di http://localhost:18790
# Pemeriksaan kesehatan: curl http://localhost:18790/health
```
+2 -3
View File
@@ -133,10 +133,9 @@ source .env.local && ./goclaw
chmod +x prepare-env.sh && ./prepare-env.sh
# Aggiungi almeno un GOCLAW_*_API_KEY a .env, poi:
docker compose -f docker-compose.yml -f docker-compose.postgres.yml \
-f docker-compose.selfservice.yml up -d
make up
# Web Dashboard su http://localhost:3000
# Web Dashboard su http://localhost:18790
# Health check: curl http://localhost:18790/health
```
+2 -3
View File
@@ -111,10 +111,9 @@ source .env.local && ./goclaw
chmod +x prepare-env.sh && ./prepare-env.sh
# Add at least one GOCLAW_*_API_KEY to .env, then:
docker compose -f docker-compose.yml -f docker-compose.postgres.yml \
-f docker-compose.selfservice.yml up -d
make up
# Web Dashboard at http://localhost:3000
# Web Dashboard at http://localhost:18790
# Health check: curl http://localhost:18790/health
```
+2 -3
View File
@@ -111,10 +111,9 @@ source .env.local && ./goclaw
chmod +x prepare-env.sh && ./prepare-env.sh
# Add at least one GOCLAW_*_API_KEY to .env, then:
docker compose -f docker-compose.yml -f docker-compose.postgres.yml \
-f docker-compose.selfservice.yml up -d
make up
# Web Dashboard at http://localhost:3000
# Web Dashboard at http://localhost:18790
# Health check: curl http://localhost:18790/health
```
+2 -3
View File
@@ -133,10 +133,9 @@ source .env.local && ./goclaw
chmod +x prepare-env.sh && ./prepare-env.sh
# Legg til minst én GOCLAW_*_API_KEY i .env, deretter:
docker compose -f docker-compose.yml -f docker-compose.postgres.yml \
-f docker-compose.selfservice.yml up -d
make up
# Web-dashbord på http://localhost:3000
# Web-dashbord på http://localhost:18790
# Helsesjekk: curl http://localhost:18790/health
```
+2 -3
View File
@@ -133,10 +133,9 @@ source .env.local && ./goclaw
chmod +x prepare-env.sh && ./prepare-env.sh
# Voeg minimaal één GOCLAW_*_API_KEY toe aan .env, dan:
docker compose -f docker-compose.yml -f docker-compose.postgres.yml \
-f docker-compose.selfservice.yml up -d
make up
# Webdashboard op http://localhost:3000
# Webdashboard op http://localhost:18790
# Statuscontrole: curl http://localhost:18790/health
```
+2 -3
View File
@@ -133,10 +133,9 @@ source .env.local && ./goclaw
chmod +x prepare-env.sh && ./prepare-env.sh
# Dodaj co najmniej jeden GOCLAW_*_API_KEY do .env, a następnie:
docker compose -f docker-compose.yml -f docker-compose.postgres.yml \
-f docker-compose.selfservice.yml up -d
make up
# Panel webowy pod adresem http://localhost:3000
# Panel webowy pod adresem http://localhost:18790
# Sprawdzenie stanu: curl http://localhost:18790/health
```
+2 -3
View File
@@ -111,10 +111,9 @@ source .env.local && ./goclaw
chmod +x prepare-env.sh && ./prepare-env.sh
# Adicione pelo menos uma GOCLAW_*_API_KEY ao .env, depois:
docker compose -f docker-compose.yml -f docker-compose.postgres.yml \
-f docker-compose.selfservice.yml up -d
make up
# Painel Web em http://localhost:3000
# Painel Web em http://localhost:18790
# Verificação de saúde: curl http://localhost:18790/health
```
+2 -3
View File
@@ -133,10 +133,9 @@ source .env.local && ./goclaw
chmod +x prepare-env.sh && ./prepare-env.sh
# Adaugă cel puțin un GOCLAW_*_API_KEY în .env, apoi:
docker compose -f docker-compose.yml -f docker-compose.postgres.yml \
-f docker-compose.selfservice.yml up -d
make up
# Tablou de bord web la http://localhost:3000
# Tablou de bord web la http://localhost:18790
# Verificare stare: curl http://localhost:18790/health
```
+2 -3
View File
@@ -111,10 +111,9 @@ source .env.local && ./goclaw
chmod +x prepare-env.sh && ./prepare-env.sh
# Добавьте как минимум один GOCLAW_*_API_KEY в .env, затем:
docker compose -f docker-compose.yml -f docker-compose.postgres.yml \
-f docker-compose.selfservice.yml up -d
make up
# Веб-дэшборд: http://localhost:3000
# Веб-дэшборд: http://localhost:18790
# Проверка работоспособности: curl http://localhost:18790/health
```
+2 -3
View File
@@ -133,10 +133,9 @@ source .env.local && ./goclaw
chmod +x prepare-env.sh && ./prepare-env.sh
# Lägg till minst en GOCLAW_*_API_KEY i .env, sedan:
docker compose -f docker-compose.yml -f docker-compose.postgres.yml \
-f docker-compose.selfservice.yml up -d
make up
# Webbinstrumentpanel på http://localhost:3000
# Webbinstrumentpanel på http://localhost:18790
# Hälsokontroll: curl http://localhost:18790/health
```
+2 -3
View File
@@ -113,10 +113,9 @@ source .env.local && ./goclaw
chmod +x prepare-env.sh && ./prepare-env.sh
# เพิ่ม GOCLAW_*_API_KEY อย่างน้อยหนึ่งรายการใน .env จากนั้น:
docker compose -f docker-compose.yml -f docker-compose.postgres.yml \
-f docker-compose.selfservice.yml up -d
make up
# Web Dashboard ที่ http://localhost:3000
# Web Dashboard ที่ http://localhost:18790
# ตรวจสอบสถานะ: curl http://localhost:18790/health
```
+2 -3
View File
@@ -133,10 +133,9 @@ source .env.local && ./goclaw
chmod +x prepare-env.sh && ./prepare-env.sh
# Magdagdag ng kahit isang GOCLAW_*_API_KEY sa .env, pagkatapos:
docker compose -f docker-compose.yml -f docker-compose.postgres.yml \
-f docker-compose.selfservice.yml up -d
make up
# Web Dashboard sa http://localhost:3000
# Web Dashboard sa http://localhost:18790
# Health check: curl http://localhost:18790/health
```
+2 -3
View File
@@ -133,10 +133,9 @@ source .env.local && ./goclaw
chmod +x prepare-env.sh && ./prepare-env.sh
# .env dosyasına en az bir GOCLAW_*_API_KEY ekleyin, ardından:
docker compose -f docker-compose.yml -f docker-compose.postgres.yml \
-f docker-compose.selfservice.yml up -d
make up
# Web Panosu: http://localhost:3000
# Web Panosu: http://localhost:18790
# Sağlık kontrolü: curl http://localhost:18790/health
```
+2 -3
View File
@@ -133,10 +133,9 @@ source .env.local && ./goclaw
chmod +x prepare-env.sh && ./prepare-env.sh
# Додайте хоча б один GOCLAW_*_API_KEY до .env, потім:
docker compose -f docker-compose.yml -f docker-compose.postgres.yml \
-f docker-compose.selfservice.yml up -d
make up
# Веб-панель за адресою http://localhost:3000
# Веб-панель за адресою http://localhost:18790
# Перевірка стану: curl http://localhost:18790/health
```
+2 -3
View File
@@ -133,10 +133,9 @@ source .env.local && ./goclaw
chmod +x prepare-env.sh && ./prepare-env.sh
# .env میں کم از کم ایک GOCLAW_*_API_KEY شامل کریں، پھر:
docker compose -f docker-compose.yml -f docker-compose.postgres.yml \
-f docker-compose.selfservice.yml up -d
make up
# ویب ڈیش بورڈ http://localhost:3000 پر
# ویب ڈیش بورڈ http://localhost:18790 پر
# ہیلتھ چیک: curl http://localhost:18790/health
```
+2 -3
View File
@@ -111,10 +111,9 @@ source .env.local && ./goclaw
chmod +x prepare-env.sh && ./prepare-env.sh
# Thêm ít nhất một GOCLAW_*_API_KEY vào .env, sau đó:
docker compose -f docker-compose.yml -f docker-compose.postgres.yml \
-f docker-compose.selfservice.yml up -d
make up
# Web Dashboard tại http://localhost:3000
# Web Dashboard tại http://localhost:18790
# Kiểm tra trạng thái: curl http://localhost:18790/health
```
+2 -3
View File
@@ -111,10 +111,9 @@ source .env.local && ./goclaw
chmod +x prepare-env.sh && ./prepare-env.sh
# 在 .env 中至少添加一个 GOCLAW_*_API_KEY,然后:
docker compose -f docker-compose.yml -f docker-compose.postgres.yml \
-f docker-compose.selfservice.yml up -d
make up
# Web 控制台地址:http://localhost:3000
# Web 控制台地址:http://localhost:18790
# 健康检查:curl http://localhost:18790/health
```
+3 -2
View File
@@ -1,7 +1,8 @@
# PostgreSQL overlay — pgvector for multi-tenant storage.
#
# Usage (with Web Dashboard):
# docker compose -f docker-compose.yml -f docker-compose.postgres.yml -f docker-compose.selfservice.yml up -d --build
# Usage (web dashboard built-in by default):
# make up
# # or: docker compose -f docker-compose.yml -f docker-compose.postgres.yml up -d --build
#
# Required env vars (set in .env or shell):
# GOCLAW_OPENROUTER_API_KEY (or another provider key)
+9 -4
View File
@@ -1,9 +1,14 @@
# Self-service overlay — adds web dashboard UI (nginx + React SPA).
# Self-service overlay — adds separate nginx web dashboard UI.
#
# Recommended usage:
# docker compose -f docker-compose.yml -f docker-compose.postgres.yml -f docker-compose.selfservice.yml up -d --build
# OPTIONAL: The default `latest` image already includes an embedded web dashboard
# served at port 18790. Use this overlay only if you need a dedicated nginx
# reverse proxy for SSL termination, custom caching, or load balancing.
#
# Dashboard: http://localhost:3000
# Usage:
# docker compose -f docker-compose.yml -f docker-compose.postgres.yml -f docker-compose.selfservice.yml up -d
# # or: make up WITH_WEB_NGINX=1
#
# Dashboard via nginx: http://localhost:3000
services:
goclaw-ui:
+19 -8
View File
@@ -1,17 +1,26 @@
# Base docker-compose — shared service definition.
# Pre-built images: ghcr.io/nextlevelbuilder/goclaw (also on Docker Hub: digitop/goclaw)
#
# Combine with overlays for your deployment:
# The default `latest` image includes an embedded web dashboard (no separate nginx needed).
# Web UI is served at the same port as the API (default: 18790).
#
# Recommended (+ Web Dashboard, pre-built images):
# docker compose -f docker-compose.yml -f docker-compose.postgres.yml -f docker-compose.selfservice.yml up -d
# Quick start:
# docker compose -f docker-compose.yml -f docker-compose.postgres.yml up -d
# # Dashboard: http://localhost:18790
#
# Build from source (add --build):
# docker compose -f docker-compose.yml -f docker-compose.postgres.yml -f docker-compose.selfservice.yml up -d --build
# Optional nginx (for custom SSL, reverse proxy, caching):
# make up WITH_WEB_NGINX=1
# # or: docker compose -f docker-compose.yml -f docker-compose.postgres.yml -f docker-compose.selfservice.yml up -d
# # Dashboard via nginx: http://localhost:3000
#
# Without dashboard: docker compose -f docker-compose.yml -f docker-compose.postgres.yml up -d
# With OTel tracing: docker compose -f docker-compose.yml -f docker-compose.postgres.yml -f docker-compose.otel.yml up -d --build
# With Claude CLI: add -f docker-compose.claude-cli.yml to auto-sync host ~/.claude credentials
# Docker image variants:
# latest — Backend + embedded web UI + Python (recommended)
# latest-base — Backend API-only, no web UI, no runtimes
# latest-full — All runtimes + skill dependencies pre-installed
# latest-otel — Latest + OpenTelemetry tracing
#
# With OTel tracing: add -f docker-compose.otel.yml
# With Claude CLI: add -f docker-compose.claude-cli.yml
services:
goclaw:
@@ -21,9 +30,11 @@ services:
dockerfile: Dockerfile
args:
ENABLE_OTEL: "${ENABLE_OTEL:-false}"
ENABLE_EMBEDUI: "${ENABLE_EMBEDUI:-true}" # set to false when using WITH_WEB_NGINX
ENABLE_PYTHON: "${ENABLE_PYTHON:-true}"
ENABLE_FULL_SKILLS: "${ENABLE_FULL_SKILLS:-false}"
VERSION: "${GOCLAW_VERSION:-dev}"
# Uncomment to test update UI: GOCLAW_VERSION=v1.0.0
ports:
- "${GOCLAW_PORT:-18790}:18790"
env_file:
+1
View File
@@ -464,6 +464,7 @@ func (r *MethodRouter) handleHealth(ctx context.Context, client *Client, req *pr
resp["latestVersion"] = info.LatestVersion
resp["updateAvailable"] = info.UpdateAvailable
resp["updateUrl"] = info.UpdateURL
resp["releaseNotes"] = info.ReleaseNotes
}
}
client.SendResponse(protocol.NewOKResponse(req.ID, resp))
+7
View File
@@ -21,6 +21,7 @@ import (
"github.com/nextlevelbuilder/goclaw/internal/config"
httpapi "github.com/nextlevelbuilder/goclaw/internal/http"
mcpbridge "github.com/nextlevelbuilder/goclaw/internal/mcp"
"github.com/nextlevelbuilder/goclaw/internal/webui"
"github.com/nextlevelbuilder/goclaw/internal/permissions"
"github.com/nextlevelbuilder/goclaw/internal/providers"
"github.com/nextlevelbuilder/goclaw/internal/store"
@@ -196,6 +197,12 @@ func (s *Server) BuildMux() *http.ServeMux {
}
}
// Embedded web UI (built with -tags embedui). Catch-all after all API routes.
if h := webui.Handler(); h != nil {
mux.Handle("/", h)
slog.Info("serving embedded web UI")
}
s.mux = mux
return mux
}
+61 -79
View File
@@ -7,22 +7,26 @@ import (
"io"
"log/slog"
"net/http"
"strconv"
"strings"
"sync"
"time"
"github.com/nextlevelbuilder/goclaw/internal/version"
)
const (
githubRepo = "nextlevelbuilder/goclaw"
liteTagPrefix = "lite-v"
updateCheckInterval = 1 * time.Hour
maxResponseBody = 2 << 20 // 2 MB
)
// UpdateInfo holds the latest release information from GitHub.
type UpdateInfo struct {
LatestVersion string `json:"latestVersion"`
UpdateURL string `json:"updateUrl"`
LatestVersion string `json:"latestVersion"`
UpdateURL string `json:"updateUrl"`
UpdateAvailable bool `json:"updateAvailable"`
ReleaseNotes string `json:"releaseNotes,omitempty"`
}
// UpdateChecker periodically checks GitHub for new releases.
@@ -30,6 +34,7 @@ type UpdateChecker struct {
currentVersion string
mu sync.RWMutex
info *UpdateInfo
etag string // ETag for conditional requests
}
// NewUpdateChecker creates an UpdateChecker for the given current version.
@@ -39,7 +44,6 @@ func NewUpdateChecker(currentVersion string) *UpdateChecker {
// Start begins periodic update checking. Call with a cancellable context.
func (uc *UpdateChecker) Start(ctx context.Context) {
// Initial check after short delay (don't block startup)
go func() {
select {
case <-time.After(5 * time.Second):
@@ -68,11 +72,21 @@ func (uc *UpdateChecker) Info() *UpdateInfo {
return uc.info
}
// githubRelease is a minimal GitHub Release API response.
type githubRelease struct {
TagName string `json:"tag_name"`
HTMLURL string `json:"html_url"`
Body string `json:"body"`
Draft bool `json:"draft"`
Prerelease bool `json:"prerelease"`
}
func (uc *UpdateChecker) check() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
url := fmt.Sprintf("https://api.github.com/repos/%s/releases/latest", githubRepo)
// Use releases list API (not /latest) to filter out lite-v* tags.
url := fmt.Sprintf("https://api.github.com/repos/%s/releases", githubRepo)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
slog.Warn("update check: failed to create request", "error", err)
@@ -81,6 +95,11 @@ func (uc *UpdateChecker) check() {
req.Header.Set("Accept", "application/vnd.github+json")
req.Header.Set("User-Agent", "goclaw/"+uc.currentVersion)
// ETag conditional request to reduce API rate limit usage.
if uc.etag != "" {
req.Header.Set("If-None-Match", uc.etag)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
slog.Warn("update check: request failed", "error", err)
@@ -88,89 +107,52 @@ func (uc *UpdateChecker) check() {
}
defer resp.Body.Close()
// 304 Not Modified — cached info is still valid.
if resp.StatusCode == http.StatusNotModified {
return
}
if resp.StatusCode != http.StatusOK {
slog.Warn("update check: unexpected status", "status", resp.StatusCode)
return
}
var release struct {
TagName string `json:"tag_name"`
HTMLURL string `json:"html_url"`
// Cache ETag.
if etag := resp.Header.Get("ETag"); etag != "" {
uc.etag = etag
}
if err := json.NewDecoder(io.LimitReader(resp.Body, 1<<20)).Decode(&release); err != nil {
var releases []githubRelease
if err := json.NewDecoder(io.LimitReader(resp.Body, maxResponseBody)).Decode(&releases); err != nil {
slog.Warn("update check: failed to decode response", "error", err)
return
}
if release.TagName == "" {
// Find the latest non-draft, non-prerelease, non-lite server release.
for _, rel := range releases {
if rel.Draft || rel.Prerelease {
continue
}
if strings.HasPrefix(rel.TagName, liteTagPrefix) {
continue
}
if !strings.HasPrefix(rel.TagName, "v") {
continue
}
info := &UpdateInfo{
LatestVersion: rel.TagName,
UpdateURL: rel.HTMLURL,
UpdateAvailable: version.IsNewer(rel.TagName, uc.currentVersion),
ReleaseNotes: rel.Body,
}
uc.mu.Lock()
uc.info = info
uc.mu.Unlock()
if info.UpdateAvailable {
slog.Info("new version available", "current", uc.currentVersion, "latest", rel.TagName)
}
return
}
info := &UpdateInfo{
LatestVersion: release.TagName,
UpdateURL: release.HTMLURL,
UpdateAvailable: isNewer(uc.currentVersion, release.TagName),
}
uc.mu.Lock()
uc.info = info
uc.mu.Unlock()
if info.UpdateAvailable {
slog.Info("new version available", "current", uc.currentVersion, "latest", release.TagName)
}
}
// isNewer returns true if latest is a newer version than current.
// Compares by stripping "v" prefix and doing simple string comparison.
// Returns false if current is "dev" (development build).
func isNewer(current, latest string) bool {
current = strings.TrimPrefix(current, "v")
latest = strings.TrimPrefix(latest, "v")
if current == "dev" || current == "" || latest == "" {
return false
}
if current == latest {
return false
}
// Semantic version comparison: split by "." and compare numerically
return compareSemver(latest, current) > 0
}
// compareSemver compares two semver strings (without "v" prefix).
// Returns >0 if a > b, <0 if a < b, 0 if equal.
// Handles pre-release suffixes by stripping them (e.g., "1.2.3-5-gabcdef").
func compareSemver(a, b string) int {
partsA := parseSemver(a)
partsB := parseSemver(b)
for i := range 3 {
va, vb := 0, 0
if i < len(partsA) {
va = partsA[i]
}
if i < len(partsB) {
vb = partsB[i]
}
if va != vb {
return va - vb
}
}
return 0
}
func parseSemver(s string) []int {
// Strip pre-release suffix: "1.2.3-5-gabcdef" → "1.2.3"
if idx := strings.IndexByte(s, '-'); idx >= 0 {
s = s[:idx]
}
parts := strings.Split(s, ".")
nums := make([]int, 0, len(parts))
for _, p := range parts {
n, _ := strconv.Atoi(p)
nums = append(nums, n)
}
return nums
}
+4 -29
View File
@@ -16,6 +16,8 @@ import (
"runtime"
"strings"
"time"
"github.com/nextlevelbuilder/goclaw/internal/version"
)
const (
@@ -336,36 +338,9 @@ func removeQuarantine(appPath string) {
}
}
// isNewer returns true if version a is newer than b (simple semver compare).
// Pre-release suffixes (e.g. "0.2.0-rc1") are stripped — only major.minor.patch compared.
// isNewer returns true if version a is newer than b.
func isNewer(a, b string) bool {
pa := parseSemver(a)
pb := parseSemver(b)
for i := range 3 {
if pa[i] > pb[i] {
return true
}
if pa[i] < pb[i] {
return false
}
}
return false
}
func parseSemver(s string) [3]int {
s = strings.TrimPrefix(s, "v")
// Strip pre-release suffix: "0.2.0-rc1" → "0.2.0"
if idx := strings.IndexByte(s, '-'); idx >= 0 {
s = s[:idx]
}
var parts [3]int
for i, p := range strings.SplitN(s, ".", 3) {
if i >= 3 {
break
}
fmt.Sscanf(p, "%d", &parts[i])
}
return parts
return version.IsNewer(a, b)
}
// ResolveAppPath returns the path to the current .app bundle (macOS) or .exe (Windows)
+47
View File
@@ -0,0 +1,47 @@
// Package version provides shared semantic version comparison utilities.
package version
import (
"strconv"
"strings"
)
// IsNewer returns true if version a is newer than b.
// Both versions may include "v" prefix and pre-release suffixes (stripped before comparison).
// Returns false if either version is "dev" or empty.
func IsNewer(a, b string) bool {
a = strings.TrimPrefix(a, "v")
b = strings.TrimPrefix(b, "v")
if a == "dev" || a == "" || b == "dev" || b == "" {
return false
}
return Compare(a, b) > 0
}
// Compare compares two semver strings. Returns >0 if a > b, <0 if a < b, 0 if equal.
// Strips "v" prefix and pre-release suffixes before comparison.
func Compare(a, b string) int {
pa := Parse(a)
pb := Parse(b)
for i := range 3 {
if pa[i] != pb[i] {
return pa[i] - pb[i]
}
}
return 0
}
// Parse extracts [major, minor, patch] from a semver string.
// Strips "v" prefix and pre-release suffixes (e.g. "v1.2.3-5-gabcdef" → [1, 2, 3]).
func Parse(s string) [3]int {
s = strings.TrimPrefix(s, "v")
// Strip pre-release suffix: "1.2.3-rc1" → "1.2.3"
if idx := strings.IndexByte(s, '-'); idx >= 0 {
s = s[:idx]
}
var parts [3]int
for i, p := range strings.SplitN(s, ".", 3) {
parts[i], _ = strconv.Atoi(p)
}
return parts
}
+23
View File
@@ -0,0 +1,23 @@
//go:build embedui
package webui
import (
"embed"
"io/fs"
)
//go:embed all:dist
var assets embed.FS
// Assets returns the embedded web UI filesystem rooted at "dist/".
func Assets() fs.FS {
sub, err := fs.Sub(assets, "dist")
if err != nil {
return nil
}
return sub
}
// HasAssets returns true when built with the embedui tag.
func HasAssets() bool { return true }
+13
View File
@@ -0,0 +1,13 @@
//go:build !embedui
// Package webui provides optional embedded web UI serving.
// When built without the embedui tag, no assets are available.
package webui
import "io/fs"
// Assets returns nil when built without the embedui tag.
func Assets() fs.FS { return nil }
// HasAssets returns false when built without the embedui tag.
func HasAssets() bool { return false }
+58
View File
@@ -0,0 +1,58 @@
package webui
import (
"io/fs"
"net/http"
"strings"
)
// apiPrefixes are URL prefixes reserved for backend APIs.
// Requests matching these are never served by the SPA handler.
var apiPrefixes = []string{"/v1/", "/ws", "/health", "/mcp/"}
// Handler returns an http.Handler that serves the embedded SPA.
// Returns nil if no assets are embedded (built without embedui tag).
func Handler() http.Handler {
fsys := Assets()
if fsys == nil {
return nil
}
fileServer := http.FileServer(http.FS(fsys))
return &spaHandler{fs: fsys, fileServer: fileServer}
}
type spaHandler struct {
fs fs.FS
fileServer http.Handler
}
func (h *spaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Never intercept API routes.
for _, prefix := range apiPrefixes {
if strings.HasPrefix(r.URL.Path, prefix) {
http.NotFound(w, r)
return
}
}
// Try to serve the file directly.
path := strings.TrimPrefix(r.URL.Path, "/")
if path == "" {
path = "index.html"
}
// Check if file exists in the embedded FS.
if _, err := fs.Stat(h.fs, path); err == nil {
// Static assets: set long cache for /assets/* (Vite hashed filenames).
if strings.HasPrefix(r.URL.Path, "/assets/") {
w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
}
h.fileServer.ServeHTTP(w, r)
return
}
// SPA fallback: serve index.html for any unmatched route.
// This handles client-side routing (React Router).
r.URL.Path = "/"
h.fileServer.ServeHTTP(w, r)
}
+4 -1
View File
@@ -86,5 +86,8 @@ fi
echo ""
echo "=== Done ==="
echo ""
echo " Run: docker compose -f docker-compose.yml -f docker-compose.postgres.yml -f docker-compose.selfservice.yml up -d --build"
echo " Run: make up"
echo ""
echo " Web dashboard: http://localhost:18790"
echo " With separate nginx: make up WITH_WEB_NGINX=1 → http://localhost:3000"
echo ""
+7
View File
@@ -79,6 +79,8 @@ echo ""
echo "GoClaw ${VERSION} installed to ${INSTALL_DIR}/goclaw"
echo "Migrations installed to ${MIGRATIONS_DIR}"
echo ""
echo "The binary includes an embedded web dashboard — no separate nginx needed."
echo ""
echo "Next steps:"
echo " 1. Set up PostgreSQL (pgvector):"
echo " docker run -d --name goclaw-pg -p 5432:5432 -e POSTGRES_PASSWORD=goclaw pgvector/pgvector:pg18"
@@ -92,3 +94,8 @@ echo " goclaw onboard"
echo ""
echo " 4. Start the gateway:"
echo " source .env.local && goclaw"
echo ""
echo " Web dashboard: http://localhost:18790"
echo " Health check: curl http://localhost:18790/health"
echo ""
echo " To update later: goclaw update --apply"
+61 -53
View File
@@ -1,4 +1,4 @@
import { useEffect } from "react";
import { lazy, Suspense, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useAuthStore } from "@/stores/use-auth-store";
import { useWsCall } from "@/hooks/use-ws-call";
@@ -12,6 +12,13 @@ import {
DialogTitle,
DialogFooter,
} from "@/components/ui/dialog";
import {
ChevronDown,
ChevronRight,
ExternalLink,
} from "lucide-react";
const ReactMarkdown = lazy(() => import("react-markdown"));
interface AboutDialogProps {
open: boolean;
@@ -25,10 +32,15 @@ export function AboutDialog({ open, onOpenChange }: AboutDialogProps) {
const { call: fetchHealth, data: health } =
useWsCall<HealthPayload>(Methods.HEALTH);
const [notesExpanded, setNotesExpanded] = useState(false);
useEffect(() => {
if (open && connected) {
fetchHealth();
}
if (!open) {
setNotesExpanded(false);
}
}, [open, connected, fetchHealth]);
const rawVersion = health?.version || serverInfo?.version || "dev";
@@ -36,14 +48,20 @@ export function AboutDialog({ open, onOpenChange }: AboutDialogProps) {
const latestVersion = health?.latestVersion;
const updateAvailable = health?.updateAvailable ?? false;
const updateUrl = health?.updateUrl;
const releaseNotes = health?.releaseNotes;
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-md">
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle className="flex items-center gap-2.5">
<img src="/goclaw-icon.svg" alt="GoClaw" className="h-7 w-7" />
{t("about.title")}
{updateAvailable && latestVersion && (
<span className="rounded-full bg-amber-500/15 px-2 py-0.5 text-xs font-medium text-amber-600 dark:text-amber-400">
{latestVersion}
</span>
)}
</DialogTitle>
</DialogHeader>
@@ -67,69 +85,59 @@ export function AboutDialog({ open, onOpenChange }: AboutDialogProps) {
<div className="font-medium">
{t("about.updateAvailable", { version: latestVersion })}
</div>
{/* Release notes (collapsible, markdown) */}
{releaseNotes && (
<div className="mt-2">
<button
onClick={() => setNotesExpanded((v) => !v)}
className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground"
>
{notesExpanded ? <ChevronDown className="size-3" /> : <ChevronRight className="size-3" />}
{t("about.releaseNotes")}
</button>
{notesExpanded && (
<div className="mt-1.5 max-h-48 overflow-y-auto rounded border bg-muted/50 p-2.5 text-xs prose prose-xs dark:prose-invert prose-headings:text-sm prose-headings:font-semibold prose-headings:mt-2 prose-headings:mb-1 prose-p:my-1 prose-ul:my-1 prose-li:my-0">
<Suspense fallback={<span>{releaseNotes}</span>}>
<ReactMarkdown>{releaseNotes}</ReactMarkdown>
</Suspense>
</div>
)}
</div>
)}
{updateUrl && (
<a
href={updateUrl}
target="_blank"
rel="noopener noreferrer"
className="mt-1 inline-block text-primary hover:underline"
className="mt-2 inline-flex items-center gap-1 text-xs text-primary hover:underline"
>
{t("about.viewRelease")}
<ExternalLink className="size-3" />
</a>
)}
</div>
)}
{/* Source Code */}
<div className="grid grid-cols-[140px_1fr] items-baseline gap-2 text-sm">
<span className="text-muted-foreground">{t("about.sourceCode")}</span>
<a
href="https://github.com/nextlevelbuilder/goclaw"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline break-all"
>
github.com/nextlevelbuilder/goclaw
</a>
</div>
{/* License */}
<div className="grid grid-cols-[140px_1fr] items-baseline gap-2 text-sm">
<span className="text-muted-foreground">{t("about.license")}</span>
<a
href="https://creativecommons.org/licenses/by-nc/4.0/"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
CC BY-NC 4.0
</a>
</div>
{/* Documentation */}
<div className="grid grid-cols-[140px_1fr] items-baseline gap-2 text-sm">
<span className="text-muted-foreground">{t("about.documentation")}</span>
<a
href="https://docs.goclaw.sh"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
docs.goclaw.sh
</a>
</div>
{/* Report Bug */}
<div className="grid grid-cols-[140px_1fr] items-baseline gap-2 text-sm">
<span className="text-muted-foreground">{t("about.reportBug")}</span>
<a
href="https://github.com/nextlevelbuilder/goclaw/issues"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline break-all"
>
github.com/.../issues
</a>
{/* Links */}
<div className="flex flex-wrap gap-x-4 gap-y-1 text-sm">
{[
{ label: t("about.sourceCode"), href: "https://github.com/nextlevelbuilder/goclaw" },
{ label: t("about.license"), href: "https://creativecommons.org/licenses/by-nc/4.0/" },
{ label: t("about.documentation"), href: "https://docs.goclaw.sh" },
{ label: t("about.reportBug"), href: "https://github.com/nextlevelbuilder/goclaw/issues" },
].map(({ label, href }) => (
<a
key={label}
href={href}
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
{label}
</a>
))}
</div>
</div>
+2 -1
View File
@@ -23,7 +23,8 @@
"reportBug": "Report a Bug",
"upToDate": "Up to date",
"updateAvailable": "Update available: {{version}}",
"viewRelease": "View release notes →"
"viewRelease": "View release notes →",
"releaseNotes": "Release Notes"
},
"languages": {
"en": "English",
+2 -1
View File
@@ -26,7 +26,8 @@
"reportBug": "Báo lỗi",
"upToDate": "Đã cập nhật",
"updateAvailable": "Có bản cập nhật: {{version}}",
"viewRelease": "Xem chi tiết →"
"viewRelease": "Xem chi tiết →",
"releaseNotes": "Ghi chú phát hành"
},
"systemSettings": "Cài đặt hệ thống",
"toggleTheme": "Chuyển đổi giao diện"
+2 -1
View File
@@ -26,7 +26,8 @@
"reportBug": "报告问题",
"upToDate": "已是最新",
"updateAvailable": "有新版本: {{version}}",
"viewRelease": "查看发行说明 →"
"viewRelease": "查看发行说明 →",
"releaseNotes": "发行说明"
},
"systemSettings": "系统设置",
"toggleTheme": "切换主题"
@@ -86,15 +86,10 @@ export function SystemHealthCard({
{health.updateAvailable === false && (
<CheckCircle2 className="h-3 w-3 text-emerald-500" />
)}
{health.updateAvailable && health.updateUrl && (
<a
href={health.updateUrl}
target="_blank"
rel="noopener noreferrer"
className="ml-1 text-primary hover:underline"
>
{health.latestVersion} →
</a>
{health.updateAvailable && health.latestVersion && (
<span className="ml-1 rounded-full bg-amber-500/15 px-2 py-0.5 text-xs font-medium text-amber-600 dark:text-amber-400">
{health.latestVersion} available
</span>
)}
</div>
)}
+1
View File
@@ -18,6 +18,7 @@ export interface HealthPayload {
latestVersion?: string;
updateAvailable?: boolean;
updateUrl?: string;
releaseNotes?: string;
}
export interface AgentInfo {