25 Commits

Author SHA1 Message Date
Gan, Jimmy f39fb4c931 Merge branch 'main' into dev
Deploy Dashboard (Main) / Backend Tests (push) Failing after 32s
Deploy Dashboard (Main) / Build Main Image (push) Has been skipped
Deploy Dashboard (Dev) / Backend Tests (push) Successful in 1m8s
Deploy Dashboard (Dev) / Frontend Tests (push) Successful in 53s
Deploy Dashboard (Main) / Frontend Tests (push) Failing after 5s
Deploy Dashboard (Main) / Deploy to Production (push) Has been skipped
Deploy Dashboard (Dev) / Build Dev Image (push) Successful in 1m34s
Deploy Dashboard (Dev) / Deploy to Dev (push) Failing after 0s
# Conflicts:
#	.gitea/workflows/deploy-dev.yml
#	.gitea/workflows/deploy.yml
2026-07-07 15:50:38 +08:00
Gan, Jimmy e676ef06ad ci: trigger clean pipeline - all tests passing
Deploy Dashboard (Dev) / Backend Tests (push) Successful in 1m24s
Deploy Dashboard (Dev) / Frontend Tests (push) Successful in 1m7s
Deploy Dashboard (Dev) / Build Dev Image (push) Failing after 3s
Deploy Dashboard (Dev) / Deploy to Dev (push) Has been skipped
2026-06-21 23:30:52 +08:00
Gan, Jimmy 7cc6135099 fix: also patch claude.ai and anthropic.com bare refs in cli.js
Deploy Claude Dev (Dev) / deploy-claude-dev-dev (push) Successful in 2m4s
The previous patch only caught https://claude.ai (36 refs)
but missed bare claude.ai (70 refs) and anthropic.com (24 refs)
used in OAuth and bootstrap URL construction.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 23:43:02 +08:00
Claude CI Bot 5f750bf07c fix(ci): use gitea:3000 for checkout — 172.21.0.1:3300 unreachable from runner
Deploy Claude Dev (Dev) / deploy-claude-dev-dev (push) Successful in 2m56s
2026-06-10 18:25:06 +00:00
Gan, Jimmy ff74fc0b65 fix: auto-start litellm on container start, add missing DEEPSEEK_API_KEY env
Deploy Claude Dev (Dev) / deploy-claude-dev-dev (push) Failing after 39s
2026-06-08 09:06:51 +08:00
Gan, Jimmy f3f32a1853 ci: retrigger claude-dev build after runner fix 2026-06-07 21:33:08 +08:00
Gan, Jimmy f2b8529a6a claude-dev: bake LiteLLM proxy + URL patch into Docker image
Deploy Claude Dev (Dev) / deploy-claude-dev-dev (push) Failing after 2m3s
- Add python3-pip + litellm[proxy] to base image
- Replace broken regex preflight patch with working binary URL replacement
  (api.anthropic.com, platform.claude.com, claude.ai → localhost:4008)
