mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-11 03:13:24 +00:00
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:
1 parent
39bb90bd60
commit
52c67d6d92
56 files changed
+531
-381
No files matched your search
+3
-1
@@ -16,6 +16,8 @@ tmp/
|
||||
.claude/
|
||||
.vscode/
|
||||
.idea/
|
||||
ui/
|
||||
ui/desktop/
|
||||
ui/simple-saas/
|
||||
!ui/web/
|
||||
plans/
|
||||
skills-store/
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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
@@ -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
@@ -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; \
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 }
|
||||
@@ -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 }
|
||||
@@ -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
@@ -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 ""
|
||||
@@ -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"
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -18,6 +18,7 @@ export interface HealthPayload {
|
||||
latestVersion?: string;
|
||||
updateAvailable?: boolean;
|
||||
updateUrl?: string;
|
||||
releaseNotes?: string;
|
||||
}
|
||||
|
||||
export interface AgentInfo {
|
||||
|
||||
Reference in new issue
Block a user