From a485c3004f46c394f1982e75231265da7f07eb35 Mon Sep 17 00:00:00 2001 From: GeekMr <140129782+WilliamWang1721@users.noreply.github.com> Date: Thu, 6 Aug 2026 12:40:37 +0800 Subject: [PATCH 1/7] ci: reproduce console blank screen --- .github/workflows/diagnose-console-blank.yml | 242 +++++++++++++++++++ 1 file changed, 242 insertions(+) create mode 100644 .github/workflows/diagnose-console-blank.yml diff --git a/.github/workflows/diagnose-console-blank.yml b/.github/workflows/diagnose-console-blank.yml new file mode 100644 index 00000000..cba8431c --- /dev/null +++ b/.github/workflows/diagnose-console-blank.yml @@ -0,0 +1,242 @@ +name: Diagnose published console rendering + +on: + push: + branches: + - agent/diagnose-console-blank + +permissions: + contents: read + +concurrency: + group: diagnose-console-blank + cancel-in-progress: true + +jobs: + browser-login: + name: ${{ matrix.name }} + runs-on: ubuntu-latest + timeout-minutes: 30 + strategy: + fail-fast: false + max-parallel: 2 + matrix: + include: + - name: ghcr-v0.3.6 + image: ghcr.io/williamwang1721/lightbridge:0.3.6 + - name: ghcr-v0.3.61 + image: ghcr.io/williamwang1721/lightbridge:0.3.61 + - name: dockerhub-v0.3.6 + image: williamwang1721/lightbridge:0.3.6 + - name: dockerhub-v0.3.61 + image: williamwang1721/lightbridge:0.3.61 + env: + TARGET_IMAGE: ${{ matrix.image }} + BASE_URL: http://127.0.0.1:18080 + ADMIN_EMAIL: console-diagnostic@lightbridge.local + ADMIN_PASSWORD: LightBridge-Console-Diagnostic-2026! + OUTPUT_DIR: /tmp/diagnostic-${{ matrix.name }} + COMPOSE_PROJECT_NAME: lb-console-${{ github.run_id }}-${{ strategy.job-index }} + + steps: + - name: Start exact published image + shell: bash + run: | + set -euo pipefail + mkdir -p "$OUTPUT_DIR" + cat >/tmp/console-diagnostic-compose.yml <<'YAML' + services: + postgres: + image: postgres:17-alpine + environment: + POSTGRES_USER: LightBridge + POSTGRES_PASSWORD: LightBridge-console-postgres-2026 + POSTGRES_DB: LightBridge + healthcheck: + test: ["CMD-SHELL", "pg_isready -U LightBridge -d LightBridge"] + interval: 2s + timeout: 5s + retries: 60 + redis: + image: redis:7-alpine + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 2s + timeout: 5s + retries: 60 + app: + image: ${TARGET_IMAGE:?TARGET_IMAGE is required} + pull_policy: always + ports: + - "127.0.0.1:18080:8080" + environment: + AUTO_SETUP: "true" + LIGHTBRIDGE_DEPLOYMENT_TYPE: container + SERVER_HOST: 0.0.0.0 + SERVER_PORT: "8080" + SERVER_MODE: release + RUN_MODE: standard + DATABASE_HOST: postgres + DATABASE_PORT: "5432" + DATABASE_USER: LightBridge + DATABASE_PASSWORD: LightBridge-console-postgres-2026 + DATABASE_DBNAME: LightBridge + DATABASE_SSLMODE: disable + REDIS_HOST: redis + REDIS_PORT: "6379" + REDIS_PASSWORD: "" + REDIS_DB: "0" + ADMIN_EMAIL: ${ADMIN_EMAIL:?ADMIN_EMAIL is required} + ADMIN_PASSWORD: ${ADMIN_PASSWORD:?ADMIN_PASSWORD is required} + JWT_SECRET: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef + TOTP_ENCRYPTION_KEY: abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789 + TZ: UTC + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + healthcheck: + test: ["CMD", "wget", "-q", "-T", "5", "-O", "-", "http://127.0.0.1:8080/health"] + interval: 3s + timeout: 6s + retries: 80 + YAML + + docker compose -f /tmp/console-diagnostic-compose.yml pull + docker compose -f /tmp/console-diagnostic-compose.yml up -d --wait --wait-timeout 240 + curl --fail --silent --show-error "$BASE_URL/health" | tee "$OUTPUT_DIR/health.json" + docker image inspect "$TARGET_IMAGE" >"$OUTPUT_DIR/image-inspect.json" + + - name: Login through a real browser and inspect the console + shell: bash + run: | + set -euo pipefail + cat >"$OUTPUT_DIR/diagnose.mjs" <<'JS' + import fs from 'node:fs/promises' + import { chromium } from 'playwright' + + const baseURL = process.env.BASE_URL + const email = process.env.ADMIN_EMAIL + const password = process.env.ADMIN_PASSWORD + const outputDir = process.env.OUTPUT_DIR + const events = [] + const pageErrors = [] + const failedRequests = [] + const errorResponses = [] + + const browser = await chromium.launch({ headless: true }) + const context = await browser.newContext({ viewport: { width: 1440, height: 1000 } }) + const page = await context.newPage() + + page.on('console', (message) => { + events.push({ type: `console:${message.type()}`, text: message.text() }) + }) + page.on('pageerror', (error) => { + pageErrors.push({ message: error.message, stack: error.stack || '' }) + }) + page.on('requestfailed', (request) => { + failedRequests.push({ + url: request.url(), + method: request.method(), + resourceType: request.resourceType(), + failure: request.failure()?.errorText || 'unknown', + }) + }) + page.on('response', (response) => { + if (response.status() >= 400) { + errorResponses.push({ + url: response.url(), + status: response.status(), + resourceType: response.request().resourceType(), + }) + } + }) + + let fatal = '' + try { + await page.goto(`${baseURL}/login`, { waitUntil: 'domcontentloaded', timeout: 60000 }) + await page.locator('#email').fill(email) + await page.locator('#password').fill(password) + const submit = page.locator('button[type="submit"]') + await submit.waitFor({ state: 'visible', timeout: 30000 }) + await submit.click() + await page.waitForURL(/\/admin\/dashboard(?:[/?#]|$)/, { timeout: 60000 }) + await page.waitForTimeout(10000) + } catch (error) { + fatal = error instanceof Error ? `${error.message}\n${error.stack || ''}` : String(error) + } + + const structure = await page.evaluate(() => { + const root = document.querySelector('#app') + const visibleText = document.body.innerText.trim() + return { + url: location.href, + title: document.title, + rootChildren: root?.children.length ?? 0, + rootHTMLLength: root?.innerHTML.length ?? 0, + bodyTextLength: visibleText.length, + bodyTextSample: visibleText.slice(0, 1000), + sidebarCount: document.querySelectorAll('#lightbridge-sidebar, .sidebar').length, + headerCount: document.querySelectorAll('.app-topbar').length, + mainCount: document.querySelectorAll('main.app-main').length, + startupError: document.body.innerText.includes('LightBridge could not start'), + } + }) + + await page.screenshot({ path: `${outputDir}/console.png`, fullPage: true }) + await fs.writeFile(`${outputDir}/page.html`, await page.content()) + const result = { + image: process.env.TARGET_IMAGE, + fatal, + structure, + pageErrors, + failedRequests, + errorResponses, + events, + } + await fs.writeFile(`${outputDir}/result.json`, JSON.stringify(result, null, 2)) + console.log(JSON.stringify(result, null, 2)) + await browser.close() + + const rendered = + !fatal && + structure.url.includes('/admin/dashboard') && + structure.rootChildren > 0 && + structure.sidebarCount > 0 && + structure.headerCount > 0 && + structure.mainCount > 0 && + structure.bodyTextLength > 50 && + !structure.startupError + + if (!rendered) process.exit(1) + JS + + docker run --rm --network host \ + -e BASE_URL -e ADMIN_EMAIL -e ADMIN_PASSWORD -e OUTPUT_DIR -e TARGET_IMAGE \ + -e PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 \ + -v "$OUTPUT_DIR:/work" \ + mcr.microsoft.com/playwright:v1.55.0-noble \ + bash -lc 'set -euo pipefail; mkdir -p /tmp/diag; cp /work/diagnose.mjs /tmp/diag/; cd /tmp/diag; npm init -y >/dev/null; npm install --no-audit --no-fund playwright@1.55.0 >/dev/null; node diagnose.mjs' + + - name: Collect service diagnostics + if: always() + shell: bash + run: | + set +e + docker compose -f /tmp/console-diagnostic-compose.yml ps -a >"$OUTPUT_DIR/compose-ps.txt" 2>&1 + docker compose -f /tmp/console-diagnostic-compose.yml logs --no-color >"$OUTPUT_DIR/compose.log" 2>&1 + + - name: Upload diagnostics + if: always() + uses: actions/upload-artifact@v4 + with: + name: console-${{ matrix.name }} + path: ${{ env.OUTPUT_DIR }} + if-no-files-found: error + retention-days: 2 + + - name: Cleanup + if: always() + shell: bash + run: docker compose -f /tmp/console-diagnostic-compose.yml down --volumes --remove-orphans || true From 4a8c7fc6927ffaa994c5c28b3488562f22c99277 Mon Sep 17 00:00:00 2001 From: GeekMr <140129782+WilliamWang1721@users.noreply.github.com> Date: Thu, 6 Aug 2026 12:43:41 +0800 Subject: [PATCH 2/7] fix: let Rollup avoid vendor chunk cycles --- frontend/vite.config.ts | 72 +++++++++-------------------------------- 1 file changed, 15 insertions(+), 57 deletions(-) diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 38770704..e02b716d 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -48,64 +48,22 @@ export default defineConfig(({ mode }) => { }), injectPublicSettings(backendUrl) ], - resolve: { - alias: { - '@': resolve(__dirname, 'src'), - // 使用 vue-i18n 运行时版本,避免 CSP unsafe-eval 问题 - 'vue-i18n': 'vue-i18n/dist/vue-i18n.runtime.esm-bundler.js' - } - }, - define: { - // 启用 vue-i18n JIT 编译,在 CSP 环境下处理消息插值 - // JIT 编译器生成 AST 对象而非 JS 代码,无需 unsafe-eval - __INTLIFY_JIT_COMPILATION__: true - }, - build: { - outDir: '../backend/internal/web/dist', - emptyOutDir: true, - rollupOptions: { - output: { - /** - * 手动分包配置 - * 分离第三方库并按功能合并应用代码,避免循环依赖 - */ - manualChunks(id: string) { - if (id.includes('node_modules')) { - // Vue 核心库 - if ( - id.includes('/vue/') || - id.includes('/vue-router/') || - id.includes('/pinia/') || - id.includes('/@vue/') - ) { - return 'vendor-vue' - } - - // UI 工具库(较大,单独分离) - if (id.includes('/@vueuse/') || id.includes('/xlsx/')) { - return 'vendor-ui' - } - - // 图表库 - if (id.includes('/chart.js/') || id.includes('/vue-chartjs/')) { - return 'vendor-chart' - } - - // 国际化 - if (id.includes('/vue-i18n/') || id.includes('/@intlify/')) { - return 'vendor-i18n' - } - - // 其他小型第三方库合并 - return 'vendor-misc' - } - - // 应用代码:按入口点自动分包,不手动干预 - // 这样可以避免循环依赖,同时保持合理的 chunk 数量 - } + resolve: { + alias: { + '@': resolve(__dirname, 'src'), + // 使用 vue-i18n 运行时版本,避免 CSP unsafe-eval 问题 + 'vue-i18n': 'vue-i18n/dist/vue-i18n.runtime.esm-bundler.js' } - } - }, + }, + define: { + // 启用 vue-i18n JIT 编译,在 CSP 环境下处理消息插值 + // JIT 编译器生成 AST 对象而非 JS 代码,无需 unsafe-eval + __INTLIFY_JIT_COMPILATION__: true + }, + build: { + outDir: '../backend/internal/web/dist', + emptyOutDir: true + }, server: { host: '0.0.0.0', port: devPort, From 6ec2f0c3ccef400ce58f12c51ce0b8addba99268 Mon Sep 17 00:00:00 2001 From: GeekMr <140129782+WilliamWang1721@users.noreply.github.com> Date: Thu, 6 Aug 2026 12:44:28 +0800 Subject: [PATCH 3/7] ci: verify console chunk-cycle fix --- .github/workflows/diagnose-console-blank.yml | 151 +++++++------------ 1 file changed, 54 insertions(+), 97 deletions(-) diff --git a/.github/workflows/diagnose-console-blank.yml b/.github/workflows/diagnose-console-blank.yml index cba8431c..2d6d3bdd 100644 --- a/.github/workflows/diagnose-console-blank.yml +++ b/.github/workflows/diagnose-console-blank.yml @@ -1,4 +1,4 @@ -name: Diagnose published console rendering +name: Verify console rendering fix on: push: @@ -14,32 +14,30 @@ concurrency: jobs: browser-login: - name: ${{ matrix.name }} runs-on: ubuntu-latest - timeout-minutes: 30 - strategy: - fail-fast: false - max-parallel: 2 - matrix: - include: - - name: ghcr-v0.3.6 - image: ghcr.io/williamwang1721/lightbridge:0.3.6 - - name: ghcr-v0.3.61 - image: ghcr.io/williamwang1721/lightbridge:0.3.61 - - name: dockerhub-v0.3.6 - image: williamwang1721/lightbridge:0.3.6 - - name: dockerhub-v0.3.61 - image: williamwang1721/lightbridge:0.3.61 + timeout-minutes: 35 env: - TARGET_IMAGE: ${{ matrix.image }} + TARGET_IMAGE: lightbridge:console-fix BASE_URL: http://127.0.0.1:18080 ADMIN_EMAIL: console-diagnostic@lightbridge.local ADMIN_PASSWORD: LightBridge-Console-Diagnostic-2026! - OUTPUT_DIR: /tmp/diagnostic-${{ matrix.name }} - COMPOSE_PROJECT_NAME: lb-console-${{ github.run_id }}-${{ strategy.job-index }} + OUTPUT_DIR: /tmp/console-diagnostic + COMPOSE_PROJECT_NAME: lb-console-${{ github.run_id }} steps: - - name: Start exact published image + - uses: actions/checkout@v6 + + - name: Build the fixed production image + shell: bash + run: | + set -euo pipefail + docker build \ + --tag "$TARGET_IMAGE" \ + --build-arg VERSION=console-fix \ + --build-arg COMMIT="$GITHUB_SHA" \ + . + + - name: Start the fixed image shell: bash run: | set -euo pipefail @@ -66,7 +64,7 @@ jobs: retries: 60 app: image: ${TARGET_IMAGE:?TARGET_IMAGE is required} - pull_policy: always + pull_policy: never ports: - "127.0.0.1:18080:8080" environment: @@ -103,12 +101,10 @@ jobs: retries: 80 YAML - docker compose -f /tmp/console-diagnostic-compose.yml pull docker compose -f /tmp/console-diagnostic-compose.yml up -d --wait --wait-timeout 240 curl --fail --silent --show-error "$BASE_URL/health" | tee "$OUTPUT_DIR/health.json" - docker image inspect "$TARGET_IMAGE" >"$OUTPUT_DIR/image-inspect.json" - - name: Login through a real browser and inspect the console + - name: Login through Chromium and require a rendered console shell: bash run: | set -euo pipefail @@ -117,121 +113,82 @@ jobs: import { chromium } from 'playwright' const baseURL = process.env.BASE_URL - const email = process.env.ADMIN_EMAIL - const password = process.env.ADMIN_PASSWORD const outputDir = process.env.OUTPUT_DIR - const events = [] const pageErrors = [] const failedRequests = [] const errorResponses = [] - const browser = await chromium.launch({ headless: true }) - const context = await browser.newContext({ viewport: { width: 1440, height: 1000 } }) - const page = await context.newPage() - - page.on('console', (message) => { - events.push({ type: `console:${message.type()}`, text: message.text() }) - }) - page.on('pageerror', (error) => { - pageErrors.push({ message: error.message, stack: error.stack || '' }) - }) - page.on('requestfailed', (request) => { - failedRequests.push({ - url: request.url(), - method: request.method(), - resourceType: request.resourceType(), - failure: request.failure()?.errorText || 'unknown', + const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } }) + + page.on('pageerror', error => pageErrors.push({ message: error.message, stack: error.stack || '' })) + page.on('requestfailed', request => failedRequests.push({ + url: request.url(), + method: request.method(), + resourceType: request.resourceType(), + failure: request.failure()?.errorText || 'unknown', + })) + page.on('response', response => { + if (response.status() >= 400) errorResponses.push({ + url: response.url(), + status: response.status(), + resourceType: response.request().resourceType(), }) }) - page.on('response', (response) => { - if (response.status() >= 400) { - errorResponses.push({ - url: response.url(), - status: response.status(), - resourceType: response.request().resourceType(), - }) - } - }) - let fatal = '' - try { - await page.goto(`${baseURL}/login`, { waitUntil: 'domcontentloaded', timeout: 60000 }) - await page.locator('#email').fill(email) - await page.locator('#password').fill(password) - const submit = page.locator('button[type="submit"]') - await submit.waitFor({ state: 'visible', timeout: 30000 }) - await submit.click() - await page.waitForURL(/\/admin\/dashboard(?:[/?#]|$)/, { timeout: 60000 }) - await page.waitForTimeout(10000) - } catch (error) { - fatal = error instanceof Error ? `${error.message}\n${error.stack || ''}` : String(error) - } + await page.goto(`${baseURL}/login`, { waitUntil: 'domcontentloaded', timeout: 60000 }) + await page.locator('#email').fill(process.env.ADMIN_EMAIL) + await page.locator('#password').fill(process.env.ADMIN_PASSWORD) + await page.locator('button[type="submit"]').click() + await page.waitForURL(/\/admin\/dashboard(?:[/?#]|$)/, { timeout: 60000 }) + await page.waitForTimeout(10000) const structure = await page.evaluate(() => { const root = document.querySelector('#app') - const visibleText = document.body.innerText.trim() return { url: location.href, - title: document.title, rootChildren: root?.children.length ?? 0, - rootHTMLLength: root?.innerHTML.length ?? 0, - bodyTextLength: visibleText.length, - bodyTextSample: visibleText.slice(0, 1000), + bodyTextLength: document.body.innerText.trim().length, sidebarCount: document.querySelectorAll('#lightbridge-sidebar, .sidebar').length, headerCount: document.querySelectorAll('.app-topbar').length, mainCount: document.querySelectorAll('main.app-main').length, - startupError: document.body.innerText.includes('LightBridge could not start'), } }) await page.screenshot({ path: `${outputDir}/console.png`, fullPage: true }) - await fs.writeFile(`${outputDir}/page.html`, await page.content()) - const result = { - image: process.env.TARGET_IMAGE, - fatal, - structure, - pageErrors, - failedRequests, - errorResponses, - events, - } + const result = { structure, pageErrors, failedRequests, errorResponses } await fs.writeFile(`${outputDir}/result.json`, JSON.stringify(result, null, 2)) console.log(JSON.stringify(result, null, 2)) await browser.close() - const rendered = - !fatal && - structure.url.includes('/admin/dashboard') && - structure.rootChildren > 0 && - structure.sidebarCount > 0 && - structure.headerCount > 0 && - structure.mainCount > 0 && - structure.bodyTextLength > 50 && - !structure.startupError - - if (!rendered) process.exit(1) + if ( + pageErrors.length || + !structure.url.includes('/admin/dashboard') || + structure.rootChildren === 0 || + structure.sidebarCount === 0 || + structure.headerCount === 0 || + structure.mainCount === 0 || + structure.bodyTextLength < 50 + ) process.exit(1) JS docker run --rm --network host \ - -e BASE_URL -e ADMIN_EMAIL -e ADMIN_PASSWORD -e OUTPUT_DIR -e TARGET_IMAGE \ - -e PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 \ + -e BASE_URL -e ADMIN_EMAIL -e ADMIN_PASSWORD -e OUTPUT_DIR \ -v "$OUTPUT_DIR:/work" \ mcr.microsoft.com/playwright:v1.55.0-noble \ bash -lc 'set -euo pipefail; mkdir -p /tmp/diag; cp /work/diagnose.mjs /tmp/diag/; cd /tmp/diag; npm init -y >/dev/null; npm install --no-audit --no-fund playwright@1.55.0 >/dev/null; node diagnose.mjs' - - name: Collect service diagnostics + - name: Collect diagnostics if: always() shell: bash run: | set +e - docker compose -f /tmp/console-diagnostic-compose.yml ps -a >"$OUTPUT_DIR/compose-ps.txt" 2>&1 docker compose -f /tmp/console-diagnostic-compose.yml logs --no-color >"$OUTPUT_DIR/compose.log" 2>&1 - name: Upload diagnostics if: always() uses: actions/upload-artifact@v4 with: - name: console-${{ matrix.name }} + name: console-rendering-fix path: ${{ env.OUTPUT_DIR }} if-no-files-found: error retention-days: 2 From 4d803a7f0b99bf19ca0973aca1850102af66c671 Mon Sep 17 00:00:00 2001 From: GeekMr <140129782+WilliamWang1721@users.noreply.github.com> Date: Thu, 6 Aug 2026 12:53:12 +0800 Subject: [PATCH 4/7] ci: verify user console route --- .github/workflows/diagnose-console-blank.yml | 237 +++++++------------ 1 file changed, 83 insertions(+), 154 deletions(-) diff --git a/.github/workflows/diagnose-console-blank.yml b/.github/workflows/diagnose-console-blank.yml index 2d6d3bdd..3b111348 100644 --- a/.github/workflows/diagnose-console-blank.yml +++ b/.github/workflows/diagnose-console-blank.yml @@ -2,8 +2,7 @@ name: Verify console rendering fix on: push: - branches: - - agent/diagnose-console-blank + branches: [agent/diagnose-console-blank] permissions: contents: read @@ -16,184 +15,114 @@ jobs: browser-login: runs-on: ubuntu-latest timeout-minutes: 35 - env: - TARGET_IMAGE: lightbridge:console-fix - BASE_URL: http://127.0.0.1:18080 - ADMIN_EMAIL: console-diagnostic@lightbridge.local - ADMIN_PASSWORD: LightBridge-Console-Diagnostic-2026! - OUTPUT_DIR: /tmp/console-diagnostic - COMPOSE_PROJECT_NAME: lb-console-${{ github.run_id }} + services: + postgres: + image: postgres:17-alpine + env: + POSTGRES_USER: LightBridge + POSTGRES_PASSWORD: test-password + POSTGRES_DB: LightBridge + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U LightBridge -d LightBridge" + --health-interval 2s + --health-timeout 5s + --health-retries 60 + redis: + image: redis:7-alpine + ports: + - 6379:6379 + options: >- + --health-cmd "redis-cli ping" + --health-interval 2s + --health-timeout 5s + --health-retries 60 steps: - uses: actions/checkout@v6 - - name: Build the fixed production image - shell: bash + - name: Build and start fixed production image run: | set -euo pipefail - docker build \ - --tag "$TARGET_IMAGE" \ + docker build -t lightbridge:console-fix \ --build-arg VERSION=console-fix \ - --build-arg COMMIT="$GITHUB_SHA" \ - . - - - name: Start the fixed image - shell: bash + --build-arg COMMIT="$GITHUB_SHA" . + docker run -d --name lightbridge --network host \ + -e AUTO_SETUP=true \ + -e LIGHTBRIDGE_DEPLOYMENT_TYPE=container \ + -e SERVER_HOST=0.0.0.0 \ + -e SERVER_PORT=18080 \ + -e SERVER_MODE=release \ + -e RUN_MODE=standard \ + -e DATABASE_HOST=127.0.0.1 \ + -e DATABASE_PORT=5432 \ + -e DATABASE_USER=LightBridge \ + -e DATABASE_PASSWORD=test-password \ + -e DATABASE_DBNAME=LightBridge \ + -e DATABASE_SSLMODE=disable \ + -e REDIS_HOST=127.0.0.1 \ + -e REDIS_PORT=6379 \ + -e ADMIN_EMAIL=console-test@example.invalid \ + -e ADMIN_PASSWORD=ConsoleTest123! \ + -e JWT_SECRET=test-jwt-secret-test-jwt-secret-test-jwt-secret-test-jwt-secret \ + -e TOTP_ENCRYPTION_KEY=test-totp-key-test-totp-key-test-totp-key-test-totp-key \ + lightbridge:console-fix + for attempt in $(seq 1 80); do + if curl -fsS http://127.0.0.1:18080/health; then exit 0; fi + sleep 3 + done + docker logs lightbridge + exit 1 + + - name: Login and require rendered console run: | set -euo pipefail - mkdir -p "$OUTPUT_DIR" - cat >/tmp/console-diagnostic-compose.yml <<'YAML' - services: - postgres: - image: postgres:17-alpine - environment: - POSTGRES_USER: LightBridge - POSTGRES_PASSWORD: LightBridge-console-postgres-2026 - POSTGRES_DB: LightBridge - healthcheck: - test: ["CMD-SHELL", "pg_isready -U LightBridge -d LightBridge"] - interval: 2s - timeout: 5s - retries: 60 - redis: - image: redis:7-alpine - healthcheck: - test: ["CMD", "redis-cli", "ping"] - interval: 2s - timeout: 5s - retries: 60 - app: - image: ${TARGET_IMAGE:?TARGET_IMAGE is required} - pull_policy: never - ports: - - "127.0.0.1:18080:8080" - environment: - AUTO_SETUP: "true" - LIGHTBRIDGE_DEPLOYMENT_TYPE: container - SERVER_HOST: 0.0.0.0 - SERVER_PORT: "8080" - SERVER_MODE: release - RUN_MODE: standard - DATABASE_HOST: postgres - DATABASE_PORT: "5432" - DATABASE_USER: LightBridge - DATABASE_PASSWORD: LightBridge-console-postgres-2026 - DATABASE_DBNAME: LightBridge - DATABASE_SSLMODE: disable - REDIS_HOST: redis - REDIS_PORT: "6379" - REDIS_PASSWORD: "" - REDIS_DB: "0" - ADMIN_EMAIL: ${ADMIN_EMAIL:?ADMIN_EMAIL is required} - ADMIN_PASSWORD: ${ADMIN_PASSWORD:?ADMIN_PASSWORD is required} - JWT_SECRET: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef - TOTP_ENCRYPTION_KEY: abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789 - TZ: UTC - depends_on: - postgres: - condition: service_healthy - redis: - condition: service_healthy - healthcheck: - test: ["CMD", "wget", "-q", "-T", "5", "-O", "-", "http://127.0.0.1:8080/health"] - interval: 3s - timeout: 6s - retries: 80 - YAML - - docker compose -f /tmp/console-diagnostic-compose.yml up -d --wait --wait-timeout 240 - curl --fail --silent --show-error "$BASE_URL/health" | tee "$OUTPUT_DIR/health.json" - - - name: Login through Chromium and require a rendered console - shell: bash - run: | - set -euo pipefail - cat >"$OUTPUT_DIR/diagnose.mjs" <<'JS' + mkdir -p /tmp/console-result + cat >/tmp/console-result/check.mjs <<'JS' import fs from 'node:fs/promises' import { chromium } from 'playwright' - const baseURL = process.env.BASE_URL - const outputDir = process.env.OUTPUT_DIR - const pageErrors = [] - const failedRequests = [] - const errorResponses = [] + const errors = [] const browser = await chromium.launch({ headless: true }) const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } }) + page.on('pageerror', error => errors.push({ message: error.message, stack: error.stack || '' })) - page.on('pageerror', error => pageErrors.push({ message: error.message, stack: error.stack || '' })) - page.on('requestfailed', request => failedRequests.push({ - url: request.url(), - method: request.method(), - resourceType: request.resourceType(), - failure: request.failure()?.errorText || 'unknown', - })) - page.on('response', response => { - if (response.status() >= 400) errorResponses.push({ - url: response.url(), - status: response.status(), - resourceType: response.request().resourceType(), - }) - }) - - await page.goto(`${baseURL}/login`, { waitUntil: 'domcontentloaded', timeout: 60000 }) - await page.locator('#email').fill(process.env.ADMIN_EMAIL) - await page.locator('#password').fill(process.env.ADMIN_PASSWORD) + await page.goto('http://127.0.0.1:18080/login', { waitUntil: 'domcontentloaded' }) + await page.locator('#email').fill('console-test@example.invalid') + await page.locator('#password').fill('ConsoleTest123!') await page.locator('button[type="submit"]').click() - await page.waitForURL(/\/admin\/dashboard(?:[/?#]|$)/, { timeout: 60000 }) + await page.waitForURL(/\/(?:admin\/)?dashboard(?:[/?#]|$)/, { timeout: 60000 }) await page.waitForTimeout(10000) - const structure = await page.evaluate(() => { - const root = document.querySelector('#app') - return { - url: location.href, - rootChildren: root?.children.length ?? 0, - bodyTextLength: document.body.innerText.trim().length, - sidebarCount: document.querySelectorAll('#lightbridge-sidebar, .sidebar').length, - headerCount: document.querySelectorAll('.app-topbar').length, - mainCount: document.querySelectorAll('main.app-main').length, - } - }) - - await page.screenshot({ path: `${outputDir}/console.png`, fullPage: true }) - const result = { structure, pageErrors, failedRequests, errorResponses } - await fs.writeFile(`${outputDir}/result.json`, JSON.stringify(result, null, 2)) - console.log(JSON.stringify(result, null, 2)) + const result = await page.evaluate(() => ({ + url: location.href, + rootChildren: document.querySelector('#app')?.children.length ?? 0, + bodyTextLength: document.body.innerText.trim().length, + sidebarCount: document.querySelectorAll('#lightbridge-sidebar, .sidebar').length, + headerCount: document.querySelectorAll('.app-topbar').length, + mainCount: document.querySelectorAll('main.app-main').length, + })) + await page.screenshot({ path: '/work/console.png', fullPage: true }) + await fs.writeFile('/work/result.json', JSON.stringify({ result, errors }, null, 2)) + console.log(JSON.stringify({ result, errors }, null, 2)) await browser.close() - if ( - pageErrors.length || - !structure.url.includes('/admin/dashboard') || - structure.rootChildren === 0 || - structure.sidebarCount === 0 || - structure.headerCount === 0 || - structure.mainCount === 0 || - structure.bodyTextLength < 50 - ) process.exit(1) + if (errors.length || result.rootChildren === 0 || result.bodyTextLength < 50 || result.sidebarCount === 0 || result.headerCount === 0 || result.mainCount === 0) process.exit(1) JS - - docker run --rm --network host \ - -e BASE_URL -e ADMIN_EMAIL -e ADMIN_PASSWORD -e OUTPUT_DIR \ - -v "$OUTPUT_DIR:/work" \ + docker run --rm --network host -v /tmp/console-result:/work \ mcr.microsoft.com/playwright:v1.55.0-noble \ - bash -lc 'set -euo pipefail; mkdir -p /tmp/diag; cp /work/diagnose.mjs /tmp/diag/; cd /tmp/diag; npm init -y >/dev/null; npm install --no-audit --no-fund playwright@1.55.0 >/dev/null; node diagnose.mjs' + bash -lc 'cd /tmp && npm init -y >/dev/null && npm install --no-audit --no-fund playwright@1.55.0 >/dev/null && node /work/check.mjs' - - name: Collect diagnostics - if: always() - shell: bash - run: | - set +e - docker compose -f /tmp/console-diagnostic-compose.yml logs --no-color >"$OUTPUT_DIR/compose.log" 2>&1 - - - name: Upload diagnostics + - name: Upload browser evidence if: always() uses: actions/upload-artifact@v4 with: name: console-rendering-fix - path: ${{ env.OUTPUT_DIR }} - if-no-files-found: error + path: /tmp/console-result + if-no-files-found: warn retention-days: 2 - - name: Cleanup - if: always() - shell: bash - run: docker compose -f /tmp/console-diagnostic-compose.yml down --volumes --remove-orphans || true + - name: Show app logs + if: failure() + run: docker logs lightbridge || true From 62f7b02282f2d5478d28cc94fd82557a18805db6 Mon Sep 17 00:00:00 2001 From: GeekMr <140129782+WilliamWang1721@users.noreply.github.com> Date: Thu, 6 Aug 2026 13:01:47 +0800 Subject: [PATCH 5/7] ci: use valid diagnostic encryption key --- .github/workflows/diagnose-console-blank.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/diagnose-console-blank.yml b/.github/workflows/diagnose-console-blank.yml index 3b111348..a6d38daa 100644 --- a/.github/workflows/diagnose-console-blank.yml +++ b/.github/workflows/diagnose-console-blank.yml @@ -66,7 +66,7 @@ jobs: -e ADMIN_EMAIL=console-test@example.invalid \ -e ADMIN_PASSWORD=ConsoleTest123! \ -e JWT_SECRET=test-jwt-secret-test-jwt-secret-test-jwt-secret-test-jwt-secret \ - -e TOTP_ENCRYPTION_KEY=test-totp-key-test-totp-key-test-totp-key-test-totp-key \ + -e TOTP_ENCRYPTION_KEY=abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789 \ lightbridge:console-fix for attempt in $(seq 1 80); do if curl -fsS http://127.0.0.1:18080/health; then exit 0; fi From 724722ccc320eedbfded56b139e1057c35799d27 Mon Sep 17 00:00:00 2001 From: GeekMr <140129782+WilliamWang1721@users.noreply.github.com> Date: Thu, 6 Aug 2026 13:07:29 +0800 Subject: [PATCH 6/7] ci: run browser check beside installed module --- .github/workflows/diagnose-console-blank.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/diagnose-console-blank.yml b/.github/workflows/diagnose-console-blank.yml index a6d38daa..a0617945 100644 --- a/.github/workflows/diagnose-console-blank.yml +++ b/.github/workflows/diagnose-console-blank.yml @@ -112,7 +112,7 @@ jobs: JS docker run --rm --network host -v /tmp/console-result:/work \ mcr.microsoft.com/playwright:v1.55.0-noble \ - bash -lc 'cd /tmp && npm init -y >/dev/null && npm install --no-audit --no-fund playwright@1.55.0 >/dev/null && node /work/check.mjs' + bash -lc 'set -euo pipefail; mkdir -p /tmp/diag; cp /work/check.mjs /tmp/diag/check.mjs; cd /tmp/diag; npm init -y >/dev/null; npm install --no-audit --no-fund playwright@1.55.0 >/dev/null; node check.mjs' - name: Upload browser evidence if: always() From 897bcab28246431109eb38bb1bf06ece0996023c Mon Sep 17 00:00:00 2001 From: GeekMr <140129782+WilliamWang1721@users.noreply.github.com> Date: Thu, 6 Aug 2026 13:18:23 +0800 Subject: [PATCH 7/7] ci: remove temporary console diagnosis workflow --- .github/workflows/diagnose-console-blank.yml | 128 ------------------- 1 file changed, 128 deletions(-) delete mode 100644 .github/workflows/diagnose-console-blank.yml diff --git a/.github/workflows/diagnose-console-blank.yml b/.github/workflows/diagnose-console-blank.yml deleted file mode 100644 index a0617945..00000000 --- a/.github/workflows/diagnose-console-blank.yml +++ /dev/null @@ -1,128 +0,0 @@ -name: Verify console rendering fix - -on: - push: - branches: [agent/diagnose-console-blank] - -permissions: - contents: read - -concurrency: - group: diagnose-console-blank - cancel-in-progress: true - -jobs: - browser-login: - runs-on: ubuntu-latest - timeout-minutes: 35 - services: - postgres: - image: postgres:17-alpine - env: - POSTGRES_USER: LightBridge - POSTGRES_PASSWORD: test-password - POSTGRES_DB: LightBridge - ports: - - 5432:5432 - options: >- - --health-cmd "pg_isready -U LightBridge -d LightBridge" - --health-interval 2s - --health-timeout 5s - --health-retries 60 - redis: - image: redis:7-alpine - ports: - - 6379:6379 - options: >- - --health-cmd "redis-cli ping" - --health-interval 2s - --health-timeout 5s - --health-retries 60 - - steps: - - uses: actions/checkout@v6 - - - name: Build and start fixed production image - run: | - set -euo pipefail - docker build -t lightbridge:console-fix \ - --build-arg VERSION=console-fix \ - --build-arg COMMIT="$GITHUB_SHA" . - docker run -d --name lightbridge --network host \ - -e AUTO_SETUP=true \ - -e LIGHTBRIDGE_DEPLOYMENT_TYPE=container \ - -e SERVER_HOST=0.0.0.0 \ - -e SERVER_PORT=18080 \ - -e SERVER_MODE=release \ - -e RUN_MODE=standard \ - -e DATABASE_HOST=127.0.0.1 \ - -e DATABASE_PORT=5432 \ - -e DATABASE_USER=LightBridge \ - -e DATABASE_PASSWORD=test-password \ - -e DATABASE_DBNAME=LightBridge \ - -e DATABASE_SSLMODE=disable \ - -e REDIS_HOST=127.0.0.1 \ - -e REDIS_PORT=6379 \ - -e ADMIN_EMAIL=console-test@example.invalid \ - -e ADMIN_PASSWORD=ConsoleTest123! \ - -e JWT_SECRET=test-jwt-secret-test-jwt-secret-test-jwt-secret-test-jwt-secret \ - -e TOTP_ENCRYPTION_KEY=abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789 \ - lightbridge:console-fix - for attempt in $(seq 1 80); do - if curl -fsS http://127.0.0.1:18080/health; then exit 0; fi - sleep 3 - done - docker logs lightbridge - exit 1 - - - name: Login and require rendered console - run: | - set -euo pipefail - mkdir -p /tmp/console-result - cat >/tmp/console-result/check.mjs <<'JS' - import fs from 'node:fs/promises' - import { chromium } from 'playwright' - - const errors = [] - const browser = await chromium.launch({ headless: true }) - const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } }) - page.on('pageerror', error => errors.push({ message: error.message, stack: error.stack || '' })) - - await page.goto('http://127.0.0.1:18080/login', { waitUntil: 'domcontentloaded' }) - await page.locator('#email').fill('console-test@example.invalid') - await page.locator('#password').fill('ConsoleTest123!') - await page.locator('button[type="submit"]').click() - await page.waitForURL(/\/(?:admin\/)?dashboard(?:[/?#]|$)/, { timeout: 60000 }) - await page.waitForTimeout(10000) - - const result = await page.evaluate(() => ({ - url: location.href, - rootChildren: document.querySelector('#app')?.children.length ?? 0, - bodyTextLength: document.body.innerText.trim().length, - sidebarCount: document.querySelectorAll('#lightbridge-sidebar, .sidebar').length, - headerCount: document.querySelectorAll('.app-topbar').length, - mainCount: document.querySelectorAll('main.app-main').length, - })) - await page.screenshot({ path: '/work/console.png', fullPage: true }) - await fs.writeFile('/work/result.json', JSON.stringify({ result, errors }, null, 2)) - console.log(JSON.stringify({ result, errors }, null, 2)) - await browser.close() - - if (errors.length || result.rootChildren === 0 || result.bodyTextLength < 50 || result.sidebarCount === 0 || result.headerCount === 0 || result.mainCount === 0) process.exit(1) - JS - docker run --rm --network host -v /tmp/console-result:/work \ - mcr.microsoft.com/playwright:v1.55.0-noble \ - bash -lc 'set -euo pipefail; mkdir -p /tmp/diag; cp /work/check.mjs /tmp/diag/check.mjs; cd /tmp/diag; npm init -y >/dev/null; npm install --no-audit --no-fund playwright@1.55.0 >/dev/null; node check.mjs' - - - name: Upload browser evidence - if: always() - uses: actions/upload-artifact@v4 - with: - name: console-rendering-fix - path: /tmp/console-result - if-no-files-found: warn - retention-days: 2 - - - name: Show app logs - if: failure() - run: docker logs lightbridge || true