- Auto-start LiteLLM on port 4008 via CMD (no more manual restart)
- Add litellm.yaml config and start-litellm.sh script
- Add .env.example
- Update docker-compose.yml env_file path to relative ./.env

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 18:57:06 +08:00
Gan, Jimmy 9a2b1f8941 ci: clean trigger - tests passing, labels fixed
Deploy Dashboard (Dev) / Backend Tests (push) Successful in 1m14s
Deploy Dashboard (Dev) / Frontend Tests (push) Successful in 50s
Deploy Dashboard (Dev) / Build Dev Image (push) Failing after 12m34s
Deploy Dashboard (Dev) / Deploy to Dev (push) Has been skipped
Deploy Claude Dev (Dev) / deploy-claude-dev-dev (push) Has been cancelled
2026-06-07 16:21:36 +08:00
Gan, Jimmy d9f4241c9d ci: fresh trigger after Gitea restart
Deploy Dashboard (Dev) / Backend Tests (push) Successful in 1m16s
Deploy Claude Dev (Dev) / deploy-claude-dev-dev (push) Has been cancelled
Deploy Dashboard (Dev) / Build Dev Image (push) Failing after 11m48s
Deploy Dashboard (Dev) / Frontend Tests (push) Failing after 13m30s
Deploy Dashboard (Dev) / Deploy to Dev (push) Has been skipped
2026-06-07 16:15:36 +08:00
Gan, Jimmy e11fd666d2 ci: trigger fresh run after Gitea restart
Deploy Dashboard (Dev) / Frontend Tests (push) Successful in 1m0s
Deploy Dashboard (Dev) / Backend Tests (push) Failing after 13m40s
Deploy Dashboard (Dev) / Deploy to Dev (push) Has been skipped
Deploy Dashboard (Dev) / Build Dev Image (push) Has been skipped
2026-06-07 16:07:38 +08:00
Gan, Jimmy 9f674e52a2 ci: fresh trigger
Deploy Dashboard (Dev) / Backend Tests (push) Successful in 1m40s
Deploy Dashboard (Dev) / Build Dev Image (push) Failing after 10m22s
Deploy Dashboard (Dev) / Frontend Tests (push) Failing after 12m22s
Deploy Dashboard (Dev) / Deploy to Dev (push) Has been skipped
2026-06-07 12:38:59 +08:00
Gan, Jimmy b7388f9a43 fix(ci): use vps label without host suffix
Deploy Dashboard (Dev) / Frontend Tests (push) Failing after 11m57s
Deploy Dashboard (Dev) / Backend Tests (push) Failing after 14m14s
Deploy Dashboard (Dev) / Build Dev Image (push) Has been skipped
Deploy Dashboard (Dev) / Deploy to Dev (push) Has been skipped
2026-06-07 12:24:00 +08:00
Gan, Jimmy 3e0941fef0 fix(ci): remove concurrency cancel-in-progress to let runs complete
Deploy Dashboard (Dev) / Frontend Tests (push) Successful in 56s
Deploy Dashboard (Dev) / Backend Tests (push) Failing after 13m19s
Deploy Dashboard (Dev) / Build Dev Image (push) Has been skipped
Deploy Dashboard (Dev) / Deploy to Dev (push) Has been skipped
2026-06-07 12:20:23 +08:00
Gan, Jimmy d17f5383ae fix(ci): checkout dev branch explicitly via GITHUB_REF_NAME
Deploy Dashboard (Dev) / Frontend Tests (push) Successful in 1m23s
Deploy Dashboard (Dev) / Backend Tests (push) Failing after 10m52s
Deploy Dashboard (Dev) / Build Dev Image (push) Has been skipped
Deploy Dashboard (Dev) / Deploy to Dev (push) Has been skipped
2026-06-07 12:17:58 +08:00
Gan, Jimmy ce0a800087 fix(ci): match runner label vps:host, fix pytest timeout flag
Deploy Dashboard (Dev) / Backend Tests (push) Successful in 1m16s
Deploy Dashboard (Dev) / Frontend Tests (push) Has been cancelled
Deploy Dashboard (Dev) / Deploy to Dev (push) Has been cancelled
Deploy Dashboard (Dev) / Build Dev Image (push) Has been cancelled
2026-06-07 12:08:16 +08:00
Gan, Jimmy 98510cb707 fix(ci): remove --timeout flag from pytest (not compatible with installed version)
Deploy Dashboard (Dev) / Backend Tests (push) Successful in 1m27s
Deploy Dashboard (Dev) / Deploy to Dev (push) Has been cancelled
Deploy Dashboard (Dev) / Frontend Tests (push) Has been cancelled
Deploy Dashboard (Dev) / Build Dev Image (push) Has been cancelled
2026-06-07 12:04:29 +08:00
Gan, Jimmy 5c5ae99e79 fix(ci): change deploy job to vps runner, route Docker commands via ssh nasts
Deploy Dashboard (Dev) / Backend Tests (push) Successful in 1m20s
Deploy Dashboard (Dev) / Frontend Tests (push) Successful in 1m1s
Deploy Dashboard (Dev) / Deploy to Dev (push) Has been cancelled
Deploy Dashboard (Dev) / Build Dev Image (push) Has been cancelled
2026-06-07 11:57:46 +08:00
Gan, Jimmy 59dd8ae1e2 ci: trigger dashboard deploy
Deploy Dashboard (Dev) / Frontend Tests (push) Has been cancelled
Deploy Dashboard (Dev) / Build Dev Image (push) Has been cancelled
Deploy Dashboard (Dev) / Deploy to Dev (push) Has been cancelled
Deploy Dashboard (Dev) / Backend Tests (push) Has been cancelled
2026-06-07 11:53:12 +08:00
Gan, Jimmy b52986fed5 fix(ci): recreate venv after cache restore to fix absolute symlinks
Deploy Dashboard (Dev) / Frontend Tests (push) Successful in 51s
Deploy Dashboard (Dev) / Backend Tests (push) Failing after 13m39s
Deploy Dashboard (Dev) / Build Dev Image (push) Has been skipped
Deploy Dashboard (Dev) / Deploy to Dev (push) Has been skipped
Running python3 -m venv venv over an existing cached venv recreates
the absolute symlinks and pyvenv.cfg paths so Python resolves to
the current system's python3 binary. Without this, cp -a preserves
stale symlinks from the cache source, causing venv/bin/python to
resolve to /usr/bin/python3 (system, not venv) and pytest not found.

Also switch validation to use python3 to match Ubuntu 24.04 convention.
2026-06-04 23:52:40 +08:00
Gan, Jimmy c041b0e7ec fix(ci): use python3 instead of python - VPS Ubuntu 24.04 has no python symlink
Deploy Dashboard (Dev) / Frontend Tests (push) Failing after 12m44s
Deploy Dashboard (Dev) / Backend Tests (push) Failing after 13m57s
Deploy Dashboard (Dev) / Build Dev Image (push) Has been skipped
Deploy Dashboard (Dev) / Deploy to Dev (push) Has been skipped
2026-06-04 20:23:57 +08:00
Gan, Jimmy 6672999757 fix(ci): validate venv cache by testing pytest, use python -m pytest
Deploy Dashboard (Dev) / Backend Tests (push) Failing after 17s
Deploy Dashboard (Dev) / Build Dev Image (push) Has been skipped
Deploy Dashboard (Dev) / Deploy to Dev (push) Has been cancelled
Deploy Dashboard (Dev) / Frontend Tests (push) Has been cancelled
2026-06-04 20:20:30 +08:00
Gan, Jimmy 12a015e915 fix(ci): move all test jobs to VPS runner - NAS HDD too slow
Deploy Dashboard (Dev) / Frontend Tests (push) Failing after 13m0s
Deploy Dashboard (Dev) / Backend Tests (push) Failing after 14m11s
Deploy Dashboard (Dev) / Build Dev Image (push) Has been skipped
Deploy Dashboard (Dev) / Deploy to Dev (push) Has been skipped
NAS is using 4.7GB swap causing tests to hang in D state for 20+ min. VPS has SSD and plenty of RAM - tests complete in ~1-2 min there.
2026-06-04 18:17:18 +08:00
Gan, Jimmy 3057fc270b fix(ci): move all test jobs to VPS runner - NAS HDD too slow
NAS is using 4.7GB swap causing tests to hang in D state for 20+ min. VPS has SSD and plenty of RAM - tests complete in ~1-2 min there.
2026-06-04 18:16:36 +08:00
Gan, Jimmy c4e9608e9d fix(ci): use Docker bridge gateway IP for checkout in dev CI
Deploy Dashboard (Dev) / Frontend Tests (push) Has been cancelled
Deploy Dashboard (Dev) / Build Dev Image (push) Has been cancelled
Deploy Dashboard (Dev) / Deploy to Dev (push) Has been cancelled
Deploy Dashboard (Dev) / Backend Tests (push) Has been cancelled
Ubuntu-latest runner containers on NAS dont have the gitea_gitea network, so http://gitea:3000 is unreachable. Use 172.21.0.1:3300 (Docker bridge gateway) instead.
2026-06-04 17:58:50 +08:00
Gan, Jimmy aa08b4c970 feat(ci): build dashboard dev images on VPS (SSD) instead of NAS HDD
Deploy Dashboard (Dev) / Backend Tests (push) Successful in 17m47s
Deploy Dashboard (Dev) / Frontend Tests (push) Successful in 44s
Deploy Dashboard (Dev) / Build Dev Image (push) Failing after 26m26s
Deploy Dashboard (Dev) / Deploy to Dev (push) Has been skipped
Add build-image-dev job (runs-on: vps) - builds with Podman on server2,
pipes the finished image to NAS via SSH, retags from localhost/ prefix.
Deploy job now depends on build-image-dev instead of backend-tests.

NAS HDD I/O was the bottleneck for Docker builds. VPS has SSD and
matches NAS x86_64 architecture perfectly.
2026-06-04 17:51:41 +08:00
14 changed files with 144 additions and 105 deletions
+1 -1
View File
@@ -22,7 +22,7 @@ jobs:
- name: Checkout repository - name: Checkout repository
run: | run: |
if [ ! -d .git ]; then if [ ! -d .git ]; then
git clone --branch "${GITHUB_REF_NAME}" --depth=1 "http://172.21.0.1:3300/${GITHUB_REPOSITORY}.git" . git clone --branch "${GITHUB_REF_NAME}" --depth=1 "http://gitea:3000/${GITHUB_REPOSITORY}.git" .
fi fi
- name: Resolve image tags - name: Resolve image tags
+43 -67
View File
@@ -7,14 +7,10 @@ on:
- '.gitea/workflows/deploy-dev.yml' - '.gitea/workflows/deploy-dev.yml'
workflow_dispatch: workflow_dispatch:
concurrency:
group: deploy-dashboard-dev
cancel-in-progress: true
jobs: jobs:
backend-tests: backend-tests:
name: Backend Tests name: Backend Tests
runs-on: ubuntu-latest runs-on: vps
env: env:
SECRET_KEY: test-secret-key-for-ci-environment-32chars-minimum SECRET_KEY: test-secret-key-for-ci-environment-32chars-minimum
@@ -22,7 +18,7 @@ jobs:
- name: Checkout repository - name: Checkout repository
run: | run: |
if [ ! -d .git ]; then if [ ! -d .git ]; then
git clone --depth=1 "http://gitea:3000/${GITHUB_REPOSITORY}.git" . git clone --depth=1 --branch "$GITHUB_REF_NAME" "http://100.78.131.124:3300/${GITHUB_REPOSITORY}.git" .
fi fi
- name: Setup Python - name: Setup Python
@@ -54,7 +50,7 @@ jobs:
cd dashboard/backend cd dashboard/backend
if [ "${{ steps.cache-python.outputs.cache-hit }}" = "true" ]; then if [ "${{ steps.cache-python.outputs.cache-hit }}" = "true" ]; then
echo "Restoring venv from cache $CACHE_KEY..." echo "Restoring venv from cache $CACHE_KEY..."
if cp -a $CACHE_DIR/venv . && [ -f venv/bin/activate ]; then if cp -a $CACHE_DIR/venv . && [ -f venv/bin/activate ] && python3 -m venv venv && venv/bin/python3 -m pytest --version >/dev/null 2>&1; then
echo "Cache restored successfully" echo "Cache restored successfully"
else else
echo "Cache corrupted, installing fresh..." echo "Cache corrupted, installing fresh..."
@@ -88,7 +84,7 @@ jobs:
run: | run: |
cd dashboard/backend cd dashboard/backend
. venv/bin/activate . venv/bin/activate
pytest tests/ -v --timeout=60 python3 -m pytest tests/ -v
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
echo "❌ Backend tests failed!" echo "❌ Backend tests failed!"
exit 1 exit 1
@@ -97,13 +93,13 @@ jobs:
frontend-tests: frontend-tests:
name: Frontend Tests name: Frontend Tests
runs-on: ubuntu-latest runs-on: vps
steps: steps:
- name: Checkout repository - name: Checkout repository
run: | run: |
if [ ! -d .git ]; then if [ ! -d .git ]; then
git clone --depth=1 "http://gitea:3000/${GITHUB_REPOSITORY}.git" . git clone --depth=1 --branch "$GITHUB_REF_NAME" "http://100.78.131.124:3300/${GITHUB_REPOSITORY}.git" .
fi fi
- name: Setup Node.js - name: Setup Node.js
@@ -166,18 +162,15 @@ jobs:
- name: Checkout repository - name: Checkout repository
run: | run: |
if [ ! -d .git ]; then if [ ! -d .git ]; then
git clone --depth=1 "http://100.78.131.124:3300/${GITHUB_REPOSITORY}.git" . git clone --depth=1 --branch "$GITHUB_REF_NAME" "http://100.78.131.124:3300/${GITHUB_REPOSITORY}.git" .
fi fi
- name: Build dev Docker image - name: Build dev Docker image
run: | run: |
set -e set -e
echo "Building on VPS (SSD)..." echo "Building on VPS (SSD)..."
CACHE_ARG="" # Podman caches layers automatically; no --cache-from needed
if podman image exists nas-dashboard-dev:latest 2>/dev/null; then podman build -t nas-dashboard-dev:latest dashboard/
CACHE_ARG="--cache-from nas-dashboard-dev:latest"
fi
podman build $CACHE_ARG -t nas-dashboard-dev:latest dashboard/
- name: Transfer image to NAS - name: Transfer image to NAS
run: | run: |
@@ -190,7 +183,7 @@ jobs:
deploy-dev: deploy-dev:
name: Deploy to Dev name: Deploy to Dev
runs-on: ubuntu-latest runs-on: vps
needs: [build-image-dev, frontend-tests] needs: [build-image-dev, frontend-tests]
env: env:
SECRET_KEY: test-secret-key-for-ci-environment-32chars-minimum SECRET_KEY: test-secret-key-for-ci-environment-32chars-minimum
@@ -199,11 +192,12 @@ jobs:
- /var/packages/ContainerManager/target/usr/bin/docker:/usr/bin/docker - /var/packages/ContainerManager/target/usr/bin/docker:/usr/bin/docker
- /var/packages/ContainerManager/target/usr/bin/docker-compose:/usr/lib/docker/cli-plugins/docker-compose - /var/packages/ContainerManager/target/usr/bin/docker-compose:/usr/lib/docker/cli-plugins/docker-compose
- /volume1/docker/nas-dashboard:/volume1/docker/nas-dashboard - /volume1/docker/nas-dashboard:/volume1/docker/nas-dashboard
steps: steps:
- name: Checkout repository - name: Checkout repository
run: | run: |
if [ ! -d .git ]; then if [ ! -d .git ]; then
git clone --depth=1 "http://gitea:3000/${GITHUB_REPOSITORY}.git" . git clone --depth=1 --branch "$GITHUB_REF_NAME" "http://gitea:3000/${GITHUB_REPOSITORY}.git" .
fi fi
- name: Sync runtime compose file - name: Sync runtime compose file
@@ -212,67 +206,50 @@ jobs:
cp dashboard/docker-compose.dev.yml /volume1/docker/nas-dashboard/docker-compose.dev.yml cp dashboard/docker-compose.dev.yml /volume1/docker/nas-dashboard/docker-compose.dev.yml
- name: Deploy dev - name: Deploy dev
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
SECRET_KEY: ${{ secrets.SECRET_KEY }}
ADMIN_PASSWORD_HASH: ${{ secrets.ADMIN_PASSWORD_HASH }}
GITEA_DB_PASSWORD: ${{ secrets.GITEA_DB_PASSWORD }}
run: | run: |
set -u set -u
export DOCKER_API_VERSION=1.43 DOCKER="/volume1/@appstore/ContainerManager/usr/bin/docker"
COMPOSE="/usr/lib/docker/cli-plugins/docker-compose"
NAS_VOL="/volume1/docker/nas-dashboard"
cd /volume1/docker/nas-dashboard # Sync compose file to NAS (already done in previous step, just in case)
mkdir -p $NAS_VOL
# Stop existing container if running # Stop existing container if running
docker compose -f docker-compose.dev.yml -p nas-dashboard-dev down || true $COMPOSE -f $NAS_VOL/docker-compose.dev.yml -p nas-dashboard-dev down 2>/dev/null || true
# Ensure network exists (create only if it doesn't exist) # Ensure network exists
if ! docker network inspect nas-dashboard_internal >/dev/null 2>&1; then if ! $DOCKER network inspect nas-dashboard_internal >/dev/null 2>&1; then
docker network create nas-dashboard_internal $DOCKER network create nas-dashboard_internal
fi fi
# Docker 24 compose fails when it can't connect external networks # Deploy
# during container creation. Try compose first; if the container cd $NAS_VOL && GITEA_TOKEN=$GITEA_TOKEN SECRET_KEY=$SECRET_KEY $COMPOSE -f docker-compose.dev.yml -p nas-dashboard-dev up -d --pull never 2>&1 || true
# isn't created, strip networks from compose file and retry.
docker compose -f docker-compose.dev.yml -p nas-dashboard-dev up -d --pull never 2>&1 || true
if ! docker container inspect nas-dashboard-dev >/dev/null 2>&1; then if ! $DOCKER container inspect nas-dashboard-dev >/dev/null 2>&1; then
echo "Compose failed to create container, stripping networks from compose file..." echo "Compose failed to create container..."
python3 -c " exit 1
import os
lines = open('docker-compose.dev.yml').readlines()
out = []
skip_net = False
for l in lines:
stripped = l.strip()
if stripped == 'networks:' and not l[0].isspace():
skip_net = True
continue
if skip_net:
if l[0].isspace():
continue
skip_net = False
if stripped == 'networks:' and l[:4].strip() == '':
skip_net = True
continue
if skip_net:
if l.startswith(' '):
continue
skip_net = False
out.append(l)
open('/tmp/ci.yml','w').writelines(out)
"
docker compose -f /tmp/ci.yml -p nas-dashboard-dev up -d --pull never
fi fi
# Connect networks after container is created (idempotent) # Connect networks after container is created (idempotent)
docker network connect nas-dashboard_internal nas-dashboard-dev 2>/dev/null || true $DOCKER network connect nas-dashboard_internal nas-dashboard-dev 2>/dev/null || true
docker network connect gitea_gitea nas-dashboard-dev 2>/dev/null || true $DOCKER network connect gitea_gitea nas-dashboard-dev 2>/dev/null || true
# Verify both connections # Verify both connections
echo "=== Network connections ===" echo "=== Network connections ==="
docker inspect nas-dashboard-dev --format '{{json .NetworkSettings.Networks}}' | python3 -c 'import json,sys;nets=json.load(sys.stdin);[print(f" Connected to {n}") for n in nets];assert "nas-dashboard_internal" in nets and "gitea_gitea" in nets, "Missing required network";print("All network connections established")' $DOCKER inspect nas-dashboard-dev --format '{{json .NetworkSettings.Networks}}' | python3 -c 'import json,sys;nets=json.load(sys.stdin);[print(f" Connected to {n}") for n in nets];assert "nas-dashboard_internal" in nets and "gitea_gitea" in nets, "Missing required network";print("All network connections established")'
- name: Wait for container to be healthy - name: Wait for container to be healthy
run: | run: |
DOCKER="/volume1/@appstore/ContainerManager/usr/bin/docker"
echo "Waiting for container to be healthy..." echo "Waiting for container to be healthy..."
for i in {1..30}; do for i in {1..30}; do
STATUS=$(docker inspect --format='{{.State.Health.Status}}' nas-dashboard-dev 2>/dev/null || echo "starting") STATUS=$($DOCKER inspect --format='{{.State.Health.Status}}' nas-dashboard-dev 2>/dev/null || echo "starting")
if [ "$STATUS" = "healthy" ]; then if [ "$STATUS" = "healthy" ]; then
echo "✅ Container is healthy" echo "✅ Container is healthy"
break break
@@ -280,28 +257,29 @@ jobs:
echo "Waiting... ($i/30) Status: $STATUS" echo "Waiting... ($i/30) Status: $STATUS"
sleep 2 sleep 2
done done
STATUS=$(docker inspect --format='{{.State.Health.Status}}' nas-dashboard-dev 2>/dev/null || echo "unknown") STATUS=$($DOCKER inspect --format='{{.State.Health.Status}}' nas-dashboard-dev 2>/dev/null || echo "unknown")
if [ "$STATUS" != "healthy" ]; then if [ "$STATUS" != "healthy" ]; then
echo "❌ Container failed to become healthy: $STATUS" echo "❌ Container failed to become healthy: $STATUS"
docker logs nas-dashboard-dev --tail 50 $DOCKER logs nas-dashboard-dev --tail 50
exit 1 exit 1
fi fi
- name: Run smoke tests - name: Run smoke tests
run: | run: |
DOCKER="/volume1/@appstore/ContainerManager/usr/bin/docker"
echo "=== Running smoke tests via docker exec ===" echo "=== Running smoke tests via docker exec ==="
# Test 1: Health check # Test 1: Health check
echo "1. Testing health endpoint..." echo "1. Testing health endpoint..."
docker exec nas-dashboard-dev python3 -c "import urllib.request,json; d=json.loads(urllib.request.urlopen('http://localhost:4000/api/health',timeout=5).read()); assert d['status']=='ok', f'unexpected: {d}'" && echo "✅ Health check passed" || { echo "❌ Health check failed"; docker logs nas-dashboard-dev --tail 50; exit 1; } $DOCKER exec nas-dashboard-dev python3 -c "import urllib.request,json; d=json.loads(urllib.request.urlopen('http://localhost:4000/api/health',timeout=5).read()); assert d['status']=='ok', f'unexpected: {d}'" && echo "✅ Health check passed" || { echo "❌ Health check failed"; $DOCKER logs nas-dashboard-dev --tail 50; exit 1; }
# Test 2: Frontend loads # Test 2: Frontend loads
echo "2. Testing frontend loads..." echo "2. Testing frontend loads..."
docker exec nas-dashboard-dev python3 -c "import urllib.request; html=urllib.request.urlopen('http://localhost:4000/',timeout=5).read().decode(); assert 'NAS Dashboard' in html, f'missing NAS Dashboard'" && echo "✅ Frontend loads" || { echo "❌ Frontend failed to load"; exit 1; } $DOCKER exec nas-dashboard-dev python3 -c "import urllib.request; html=urllib.request.urlopen('http://localhost:4000/',timeout=5).read().decode(); assert 'NAS Dashboard' in html, f'missing NAS Dashboard'" && echo "✅ Frontend loads" || { echo "❌ Frontend failed to load"; exit 1; }
# Test 3: API endpoints require auth # Test 3: API endpoints require auth
echo "3. Testing API endpoints require auth..." echo "3. Testing API endpoints require auth..."
docker exec nas-dashboard-dev python3 -c " $DOCKER exec nas-dashboard-dev python3 -c "
import urllib.request, json import urllib.request, json
try: try:
resp = urllib.request.urlopen('http://localhost:4000/api/conversations/dates', timeout=5) resp = urllib.request.urlopen('http://localhost:4000/api/conversations/dates', timeout=5)
@@ -313,7 +291,7 @@ jobs:
# Test 4: Double /api/ prefix bug (non-failing) # Test 4: Double /api/ prefix bug (non-failing)
echo "4. Testing for double /api/ prefix bug..." echo "4. Testing for double /api/ prefix bug..."
docker exec nas-dashboard-dev python3 -c " $DOCKER exec nas-dashboard-dev python3 -c "
import urllib.request import urllib.request
try: try:
urllib.request.urlopen('http://localhost:4000/api/api/conversations/dates', timeout=5) urllib.request.urlopen('http://localhost:4000/api/api/conversations/dates', timeout=5)
@@ -323,7 +301,5 @@ jobs:
print('OK: /api/api/ returned 404') print('OK: /api/api/ returned 404')
" && echo "✅ No double /api/ prefix bug" || echo "⚠️ Double API prefix warning" " && echo "✅ No double /api/ prefix bug" || echo "⚠️ Double API prefix warning"
# Test 5: Container health already verified in previous step
echo "" echo ""
echo "=== All smoke tests passed! ===" echo "=== All smoke tests passed! ==="
+13 -11
View File
@@ -168,8 +168,8 @@ jobs:
fi fi
echo "✅ Frontend tests passed" echo "✅ Frontend tests passed"
build-image: build-image-main:
name: Build Docker Image name: Build Main Image
runs-on: vps runs-on: vps
needs: [backend-tests] needs: [backend-tests]
@@ -199,8 +199,8 @@ jobs:
deploy: deploy:
name: Deploy to Production name: Deploy to Production
runs-on: nas runs-on: nas
if: always() && needs.build-image.result == 'success' if: always() && needs.build-image-main.result == 'success'
needs: [build-image, frontend-tests] needs: [build-image-main, frontend-tests]
env: env:
SECRET_KEY: test-secret-key-for-ci-environment-32chars-minimum SECRET_KEY: test-secret-key-for-ci-environment-32chars-minimum
GITEA_DB_PASSWORD: nyM3P4v1aYAsT7zfUtdguImNVHgFltCKCY/OjzWZVRE= GITEA_DB_PASSWORD: nyM3P4v1aYAsT7zfUtdguImNVHgFltCKCY/OjzWZVRE=
@@ -222,12 +222,16 @@ jobs:
cp dashboard/docker-compose.yml /nas-dashboard/docker-compose.yml cp dashboard/docker-compose.yml /nas-dashboard/docker-compose.yml
- name: Deploy and verify - name: Deploy and verify
env:
GITEA_TOKEN: ${{ secrets.CI_TOKEN }}
GITEA_DB_PASSWORD: ${{ secrets.CI_DB_PASSWORD }}
SECRET_KEY: ${{ secrets.SECRET_KEY }}
ADMIN_PASSWORD_HASH: ${{ secrets.ADMIN_PASSWORD_HASH }}
run: | run: |
set -e set -e
export DOCKER_API_VERSION=1.43 export DOCKER_API_VERSION=1.43
# Stop and remove old containers to force recreate with new image. # Stop and remove old containers to force recreate with new image.
# External networks (gitea_gitea, nas-dashboard_internal) are preserved.
docker compose -f /nas-dashboard/docker-compose.yml down 2>/dev/null || true docker compose -f /nas-dashboard/docker-compose.yml down 2>/dev/null || true
docker rm -f docker-socket-proxy nas-dashboard 2>/dev/null || true docker rm -f docker-socket-proxy nas-dashboard 2>/dev/null || true
echo "Old containers stopped and removed" echo "Old containers stopped and removed"
@@ -259,8 +263,8 @@ jobs:
--health-retries 3 \ --health-retries 3 \
-e DOCKER_HOST=tcp://docker-socket-proxy:2375 \ -e DOCKER_HOST=tcp://docker-socket-proxy:2375 \
-e GITEA_URL=http://gitea:3000 \ -e GITEA_URL=http://gitea:3000 \
-e GITEA_TOKEN=${GITEA_TOKEN} \ -e GITEA_TOKEN=$GITEA_TOKEN \
-e GITEA_DB_PASSWORD=${GITEA_DB_PASSWORD} \ -e GITEA_DB_PASSWORD=$GITEA_DB_PASSWORD \
-e VOLUME_ROOT=/volume1 \ -e VOLUME_ROOT=/volume1 \
-e SSH_HOST=host.docker.internal \ -e SSH_HOST=host.docker.internal \
-e SSH_USER=zjgump \ -e SSH_USER=zjgump \
@@ -273,11 +277,9 @@ jobs:
-e CORS_ORIGINS=https://nas.jimmygan.com \ -e CORS_ORIGINS=https://nas.jimmygan.com \
-e WEBAUTHN_RP_ID=jimmygan.com \ -e WEBAUTHN_RP_ID=jimmygan.com \
-e WEBAUTHN_ORIGINS=https://nas.jimmygan.com,https://nas.jimmygan.com:8443,https://auth.jimmygan.com:8443 \ -e WEBAUTHN_ORIGINS=https://nas.jimmygan.com,https://nas.jimmygan.com:8443,https://auth.jimmygan.com:8443 \
-e SECRET_KEY=${SECRET_KEY} \ -e SECRET_KEY=$SECRET_KEY \
-e ADMIN_USER=jimmy \ -e ADMIN_USER=jimmy \
-e ADMIN_PASSWORD_HASH=${ADMIN_PASSWORD_HASH} \ -e ADMIN_PASSWORD_HASH=$ADMIN_PASSWORD_HASH \
-e TRANSMISSION_USER=${TRANSMISSION_USER:-admin} \
-e TRANSMISSION_PASS=${TRANSMISSION_PASS:-admin} \
-v /volume1:/volume1 \ -v /volume1:/volume1 \
-v /volume1/docker/nas-dashboard/ssh/dashboard_terminal:/app/ssh/id_ed25519:ro \ -v /volume1/docker/nas-dashboard/ssh/dashboard_terminal:/app/ssh/id_ed25519:ro \
-v /volume1/docker/nas-dashboard/ssh/vps_terminal:/app/ssh/vps_id_ed25519:ro \ -v /volume1/docker/nas-dashboard/ssh/vps_terminal:/app/ssh/vps_id_ed25519:ro \
+1
View File
@@ -31,3 +31,4 @@ venv/
.env.local .env.local
.env.production .env.production
dashboard/backend/.vite/ dashboard/backend/.vite/
dashboard/frontend/coverage/
+9 -4
View File
@@ -13,10 +13,15 @@
- Sidebar: categorized as Main (Overview, Docker, Files, Terminal), Media (Navidrome, Jellyfin, Audiobookshelf, Immich), Tools (OpenClaw, Repos, Gitea Web, Stirling PDF, Vaultwarden, Speedtest) - Sidebar: categorized as Main (Overview, Docker, Files, Terminal), Media (Navidrome, Jellyfin, Audiobookshelf, Immich), Tools (OpenClaw, Repos, Gitea Web, Stirling PDF, Vaultwarden, Speedtest)
- New pages: create route in `src/routes/`, import in `App.svelte`, add to appropriate category in `Sidebar.svelte` - New pages: create route in `src/routes/`, import in `App.svelte`, add to appropriate category in `Sidebar.svelte`
## Build & Deploy ## Authentication
- Build images on Mac: `docker build --platform linux/amd64` - **Gitea API Access:** Use a dedicated `claude-ci-bot` user with a Personal Access Token (PAT).
- Transfer: `docker save <image> | ssh zjgump@100.78.131.124 "/volume1/@appstore/ContainerManager/usr/bin/docker load"` - **Secret Management:** Store the PAT in an environment variable `GITEA_TOKEN`. Avoid hardcoding paths to secret files.
- Deploy: `git push` triggers Gitea Actions CI (`docker compose up -d --pull never`) — do NOT manually restart - **Project Secrets:** Keep sensitive configurations (like the PAT) in a local `.env` file that is ignored by git.
- Build images on VPS (server2) to avoid NAS HDD I/O saturation:
1. Build on VPS with Podman
2. Stream image to NAS: `podman save <image> | ssh nasts "/volume1/@appstore/ContainerManager/usr/bin/docker load"`
3. Deploy: `git push` triggers Gitea Actions CI (builds on VPS, transfers, then `docker compose up`) — do NOT manually restart
- CI workflows are in `.gitea/workflows/` in this repo - CI workflows are in `.gitea/workflows/` in this repo
## claude-dev CI Contract ## claude-dev CI Contract
+6
View File
@@ -0,0 +1,6 @@
ANTHROPIC_API_KEY=sk-your-deepseek-api-key
ANTHROPIC_BASE_URL=http://localhost:4008
ANTHROPIC_MODEL=deepseek-v4-pro
ANTHROPIC_SMALL_FAST_MODEL=deepseek-v4-flash
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1
CLAUDE_DEV_IMAGE_TAG=latest
+7 -1
View File
@@ -5,6 +5,12 @@ WORKDIR /repos/nas-tools
COPY required-tools.txt /opt/claude-dev/required-tools.txt COPY required-tools.txt /opt/claude-dev/required-tools.txt
COPY scripts /opt/claude-dev/scripts COPY scripts /opt/claude-dev/scripts
# LiteLLM translation proxy config
RUN mkdir -p /etc/claude-dev
COPY config/litellm.yaml /etc/claude-dev/litellm.yaml
COPY scripts/start-litellm.sh /opt/claude-dev/scripts/start-litellm.sh
RUN chmod +x /opt/claude-dev/scripts/*.sh RUN chmod +x /opt/claude-dev/scripts/*.sh
CMD ["bash"] # Start LiteLLM proxy, then bash
CMD ["/opt/claude-dev/scripts/start-litellm.sh"]
+35 -18
View File
@@ -14,13 +14,16 @@ RUN apt-get update \
make \ make \
openssh-client \ openssh-client \
python3 \ python3 \
python3-pip \
python3-venv \
ripgrep \ ripgrep \
rsync \ rsync \
tmux \ tmux \
unzip \ unzip \
wget \ wget \
zip \ zip \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/* \
&& pip3 install --break-system-packages --no-cache-dir "litellm[proxy]" -i https://pypi.tuna.tsinghua.edu.cn/simple
RUN ln -sf /usr/bin/fdfind /usr/local/bin/fd RUN ln -sf /usr/bin/fdfind /usr/local/bin/fd
@@ -34,27 +37,41 @@ RUN wget -q -O /usr/local/bin/tea "https://gitea.com/gitea/tea/releases/download
RUN npm install -g @anthropic-ai/claude-code RUN npm install -g @anthropic-ai/claude-code
# Preflight bypass: replace all Anthropic URLs in the bundled binary with localhost:4008
# (LiteLLM proxy). This is a binary-level patch since the bundled cli.js is minified.
RUN python3 - <<'PY' RUN python3 - <<'PY'
from pathlib import Path path = "/usr/local/lib/node_modules/@anthropic-ai/claude-code/cli.js"
import re with open(path, "rb") as f:
d = bytearray(f.read())
path = Path("/usr/local/lib/node_modules/@anthropic-ai/claude-code/cli.js") replacements = [
if not path.exists(): (b"https://api.anthropic.com", b"http://localhost:4008"),
raise SystemExit(f"claude cli not found: {path}") (b"https://platform.claude.com", b"http://localhost:4008"),
(b"https://claude.ai", b"http://localhost:4008"),
(b"api.anthropic.com", b"localhost:4008"),
(b"platform.claude.com", b"localhost:4008"),
(b"claude.ai", b"localhost:4008"),
(b"anthropic.com", b"localhost:4008"),
]
total = 0
for old, new in replacements:
c = d.count(old)
if c:
d = d.replace(old, new)
total += c
d = d.replace(b"https://localhost:4008", b"http://localhost:4008")
d = d.replace(b"http://localhost:4008:4008", b"http://localhost:4008")
content = path.read_text() with open(path, "wb") as f:
pattern = r'let A=U7\(\),q=new URL\(A\.TOKEN_URL\),K=\[`\$\{A\.BASE_API_URL\}/api/hello`,`\$\{q\.origin\}/v1/oauth/hello`\],Y=async\(_\)=>\{try\{let \$=await I8\.get\(_,\{headers:\{"User-Agent":ey\(\)\}\}\);if\(\$\.status!==200\)return\{success:!1,error:`Failed to connect to \$\{new URL\(_\)\.hostname\}: Status \$\{\$\.status\}`\};return\{success:!0\}\}catch\(\$\)\{' f.write(d)
replacement = 'return'
patched, count = re.subn(pattern, replacement, content, count=1)
if count != 1:
raise SystemExit("preflight patch target not found in claude cli; refusing to build")
path.write_text(patched) # Verify
with open(path, "rb") as f:
if re.search(pattern, path.read_text()): d = f.read()
raise SystemExit("preflight patch did not apply cleanly") remaining = sum(d.count(p) for p in [b"api.anthropic.com", b"platform.claude.com"])
if remaining:
print("Applied Claude preflight bypass patch") raise SystemExit(f"Patch incomplete: {remaining} anthropic refs remain")
print(f"Patched {total} Anthropic URL refs → localhost:4008")
PY PY
CMD ["bash"] CMD ["bash"]
+12
View File
@@ -0,0 +1,12 @@
model_list:
- model_name: deepseek-v4-flash
litellm_params:
model: deepseek/deepseek-chat
api_key: os.environ/DEEPSEEK_API_KEY
- model_name: deepseek-v4-pro
litellm_params:
model: deepseek/deepseek-reasoner
api_key: os.environ/DEEPSEEK_API_KEY
general_settings:
disable_auth: true
+1 -1
View File
@@ -8,7 +8,7 @@ services:
tty: true tty: true
working_dir: /repos/nas-tools working_dir: /repos/nas-tools
env_file: env_file:
- /volume1/docker/claude-dev/.env - ./.env
volumes: volumes:
- /volume1/repos:/repos - /volume1/repos:/repos
- /volume1/docker/claude-dev/claude-config:/root/.claude - /volume1/docker/claude-dev/claude-config:/root/.claude
+1
View File
@@ -19,3 +19,4 @@ make
tmux tmux
tea tea
claude claude
litellm
+4
View File
@@ -0,0 +1,4 @@
#!/bin/sh
/usr/local/bin/litellm --config /etc/claude-dev/litellm.yaml --port 4008 --host 0.0.0.0 > /tmp/litellm.log 2>&1 &
sleep 2
exec bash "$@"
+9
View File
@@ -1 +1,10 @@
# Dashboard # Dashboard
NAS management dashboard with Torrent client integration, CI/CD deployed on Oracle VPS.
Trigger CI: 2026-06-07
Trigger clean CI: Sun Jun 21 23:30:52 CST 2026
+2 -2
View File
@@ -1,10 +1,10 @@
services: services:
litellm: litellm:
image: ghcr.io/berriai/litellm:main-latest image: ghcr.io/berriai/litellm:main-v1.16.19
container_name: litellm container_name: litellm
restart: unless-stopped restart: unless-stopped
labels: labels:
- "com.centurylinklabs.watchtower.enable=true" - "com.centurylinklabs.watchtower.enable=false"
env_file: env_file:
- .env - .env
environment: environment: