feat(task4): refactor miniapp static serving and frontend bundle/tests

This commit is contained in:
dj-oyu 2026-03-05 01:31:41 +09:00
parent 47bdad65fa
commit 457a04e64f
20 changed files with 6448 additions and 2204 deletions

View file

@ -1,7 +1,7 @@
name: PR
on:
pull_request: { }
pull_request: {}
jobs:
lint:
@ -16,6 +16,9 @@ jobs:
with:
go-version-file: go.mod
- name: Setup Bun
uses: oven-sh/setup-bun@v2
- name: Run go generate
run: go generate ./...
@ -36,8 +39,26 @@ jobs:
with:
go-version-file: go.mod
- name: Setup Bun
uses: oven-sh/setup-bun@v2
- name: Setup Node
uses: actions/setup-node@v6
with:
node-version: '24'
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Run go generate
run: go generate ./...
- name: Run frontend tests
run: |
pnpm --dir pkg/miniapp/frontend install --frozen-lockfile
pnpm --dir pkg/miniapp/frontend test
- name: Run go test
run: go test ./...

8
.gitignore vendored
View file

@ -44,6 +44,14 @@ tasks/
# Added by goreleaser init:
dist/
!pkg/miniapp/static/dist/
!pkg/miniapp/static/dist/**
# Windows Application Icon/Resource
*.syso
# Frontend dependencies
node_modules/

View file

@ -63,8 +63,9 @@ Lint: `golangci-lint run`
| [`todo/TASKS-1.md`](todo/TASKS-1.md) | ~~**Memory & Performance Optimization**~~ ✅ 実装済みMemoryStore キャッシュ+パース済み state、FunctionCall.Arguments map統一、ToolDefinition.Parameters RawMessage化、検索結果フォーマット共通化、stats 定期フラッシュ) |
| [`todo/TASKS-2.md`](todo/TASKS-2.md) | **Subagent Orchestration (Container Model)** — SubagentContainer、Orchestrator、Presets enforcement、Subagent Plan ModeTASKS-1 の型変更前提メモ追記済み) |
| [`todo/TASKS-3.md`](todo/TASKS-3.md) | ~~**Session DAG (SQLite Store)**~~ ✅ 実装済みPhase 03: SQLite SessionStore、LegacyAdapter、Fork/Report、CompactOldTurns、`/session` CLI コマンド、Mini App グラフ UI |
| [`todo/TASKS-4.md`](todo/TASKS-4.md) | **Mini App & Static Serving** — 静的配信の汎用化、バンドラ導入、フロントエンドテスト追加 |
| [`todo/TASKS-4.md`](todo/TASKS-4.md) | ~~**Mini App & Static Serving**~~ ✅ 実装済み(`http.FileServer` 統合、テンプレート注入、Bun ビルド導線、frontend unit test + CI `pnpm test` |
| [`todo/TASKS-5.md`](todo/TASKS-5.md) | ~~**Heartbeat Worktree Management**~~ ✅ 実装済み(`/plan worktrees``list/inspect/merge/dispose`、安全化した `PruneOrphaned`、Mini App `/miniapp/api/worktrees` + Git タブ UI |
| [`todo/TASKS-6.md`](todo/TASKS-6.md) | **SOUL.md — AI Persona Evolution** — 睡眠フェーズで体験を統合・忘却し人格を再構成。TASKS-2 完了後に着手 |
| [`todo/TASKS-7.md`](todo/TASKS-7.md) | **Provider Wire Compatibility Hardening** — openai_compat の provider 別 wire 分岐OpenAI strict / Gemini、thought_signature round-trip 保全、互換テスト追加 |

View file

@ -0,0 +1,27 @@
import { copyFile, mkdir, rm } from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const rootDir = path.dirname(fileURLToPath(import.meta.url));
const srcDir = path.join(rootDir, 'src');
const outDir = path.join(rootDir, '..', 'static', 'dist');
await rm(outDir, { recursive: true, force: true });
await mkdir(outDir, { recursive: true });
const result = await Bun.build({
entrypoints: [path.join(srcDir, 'app.js')],
outdir: outDir,
target: 'browser',
format: 'iife',
sourcemap: 'none',
});
if (!result.success) {
for (const log of result.logs) {
console.error(log);
}
process.exit(1);
}
await copyFile(path.join(srcDir, 'map.js'), path.join(outDir, 'map.js'));

View file

@ -0,0 +1,14 @@
{
"name": "miniapp-frontend",
"private": true,
"type": "module",
"scripts": {
"build": "bun run ./build.mjs",
"test": "vitest run"
},
"devDependencies": {
"happy-dom": "^16.8.1",
"vitest": "^2.1.9"
}
}

900
pkg/miniapp/frontend/pnpm-lock.yaml generated Normal file
View file

@ -0,0 +1,900 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
devDependencies:
happy-dom:
specifier: ^16.8.1
version: 16.8.1
vitest:
specifier: ^2.1.9
version: 2.1.9(happy-dom@16.8.1)
packages:
'@esbuild/aix-ppc64@0.21.5':
resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==}
engines: {node: '>=12'}
cpu: [ppc64]
os: [aix]
'@esbuild/android-arm64@0.21.5':
resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==}
engines: {node: '>=12'}
cpu: [arm64]
os: [android]
'@esbuild/android-arm@0.21.5':
resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==}
engines: {node: '>=12'}
cpu: [arm]
os: [android]
'@esbuild/android-x64@0.21.5':
resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==}
engines: {node: '>=12'}
cpu: [x64]
os: [android]
'@esbuild/darwin-arm64@0.21.5':
resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==}
engines: {node: '>=12'}
cpu: [arm64]
os: [darwin]
'@esbuild/darwin-x64@0.21.5':
resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==}
engines: {node: '>=12'}
cpu: [x64]
os: [darwin]
'@esbuild/freebsd-arm64@0.21.5':
resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==}
engines: {node: '>=12'}
cpu: [arm64]
os: [freebsd]
'@esbuild/freebsd-x64@0.21.5':
resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==}
engines: {node: '>=12'}
cpu: [x64]
os: [freebsd]
'@esbuild/linux-arm64@0.21.5':
resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==}
engines: {node: '>=12'}
cpu: [arm64]
os: [linux]
'@esbuild/linux-arm@0.21.5':
resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==}
engines: {node: '>=12'}
cpu: [arm]
os: [linux]
'@esbuild/linux-ia32@0.21.5':
resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==}
engines: {node: '>=12'}
cpu: [ia32]
os: [linux]
'@esbuild/linux-loong64@0.21.5':
resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==}
engines: {node: '>=12'}
cpu: [loong64]
os: [linux]
'@esbuild/linux-mips64el@0.21.5':
resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==}
engines: {node: '>=12'}
cpu: [mips64el]
os: [linux]
'@esbuild/linux-ppc64@0.21.5':
resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==}
engines: {node: '>=12'}
cpu: [ppc64]
os: [linux]
'@esbuild/linux-riscv64@0.21.5':
resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==}
engines: {node: '>=12'}
cpu: [riscv64]
os: [linux]
'@esbuild/linux-s390x@0.21.5':
resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==}
engines: {node: '>=12'}
cpu: [s390x]
os: [linux]
'@esbuild/linux-x64@0.21.5':
resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==}
engines: {node: '>=12'}
cpu: [x64]
os: [linux]
'@esbuild/netbsd-x64@0.21.5':
resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==}
engines: {node: '>=12'}
cpu: [x64]
os: [netbsd]
'@esbuild/openbsd-x64@0.21.5':
resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==}
engines: {node: '>=12'}
cpu: [x64]
os: [openbsd]
'@esbuild/sunos-x64@0.21.5':
resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==}
engines: {node: '>=12'}
cpu: [x64]
os: [sunos]
'@esbuild/win32-arm64@0.21.5':
resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==}
engines: {node: '>=12'}
cpu: [arm64]
os: [win32]
'@esbuild/win32-ia32@0.21.5':
resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==}
engines: {node: '>=12'}
cpu: [ia32]
os: [win32]
'@esbuild/win32-x64@0.21.5':
resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==}
engines: {node: '>=12'}
cpu: [x64]
os: [win32]
'@jridgewell/sourcemap-codec@1.5.5':
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
'@rollup/rollup-android-arm-eabi@4.59.0':
resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==}
cpu: [arm]
os: [android]
'@rollup/rollup-android-arm64@4.59.0':
resolution: {integrity: sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==}
cpu: [arm64]
os: [android]
'@rollup/rollup-darwin-arm64@4.59.0':
resolution: {integrity: sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==}
cpu: [arm64]
os: [darwin]
'@rollup/rollup-darwin-x64@4.59.0':
resolution: {integrity: sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==}
cpu: [x64]
os: [darwin]
'@rollup/rollup-freebsd-arm64@4.59.0':
resolution: {integrity: sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==}
cpu: [arm64]
os: [freebsd]
'@rollup/rollup-freebsd-x64@4.59.0':
resolution: {integrity: sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==}
cpu: [x64]
os: [freebsd]
'@rollup/rollup-linux-arm-gnueabihf@4.59.0':
resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==}
cpu: [arm]
os: [linux]
'@rollup/rollup-linux-arm-musleabihf@4.59.0':
resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==}
cpu: [arm]
os: [linux]
'@rollup/rollup-linux-arm64-gnu@4.59.0':
resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==}
cpu: [arm64]
os: [linux]
'@rollup/rollup-linux-arm64-musl@4.59.0':
resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==}
cpu: [arm64]
os: [linux]
'@rollup/rollup-linux-loong64-gnu@4.59.0':
resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==}
cpu: [loong64]
os: [linux]
'@rollup/rollup-linux-loong64-musl@4.59.0':
resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==}
cpu: [loong64]
os: [linux]
'@rollup/rollup-linux-ppc64-gnu@4.59.0':
resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==}
cpu: [ppc64]
os: [linux]
'@rollup/rollup-linux-ppc64-musl@4.59.0':
resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==}
cpu: [ppc64]
os: [linux]
'@rollup/rollup-linux-riscv64-gnu@4.59.0':
resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==}
cpu: [riscv64]
os: [linux]
'@rollup/rollup-linux-riscv64-musl@4.59.0':
resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==}
cpu: [riscv64]
os: [linux]
'@rollup/rollup-linux-s390x-gnu@4.59.0':
resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==}
cpu: [s390x]
os: [linux]
'@rollup/rollup-linux-x64-gnu@4.59.0':
resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==}
cpu: [x64]
os: [linux]
'@rollup/rollup-linux-x64-musl@4.59.0':
resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==}
cpu: [x64]
os: [linux]
'@rollup/rollup-openbsd-x64@4.59.0':
resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==}
cpu: [x64]
os: [openbsd]
'@rollup/rollup-openharmony-arm64@4.59.0':
resolution: {integrity: sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==}
cpu: [arm64]
os: [openharmony]
'@rollup/rollup-win32-arm64-msvc@4.59.0':
resolution: {integrity: sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==}
cpu: [arm64]
os: [win32]
'@rollup/rollup-win32-ia32-msvc@4.59.0':
resolution: {integrity: sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==}
cpu: [ia32]
os: [win32]
'@rollup/rollup-win32-x64-gnu@4.59.0':
resolution: {integrity: sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==}
cpu: [x64]
os: [win32]
'@rollup/rollup-win32-x64-msvc@4.59.0':
resolution: {integrity: sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==}
cpu: [x64]
os: [win32]
'@types/estree@1.0.8':
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
'@vitest/expect@2.1.9':
resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==}
'@vitest/mocker@2.1.9':
resolution: {integrity: sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==}
peerDependencies:
msw: ^2.4.9
vite: ^5.0.0
peerDependenciesMeta:
msw:
optional: true
vite:
optional: true
'@vitest/pretty-format@2.1.9':
resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==}
'@vitest/runner@2.1.9':
resolution: {integrity: sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==}
'@vitest/snapshot@2.1.9':
resolution: {integrity: sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==}
'@vitest/spy@2.1.9':
resolution: {integrity: sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==}
'@vitest/utils@2.1.9':
resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==}
assertion-error@2.0.1:
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
engines: {node: '>=12'}
cac@6.7.14:
resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
engines: {node: '>=8'}
chai@5.3.3:
resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==}
engines: {node: '>=18'}
check-error@2.1.3:
resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==}
engines: {node: '>= 16'}
debug@4.4.3:
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
engines: {node: '>=6.0'}
peerDependencies:
supports-color: '*'
peerDependenciesMeta:
supports-color:
optional: true
deep-eql@5.0.2:
resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==}
engines: {node: '>=6'}
es-module-lexer@1.7.0:
resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==}
esbuild@0.21.5:
resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==}
engines: {node: '>=12'}
hasBin: true
estree-walker@3.0.3:
resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
expect-type@1.3.0:
resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==}
engines: {node: '>=12.0.0'}
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
happy-dom@16.8.1:
resolution: {integrity: sha512-n0QrmT9lD81rbpKsyhnlz3DgnMZlaOkJPpgi746doA+HvaMC79bdWkwjrNnGJRvDrWTI8iOcJiVTJ5CdT/AZRw==}
engines: {node: '>=18.0.0'}
loupe@3.2.1:
resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==}
magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
nanoid@3.3.11:
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
pathe@1.1.2:
resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==}
pathval@2.0.1:
resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==}
engines: {node: '>= 14.16'}
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
postcss@8.5.8:
resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==}
engines: {node: ^10 || ^12 || >=14}
rollup@4.59.0:
resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==}
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true
siginfo@2.0.0:
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
stackback@0.0.2:
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
std-env@3.10.0:
resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==}
tinybench@2.9.0:
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
tinyexec@0.3.2:
resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==}
tinypool@1.1.1:
resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==}
engines: {node: ^18.0.0 || >=20.0.0}
tinyrainbow@1.2.0:
resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==}
engines: {node: '>=14.0.0'}
tinyspy@3.0.2:
resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==}
engines: {node: '>=14.0.0'}
vite-node@2.1.9:
resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==}
engines: {node: ^18.0.0 || >=20.0.0}
hasBin: true
vite@5.4.21:
resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==}
engines: {node: ^18.0.0 || >=20.0.0}
hasBin: true
peerDependencies:
'@types/node': ^18.0.0 || >=20.0.0
less: '*'
lightningcss: ^1.21.0
sass: '*'
sass-embedded: '*'
stylus: '*'
sugarss: '*'
terser: ^5.4.0
peerDependenciesMeta:
'@types/node':
optional: true
less:
optional: true
lightningcss:
optional: true
sass:
optional: true
sass-embedded:
optional: true
stylus:
optional: true
sugarss:
optional: true
terser:
optional: true
vitest@2.1.9:
resolution: {integrity: sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==}
engines: {node: ^18.0.0 || >=20.0.0}
hasBin: true
peerDependencies:
'@edge-runtime/vm': '*'
'@types/node': ^18.0.0 || >=20.0.0
'@vitest/browser': 2.1.9
'@vitest/ui': 2.1.9
happy-dom: '*'
jsdom: '*'
peerDependenciesMeta:
'@edge-runtime/vm':
optional: true
'@types/node':
optional: true
'@vitest/browser':
optional: true
'@vitest/ui':
optional: true
happy-dom:
optional: true
jsdom:
optional: true
webidl-conversions@7.0.0:
resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==}
engines: {node: '>=12'}
whatwg-mimetype@3.0.0:
resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==}
engines: {node: '>=12'}
why-is-node-running@2.3.0:
resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==}
engines: {node: '>=8'}
hasBin: true
snapshots:
'@esbuild/aix-ppc64@0.21.5':
optional: true
'@esbuild/android-arm64@0.21.5':
optional: true
'@esbuild/android-arm@0.21.5':
optional: true
'@esbuild/android-x64@0.21.5':
optional: true
'@esbuild/darwin-arm64@0.21.5':
optional: true
'@esbuild/darwin-x64@0.21.5':
optional: true
'@esbuild/freebsd-arm64@0.21.5':
optional: true
'@esbuild/freebsd-x64@0.21.5':
optional: true
'@esbuild/linux-arm64@0.21.5':
optional: true
'@esbuild/linux-arm@0.21.5':
optional: true
'@esbuild/linux-ia32@0.21.5':
optional: true
'@esbuild/linux-loong64@0.21.5':
optional: true
'@esbuild/linux-mips64el@0.21.5':
optional: true
'@esbuild/linux-ppc64@0.21.5':
optional: true
'@esbuild/linux-riscv64@0.21.5':
optional: true
'@esbuild/linux-s390x@0.21.5':
optional: true
'@esbuild/linux-x64@0.21.5':
optional: true
'@esbuild/netbsd-x64@0.21.5':
optional: true
'@esbuild/openbsd-x64@0.21.5':
optional: true
'@esbuild/sunos-x64@0.21.5':
optional: true
'@esbuild/win32-arm64@0.21.5':
optional: true
'@esbuild/win32-ia32@0.21.5':
optional: true
'@esbuild/win32-x64@0.21.5':
optional: true
'@jridgewell/sourcemap-codec@1.5.5': {}
'@rollup/rollup-android-arm-eabi@4.59.0':
optional: true
'@rollup/rollup-android-arm64@4.59.0':
optional: true
'@rollup/rollup-darwin-arm64@4.59.0':
optional: true
'@rollup/rollup-darwin-x64@4.59.0':
optional: true
'@rollup/rollup-freebsd-arm64@4.59.0':
optional: true
'@rollup/rollup-freebsd-x64@4.59.0':
optional: true
'@rollup/rollup-linux-arm-gnueabihf@4.59.0':
optional: true
'@rollup/rollup-linux-arm-musleabihf@4.59.0':
optional: true
'@rollup/rollup-linux-arm64-gnu@4.59.0':
optional: true
'@rollup/rollup-linux-arm64-musl@4.59.0':
optional: true
'@rollup/rollup-linux-loong64-gnu@4.59.0':
optional: true
'@rollup/rollup-linux-loong64-musl@4.59.0':
optional: true
'@rollup/rollup-linux-ppc64-gnu@4.59.0':
optional: true
'@rollup/rollup-linux-ppc64-musl@4.59.0':
optional: true
'@rollup/rollup-linux-riscv64-gnu@4.59.0':
optional: true
'@rollup/rollup-linux-riscv64-musl@4.59.0':
optional: true
'@rollup/rollup-linux-s390x-gnu@4.59.0':
optional: true
'@rollup/rollup-linux-x64-gnu@4.59.0':
optional: true
'@rollup/rollup-linux-x64-musl@4.59.0':
optional: true
'@rollup/rollup-openbsd-x64@4.59.0':
optional: true
'@rollup/rollup-openharmony-arm64@4.59.0':
optional: true
'@rollup/rollup-win32-arm64-msvc@4.59.0':
optional: true
'@rollup/rollup-win32-ia32-msvc@4.59.0':
optional: true
'@rollup/rollup-win32-x64-gnu@4.59.0':
optional: true
'@rollup/rollup-win32-x64-msvc@4.59.0':
optional: true
'@types/estree@1.0.8': {}
'@vitest/expect@2.1.9':
dependencies:
'@vitest/spy': 2.1.9
'@vitest/utils': 2.1.9
chai: 5.3.3
tinyrainbow: 1.2.0
'@vitest/mocker@2.1.9(vite@5.4.21)':
dependencies:
'@vitest/spy': 2.1.9
estree-walker: 3.0.3
magic-string: 0.30.21
optionalDependencies:
vite: 5.4.21
'@vitest/pretty-format@2.1.9':
dependencies:
tinyrainbow: 1.2.0
'@vitest/runner@2.1.9':
dependencies:
'@vitest/utils': 2.1.9
pathe: 1.1.2
'@vitest/snapshot@2.1.9':
dependencies:
'@vitest/pretty-format': 2.1.9
magic-string: 0.30.21
pathe: 1.1.2
'@vitest/spy@2.1.9':
dependencies:
tinyspy: 3.0.2
'@vitest/utils@2.1.9':
dependencies:
'@vitest/pretty-format': 2.1.9
loupe: 3.2.1
tinyrainbow: 1.2.0
assertion-error@2.0.1: {}
cac@6.7.14: {}
chai@5.3.3:
dependencies:
assertion-error: 2.0.1
check-error: 2.1.3
deep-eql: 5.0.2
loupe: 3.2.1
pathval: 2.0.1
check-error@2.1.3: {}
debug@4.4.3:
dependencies:
ms: 2.1.3
deep-eql@5.0.2: {}
es-module-lexer@1.7.0: {}
esbuild@0.21.5:
optionalDependencies:
'@esbuild/aix-ppc64': 0.21.5
'@esbuild/android-arm': 0.21.5
'@esbuild/android-arm64': 0.21.5
'@esbuild/android-x64': 0.21.5
'@esbuild/darwin-arm64': 0.21.5
'@esbuild/darwin-x64': 0.21.5
'@esbuild/freebsd-arm64': 0.21.5
'@esbuild/freebsd-x64': 0.21.5
'@esbuild/linux-arm': 0.21.5
'@esbuild/linux-arm64': 0.21.5
'@esbuild/linux-ia32': 0.21.5
'@esbuild/linux-loong64': 0.21.5
'@esbuild/linux-mips64el': 0.21.5
'@esbuild/linux-ppc64': 0.21.5
'@esbuild/linux-riscv64': 0.21.5
'@esbuild/linux-s390x': 0.21.5
'@esbuild/linux-x64': 0.21.5
'@esbuild/netbsd-x64': 0.21.5
'@esbuild/openbsd-x64': 0.21.5
'@esbuild/sunos-x64': 0.21.5
'@esbuild/win32-arm64': 0.21.5
'@esbuild/win32-ia32': 0.21.5
'@esbuild/win32-x64': 0.21.5
estree-walker@3.0.3:
dependencies:
'@types/estree': 1.0.8
expect-type@1.3.0: {}
fsevents@2.3.3:
optional: true
happy-dom@16.8.1:
dependencies:
webidl-conversions: 7.0.0
whatwg-mimetype: 3.0.0
loupe@3.2.1: {}
magic-string@0.30.21:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
ms@2.1.3: {}
nanoid@3.3.11: {}
pathe@1.1.2: {}
pathval@2.0.1: {}
picocolors@1.1.1: {}
postcss@8.5.8:
dependencies:
nanoid: 3.3.11
picocolors: 1.1.1
source-map-js: 1.2.1
rollup@4.59.0:
dependencies:
'@types/estree': 1.0.8
optionalDependencies:
'@rollup/rollup-android-arm-eabi': 4.59.0
'@rollup/rollup-android-arm64': 4.59.0
'@rollup/rollup-darwin-arm64': 4.59.0
'@rollup/rollup-darwin-x64': 4.59.0
'@rollup/rollup-freebsd-arm64': 4.59.0
'@rollup/rollup-freebsd-x64': 4.59.0
'@rollup/rollup-linux-arm-gnueabihf': 4.59.0
'@rollup/rollup-linux-arm-musleabihf': 4.59.0
'@rollup/rollup-linux-arm64-gnu': 4.59.0
'@rollup/rollup-linux-arm64-musl': 4.59.0
'@rollup/rollup-linux-loong64-gnu': 4.59.0
'@rollup/rollup-linux-loong64-musl': 4.59.0
'@rollup/rollup-linux-ppc64-gnu': 4.59.0
'@rollup/rollup-linux-ppc64-musl': 4.59.0
'@rollup/rollup-linux-riscv64-gnu': 4.59.0
'@rollup/rollup-linux-riscv64-musl': 4.59.0
'@rollup/rollup-linux-s390x-gnu': 4.59.0
'@rollup/rollup-linux-x64-gnu': 4.59.0
'@rollup/rollup-linux-x64-musl': 4.59.0
'@rollup/rollup-openbsd-x64': 4.59.0
'@rollup/rollup-openharmony-arm64': 4.59.0
'@rollup/rollup-win32-arm64-msvc': 4.59.0
'@rollup/rollup-win32-ia32-msvc': 4.59.0
'@rollup/rollup-win32-x64-gnu': 4.59.0
'@rollup/rollup-win32-x64-msvc': 4.59.0
fsevents: 2.3.3
siginfo@2.0.0: {}
source-map-js@1.2.1: {}
stackback@0.0.2: {}
std-env@3.10.0: {}
tinybench@2.9.0: {}
tinyexec@0.3.2: {}
tinypool@1.1.1: {}
tinyrainbow@1.2.0: {}
tinyspy@3.0.2: {}
vite-node@2.1.9:
dependencies:
cac: 6.7.14
debug: 4.4.3
es-module-lexer: 1.7.0
pathe: 1.1.2
vite: 5.4.21
transitivePeerDependencies:
- '@types/node'
- less
- lightningcss
- sass
- sass-embedded
- stylus
- sugarss
- supports-color
- terser
vite@5.4.21:
dependencies:
esbuild: 0.21.5
postcss: 8.5.8
rollup: 4.59.0
optionalDependencies:
fsevents: 2.3.3
vitest@2.1.9(happy-dom@16.8.1):
dependencies:
'@vitest/expect': 2.1.9
'@vitest/mocker': 2.1.9(vite@5.4.21)
'@vitest/pretty-format': 2.1.9
'@vitest/runner': 2.1.9
'@vitest/snapshot': 2.1.9
'@vitest/spy': 2.1.9
'@vitest/utils': 2.1.9
chai: 5.3.3
debug: 4.4.3
expect-type: 1.3.0
magic-string: 0.30.21
pathe: 1.1.2
std-env: 3.10.0
tinybench: 2.9.0
tinyexec: 0.3.2
tinypool: 1.1.1
tinyrainbow: 1.2.0
vite: 5.4.21
vite-node: 2.1.9
why-is-node-running: 2.3.0
optionalDependencies:
happy-dom: 16.8.1
transitivePeerDependencies:
- less
- lightningcss
- msw
- sass
- sass-embedded
- stylus
- sugarss
- supports-color
- terser
webidl-conversions@7.0.0: {}
whatwg-mimetype@3.0.0: {}
why-is-node-running@2.3.0:
dependencies:
siginfo: 2.0.0
stackback: 0.0.2

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,106 @@
const SAFE_LEVELS = new Set(['debug', 'info', 'warn', 'error']);
function stringifyFieldValue(value) {
if (value === null || value === undefined) {
return '';
}
if (typeof value === 'string') {
return value;
}
if (typeof value === 'number' || typeof value === 'boolean') {
return String(value);
}
try {
return JSON.stringify(value);
} catch {
return String(value);
}
}
export function escapeHtml(value) {
return String(value == null ? '' : value)
.replaceAll('&', '&')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#39;');
}
export function renderFields(fields) {
if (!fields || typeof fields !== 'object' || Array.isArray(fields)) {
return '';
}
const keys = Object.keys(fields);
if (keys.length === 0) {
return '';
}
const parts = keys.map((key) => `${key}=${stringifyFieldValue(fields[key])}`);
return ` <span class="log-fields">{${escapeHtml(parts.join(', '))}}</span>`;
}
export function filterLogs(entries, component = '') {
if (!Array.isArray(entries) || entries.length === 0) {
return [];
}
if (!component) {
return entries.slice();
}
return entries.filter((entry) => (entry?.component || '') === component);
}
export function paginateLogs(entries, page = 1, pageSize = 100) {
const list = Array.isArray(entries) ? entries : [];
const size = Math.max(1, Number(pageSize) || 100);
const totalPages = Math.max(1, Math.ceil(list.length / size));
const currentPage = Math.min(Math.max(1, Number(page) || 1), totalPages);
// Page 1 is the newest page (tail of the list).
const end = list.length - (currentPage - 1) * size;
const start = Math.max(0, end - size);
return {
items: list.slice(start, Math.max(start, end)),
currentPage,
totalPages,
pageSize: size,
};
}
export function renderLogs(entries, options = {}) {
const component = options.component || '';
const filtered = filterLogs(entries, component);
const paged = paginateLogs(filtered, options.page, options.pageSize);
let html = '';
for (const entry of paged.items) {
const levelRaw = String(entry?.level || 'info').toLowerCase();
const level = SAFE_LEVELS.has(levelRaw) ? levelRaw : 'info';
const ts = entry?.timestamp ? String(entry.timestamp).substring(11, 19) : '';
const componentHTML = entry?.component
? `<span class="log-comp">${escapeHtml(entry.component)}</span>`
: '';
const fieldsHTML = renderFields(entry?.fields);
const message = escapeHtml(entry?.message || '');
html += '<div class="log-entry">' +
`<span class="log-ts">${ts}</span>` +
`<span class="log-badge ${level}">${level}</span>` +
componentHTML +
`<span class="log-msg">${message}${fieldsHTML}</span>` +
'</div>';
}
return {
html,
totalItems: filtered.length,
currentPage: paged.currentPage,
totalPages: paged.totalPages,
pageSize: paged.pageSize,
};
}
export function renderLogsInto(container, entries, options = {}) {
const view = renderLogs(entries, options);
container.innerHTML = view.html;
return view;
}

View file

@ -0,0 +1,70 @@
import { describe, expect, it } from 'vitest';
import { escapeHtml, filterLogs, paginateLogs, renderFields, renderLogs, renderLogsInto } from './logs_view.js';
describe('logs_view', () => {
it('renders a normal log message row', () => {
const view = renderLogs([
{
timestamp: '2026-03-05T12:34:56Z',
level: 'INFO',
component: 'telego',
message: 'connected',
},
]);
expect(view.totalItems).toBe(1);
expect(view.html).toContain('12:34:56');
expect(view.html).toContain('log-badge info');
expect(view.html).toContain('connected');
expect(view.html).toContain('telego');
});
it('renders fields for empty/single/multiple cases', () => {
expect(renderFields(null)).toBe('');
expect(renderFields({})).toBe('');
expect(renderFields({ req_id: 42 })).toContain('{req_id=42}');
const multi = renderFields({ a: 'x', b: 2 });
expect(multi).toContain('a=x');
expect(multi).toContain('b=2');
});
it('sanitizes potentially dangerous HTML', () => {
const xss = '<img src=x onerror=alert(1) />';
const escaped = escapeHtml(xss);
expect(escaped).not.toContain('<img');
expect(escaped).toContain('&lt;img');
const view = renderLogs([{ message: xss, level: 'warn' }]);
const container = document.createElement('div');
renderLogsInto(container, [{ message: xss, level: 'warn' }]);
expect(container.querySelector('img')).toBeNull();
expect(view.html).toContain('&lt;img');
});
it('supports filtering and pagination', () => {
const entries = [];
for (let i = 0; i < 25; i++) {
entries.push({
level: 'debug',
component: i % 2 === 0 ? 'telego' : 'dev-console',
message: `entry-${i}`,
});
}
const filtered = filterLogs(entries, 'telego');
expect(filtered.length).toBe(13);
const pageInfo = paginateLogs(filtered, 2, 5);
expect(pageInfo.totalPages).toBe(3);
expect(pageInfo.items.length).toBe(5);
const page1 = renderLogs(entries, { component: 'telego', page: 1, pageSize: 5 });
const page2 = renderLogs(entries, { component: 'telego', page: 2, pageSize: 5 });
expect(page1.totalPages).toBe(3);
expect(page1.html).not.toBe(page2.html);
expect(page1.html).toContain('entry-24');
expect(page2.html).toContain('entry-14');
});
});

View file

@ -203,3 +203,10 @@ function _drawMapFallback(ctx) {
_r(ctx, '#4a2408', 162, 284, 12, 16);
_r(ctx, _C.doorGold, 170, 291, 5, 5); // handle
}
// Expose map helpers for app.js runtime.
globalThis.MAP_POSITIONS = MAP_POSITIONS;
globalThis.loadMapAsset = loadMapAsset;
globalThis.drawMap = drawMap;

View file

@ -0,0 +1,912 @@
:root {
--bg: var(--tg-theme-bg-color, #ffffff);
--text: var(--tg-theme-text-color, #000000);
--hint: var(--tg-theme-hint-color, #999999);
--link: var(--tg-theme-link-color, #2481cc);
--btn: var(--tg-theme-button-color, #2481cc);
--btn-text: var(--tg-theme-button-text-color, #ffffff);
--secondary-bg: var(--tg-theme-secondary-bg-color, #f0f0f0);
--done: #34c759;
--current: var(--btn);
--pending-phase: var(--hint);
/* Liquid Glass */
--glass-bg: rgba(255, 255, 255, 0.55);
--glass-border: rgba(0, 0, 0, 0.08);
--glass-border-interactive: rgba(0, 0, 0, 0.15);
--glass-shadow: rgba(0, 0, 0, 0.06);
--glass-divider: rgba(0, 0, 0, 0.06);
--tab-bar-bg: rgba(255, 255, 255, 0.72);
--tab-track-bg: rgba(0, 0, 0, 0.06);
--tab-pill-bg: rgba(255, 255, 255, 0.9);
}
@media (prefers-color-scheme: dark) {
:root {
--bg: var(--tg-theme-bg-color, #1c1c1e);
--text: var(--tg-theme-text-color, #ffffff);
--hint: var(--tg-theme-hint-color, #8e8e93);
--link: var(--tg-theme-link-color, #5ac8fa);
--btn: var(--tg-theme-button-color, #5ac8fa);
--btn-text: var(--tg-theme-button-text-color, #ffffff);
--secondary-bg: var(--tg-theme-secondary-bg-color, #2c2c2e);
/* Liquid Glass — dark */
--glass-bg: rgba(255, 255, 255, 0.08);
--glass-border: rgba(255, 255, 255, 0.1);
--glass-border-interactive: rgba(255, 255, 255, 0.22);
--glass-shadow: rgba(0, 0, 0, 0.2);
--glass-divider: rgba(255, 255, 255, 0.08);
--tab-bar-bg: rgba(28, 28, 30, 0.72);
--tab-track-bg: rgba(255, 255, 255, 0.1);
--tab-pill-bg: rgba(255, 255, 255, 0.16);
}
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--bg);
color: var(--text);
font-size: 14px;
padding-bottom: 80px;
}
.tabs {
display: flex;
position: sticky;
top: 0;
z-index: 10;
padding: 8px 12px;
background: var(--tab-bar-bg);
-webkit-backdrop-filter: saturate(180%) blur(20px);
backdrop-filter: saturate(180%) blur(20px);
}
.tabs-inner {
display: flex;
position: relative;
width: 100%;
background: var(--tab-track-bg);
border-radius: 10px;
padding: 2px;
}
.tab-indicator {
position: absolute;
top: 2px;
bottom: 2px;
left: 2px;
width: calc(100% / var(--tab-count, 7) - 2px);
border-radius: 8px;
background: var(--tab-pill-bg);
box-shadow: 0 0.5px 2px rgba(0,0,0,0.12), 0 0.5px 1px rgba(0,0,0,0.08);
transition: transform 0.38s cubic-bezier(0.25, 1, 0.5, 1);
z-index: 0;
}
.tab {
flex: 1;
padding: 7px 4px;
text-align: center;
font-size: 13px;
font-weight: 500;
color: var(--hint);
border: none;
background: none;
cursor: pointer;
transition: color 0.25s ease;
position: relative;
z-index: 1;
-webkit-tap-highlight-color: transparent;
}
.tab.active {
color: var(--text);
font-weight: 600;
}
.hidden { display: none !important; }
.glass {
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: 16px;
box-shadow: 0 1px 3px var(--glass-shadow);
-webkit-backdrop-filter: blur(12px);
backdrop-filter: blur(12px);
}
.glass-interactive { border-color: var(--glass-border-interactive); }
.panel { display: none; padding: 16px; }
.panel.active { display: block; }
.card {
padding: 16px;
margin-bottom: 12px;
}
.card-title {
font-size: 12px;
color: var(--hint);
margin-bottom: 8px;
text-transform: uppercase;
letter-spacing: 0.6px;
font-weight: 600;
}
.card-value {
font-size: 22px;
font-weight: 700;
}
/* Plan - phase/step list */
.phase {
margin-bottom: 16px;
}
.phase-header {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 0 8px;
font-weight: 600;
font-size: 15px;
}
.phase-indicator {
width: 24px;
height: 24px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 11px;
font-weight: 700;
color: #fff;
flex-shrink: 0;
}
.phase-indicator.done { background: var(--done); }
.phase-indicator.current { background: var(--current); }
.phase-indicator.pending { background: var(--glass-divider); color: var(--hint); }
.phase-title { flex: 1; }
.phase-progress {
font-size: 12px;
color: var(--hint);
font-weight: 500;
}
.step {
display: flex;
align-items: flex-start;
gap: 10px;
padding: 12px 14px 12px 34px;
margin-bottom: 6px;
border-radius: 12px;
cursor: pointer;
transition: transform 0.2s cubic-bezier(0.25, 1, 0.5, 1), box-shadow 0.2s;
-webkit-tap-highlight-color: transparent;
}
.step:active { transform: scale(0.97); }
.step:not(.step-done) {
background: var(--glass-bg);
border: 1px solid var(--glass-border-interactive);
box-shadow: 0 0.5px 2px var(--glass-shadow);
}
.step-check {
width: 22px;
height: 22px;
border-radius: 50%;
border: 2px solid var(--hint);
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
margin-top: 0;
transition: all 0.2s;
}
.step-check.done {
background: var(--done);
border-color: var(--done);
}
.step-check.done::after {
content: '';
width: 6px;
height: 10px;
border: solid #fff;
border-width: 0 2px 2px 0;
transform: rotate(45deg);
margin-top: -2px;
}
.step-text {
flex: 1;
font-size: 14px;
line-height: 1.4;
}
.step-text.done {
color: var(--hint);
text-decoration: line-through;
}
/* Skills */
.skill-item {
padding: 14px 14px 14px 16px;
margin-bottom: 10px;
cursor: pointer;
transition: transform 0.2s cubic-bezier(0.25, 1, 0.5, 1), box-shadow 0.2s, border-color 0.2s;
display: flex;
align-items: center;
gap: 12px;
-webkit-tap-highlight-color: transparent;
}
.skill-item:active {
transform: scale(0.98);
}
.skill-item.selected {
border-color: var(--btn);
box-shadow: 0 2px 8px var(--glass-shadow);
}
.skill-body { flex: 1; min-width: 0; }
.skill-name {
font-weight: 600;
font-size: 15px;
margin-bottom: 4px;
}
.skill-desc {
font-size: 13px;
color: var(--hint);
line-height: 1.4;
}
.skill-source {
display: inline-block;
font-size: 11px;
padding: 3px 10px;
border-radius: 20px;
background: var(--tab-track-bg);
color: var(--hint);
margin-top: 6px;
font-weight: 500;
}
.skill-arrow {
color: var(--hint);
font-size: 22px;
flex-shrink: 0;
transition: color 0.2s;
}
.skill-item.selected .skill-arrow { color: var(--btn); }
/* Stats */
.stat-row {
display: flex;
justify-content: space-between;
padding: 11px 0;
border-bottom: 1px solid var(--glass-divider);
}
.stat-row:last-child { border-bottom: none; }
.stat-label { color: var(--hint); font-size: 14px; }
.stat-value { font-weight: 600; font-size: 14px; }
/* Send bar */
.send-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: var(--tab-bar-bg);
-webkit-backdrop-filter: saturate(180%) blur(20px);
backdrop-filter: saturate(180%) blur(20px);
padding: 12px 16px;
display: flex;
gap: 8px;
border-top: 1px solid var(--glass-divider);
}
.send-input {
flex: 1;
padding: 10px 16px;
border-radius: 20px;
color: var(--text);
font-size: 14px;
outline: none;
-webkit-backdrop-filter: blur(8px);
backdrop-filter: blur(8px);
transition: border-color 0.2s;
}
.send-input:focus { border-color: var(--btn); }
.send-btn {
padding: 10px 20px;
border-radius: 20px;
border: none;
background: var(--btn);
color: var(--btn-text);
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s cubic-bezier(0.25, 1, 0.5, 1), background 0.15s;
-webkit-tap-highlight-color: transparent;
}
.send-btn:active { transform: scale(0.95); }
.send-btn:disabled { opacity: 0.5; }
.send-btn.sent { background: var(--done); }
.empty-state {
text-align: center;
color: var(--hint);
padding: 48px 20px;
font-size: 15px;
}
.loading {
text-align: center;
color: var(--hint);
padding: 48px 20px;
font-size: 15px;
}
.cmd-tiles {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
.cmd-tile {
padding: 16px 14px;
border-radius: 14px;
color: var(--text);
font-size: 15px;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s cubic-bezier(0.25, 1, 0.5, 1), background 0.15s;
text-align: center;
-webkit-tap-highlight-color: transparent;
}
.cmd-tile:active {
transform: scale(0.96);
}
.cmd-tile.sent {
background: var(--btn);
color: var(--btn-text);
border-color: var(--btn);
}
/* MEMORY.md raw display */
.memory-view {
font-family: 'SF Mono', 'Menlo', 'Consolas', monospace;
font-size: 13px;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-word;
padding: 14px 16px;
}
.memory-view .md-h1 {
font-size: 18px;
font-weight: 700;
margin: 16px 0 8px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.memory-view .md-h1:first-child { margin-top: 0; }
.memory-view .md-h2 {
font-size: 15px;
font-weight: 700;
margin: 14px 0 6px;
color: var(--link);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.memory-view .md-h3 {
font-size: 14px;
font-weight: 600;
margin: 12px 0 4px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.memory-view .md-checkbox {
margin: 2px 0;
}
.memory-view .md-checkbox-icon {
display: inline-block;
width: 16px;
height: 16px;
border-radius: 4px;
border: 1.5px solid var(--hint);
vertical-align: middle;
margin-right: 6px;
position: relative;
top: -1px;
}
.memory-view .md-checkbox-icon.checked {
background: var(--done);
border-color: var(--done);
}
.memory-view .md-checkbox-icon.checked::after {
content: '';
position: absolute;
left: 4px;
top: 1px;
width: 4px;
height: 8px;
border: solid #fff;
border-width: 0 1.5px 1.5px 0;
transform: rotate(45deg);
}
.memory-view .md-quote {
border-left: 3px solid var(--hint);
padding-left: 10px;
color: var(--hint);
margin: 4px 0;
}
.memory-view .md-bullet {
margin: 2px 0;
padding-left: 12px;
text-indent: -12px;
}
.memory-view .md-bullet::before {
content: '\2022 ';
color: var(--hint);
}
/* Slide to Approve */
.slide-approve-wrap {
margin-top: 16px;
}
.slide-approve-track {
position: relative;
height: 56px;
border-radius: 28px;
overflow: hidden;
touch-action: none;
border: 1.5px solid;
border-image: linear-gradient(135deg, var(--btn), var(--glass-border)) 1;
border-image: none;
border-color: var(--btn);
box-shadow: 0 2px 8px rgba(0,0,0,0.18), inset 0 1px 0 rgba(255,255,255,0.08);
transition: background 0.3s, border-color 0.3s, box-shadow 0.3s;
}
.slide-approve-track.approved {
background: var(--done);
border-color: var(--done);
box-shadow: 0 0 16px rgba(76,175,80,0.4), 0 2px 8px rgba(0,0,0,0.18);
}
.slide-approve-thumb {
position: absolute;
top: 3px;
left: 3px;
width: 50px;
height: 50px;
border-radius: 50%;
background: var(--btn);
color: var(--btn-text);
display: flex;
align-items: center;
justify-content: center;
cursor: grab;
transition: left 0.3s cubic-bezier(0.25, 1, 0.5, 1);
z-index: 1;
user-select: none;
-webkit-user-select: none;
box-shadow: 0 2px 6px rgba(0,0,0,0.25);
}
.slide-approve-thumb svg {
width: 22px;
height: 22px;
}
.slide-approve-thumb.dragging {
transition: none;
cursor: grabbing;
}
@keyframes shimmer {
0%, 100% { opacity: 0.7; }
50% { opacity: 1; }
}
.slide-approve-label {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
color: var(--text);
font-size: 15px;
font-weight: 600;
pointer-events: none;
user-select: none;
-webkit-user-select: none;
transition: color 0.3s;
animation: shimmer 2.5s ease-in-out infinite;
}
.slide-approve-track.approved .slide-approve-label {
color: #fff;
animation: none;
}
/* Git log */
.git-commit {
display: flex;
gap: 8px;
padding: 10px 12px;
border-bottom: 1px solid var(--glass-border);
font-size: 13px;
line-height: 1.4;
}
.git-commit:last-child { border-bottom: none; }
.git-hash {
font-family: monospace;
color: var(--btn);
flex-shrink: 0;
}
.git-subject { flex: 1; color: var(--text); }
.git-meta { color: var(--hint); font-size: 11px; flex-shrink: 0; text-align: right; }
.git-status {
font-family: monospace;
font-weight: 700;
font-size: 12px;
flex-shrink: 0;
width: 24px;
text-align: center;
}
.git-status-m { color: #e2b93d; }
.git-status-a { color: #4caf50; }
.git-status-d { color: #ef5350; }
.git-status-u { color: var(--hint); }
.git-repo-item {
padding: 14px 14px 14px 16px;
margin-bottom: 10px;
cursor: pointer;
transition: transform 0.2s cubic-bezier(0.25, 1, 0.5, 1), box-shadow 0.2s;
display: flex;
align-items: center;
gap: 12px;
-webkit-tap-highlight-color: transparent;
}
.git-repo-item:active { transform: scale(0.98); }
.git-repo-body { flex: 1; min-width: 0; }
.git-repo-name { font-weight: 600; font-size: 15px; margin-bottom: 4px; }
.git-repo-branch {
font-size: 13px;
color: var(--hint);
font-family: monospace;
}
.git-repo-arrow {
color: var(--hint);
font-size: 22px;
flex-shrink: 0;
}
.git-back-btn {
display: inline-flex;
align-items: center;
gap: 4px;
color: var(--btn);
font-size: 14px;
font-weight: 600;
background: none;
border: none;
cursor: pointer;
padding: 8px 0;
margin-bottom: 8px;
-webkit-tap-highlight-color: transparent;
}
.git-back-btn:active { opacity: 0.6; }
.worktree-list {
margin-top: 8px;
display: flex;
flex-direction: column;
gap: 8px;
}
.worktree-item {
display: flex;
align-items: flex-start;
gap: 10px;
padding: 10px;
border-radius: 12px;
border: 1px solid var(--glass-border);
background: var(--glass-bg);
}
.worktree-item.dirty {
border-color: rgba(255, 152, 0, 0.45);
}
.worktree-main {
flex: 1;
min-width: 0;
}
.worktree-name-row {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 4px;
}
.worktree-name {
font-size: 14px;
font-weight: 600;
word-break: break-word;
}
.worktree-branch {
font-family: monospace;
font-size: 12px;
color: var(--hint);
margin-bottom: 3px;
}
.worktree-last {
font-size: 11px;
color: var(--hint);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.worktree-dirty,
.worktree-clean {
font-size: 10px;
font-weight: 700;
padding: 2px 6px;
border-radius: 999px;
}
.worktree-dirty {
color: #c26b00;
background: rgba(255, 152, 0, 0.2);
}
.worktree-clean {
color: #1b8f3a;
background: rgba(76, 175, 80, 0.18);
}
.worktree-actions {
display: flex;
flex-direction: column;
gap: 6px;
flex-shrink: 0;
}
.worktree-btn {
border: 1px solid var(--glass-border-interactive);
background: var(--glass-bg);
color: var(--text);
border-radius: 8px;
padding: 6px 10px;
font-size: 12px;
font-weight: 600;
cursor: pointer;
min-width: 74px;
}
.worktree-btn.merge { color: var(--btn); }
.worktree-btn.dispose { color: #d14b4b; }
.worktree-btn:disabled {
opacity: 0.6;
cursor: default;
}
/* Dev header */
.dev-header {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 4px;
margin-bottom: 8px;
}
.dev-header-title { font-weight: 600; font-size: 15px; }
.dev-header-target {
color: var(--hint);
font-size: 13px;
margin-left: auto;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Dev target cards */
.dev-target-item {
padding: 12px 14px;
margin-bottom: 8px;
cursor: pointer;
display: flex;
align-items: center;
gap: 10px;
transition: transform 0.2s cubic-bezier(0.25, 1, 0.5, 1), box-shadow 0.2s, border-color 0.2s;
-webkit-tap-highlight-color: transparent;
}
.dev-target-item:active { transform: scale(0.98); }
.dev-target-item.active {
border-color: var(--btn);
box-shadow: 0 2px 8px var(--glass-shadow);
}
.dev-target-dot {
width: 10px;
height: 10px;
border-radius: 50%;
flex-shrink: 0;
background: var(--hint);
}
.dev-target-dot.on { background: var(--done); }
.dev-target-name { font-weight: 600; font-size: 14px; }
.dev-target-url {
color: var(--hint);
font-size: 13px;
margin-left: auto;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.dev-target-delete {
flex-shrink: 0;
width: 24px;
height: 24px;
padding: 6px;
margin: -6px;
margin-left: 8px;
border-radius: 50%;
color: var(--hint);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: color 0.15s, background 0.15s;
-webkit-tap-highlight-color: transparent;
}
.dev-target-delete:active {
color: #ff3b30;
background: rgba(255, 59, 48, 0.1);
}
/* Log viewer */
.log-filter-chips { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 8px; }
.log-filter-chip {
padding: 4px 10px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
border: 1px solid var(--glass-border-interactive);
background: var(--glass-bg);
color: var(--hint);
cursor: pointer;
transition: all 0.2s;
-webkit-tap-highlight-color: transparent;
}
.log-filter-chip.active {
background: var(--btn);
color: var(--btn-text);
border-color: var(--btn);
}
#logs-content {
max-height: 50vh;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
.log-pagination {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-top: 8px;
color: var(--hint);
font-size: 11px;
}
.log-page-btn {
padding: 4px 10px;
border-radius: 10px;
font-size: 11px;
font-weight: 500;
border: 1px solid var(--glass-border-interactive);
background: var(--glass-bg);
color: var(--text);
cursor: pointer;
}
.log-page-btn:disabled {
opacity: 0.45;
cursor: default;
}
.log-entry {
display: flex;
align-items: flex-start;
gap: 6px;
padding: 3px 0;
font-size: 11px;
font-family: 'SF Mono', 'Menlo', 'Consolas', monospace;
line-height: 1.4;
border-bottom: 1px solid var(--glass-divider);
}
.log-ts { color: var(--hint); flex-shrink: 0; white-space: nowrap; }
.log-badge {
flex-shrink: 0;
padding: 0 4px;
border-radius: 4px;
font-size: 9px;
font-weight: 700;
text-transform: uppercase;
line-height: 16px;
}
.log-badge.error { background: rgba(255,59,48,0.15); color: #ff3b30; }
.log-badge.warn { background: rgba(255,204,0,0.15); color: #cc9900; }
.log-badge.info { background: rgba(52,199,89,0.12); color: #34c759; }
.log-badge.debug { background: rgba(142,142,147,0.12); color: #8e8e93; }
.log-comp { color: var(--link); flex-shrink: 0; font-size: 10px; }
.log-msg { flex: 1; word-break: break-all; color: var(--text); }
.log-fields { color: var(--hint); font-size: 10px; }
.log-actions { display: flex; gap: 8px; align-items: center; margin-top: 8px; }
.log-snap-btn {
padding: 6px 12px;
border-radius: 10px;
font-size: 12px;
font-weight: 500;
background: var(--glass-bg);
border: 1px solid var(--glass-border-interactive);
color: var(--text);
cursor: pointer;
}
/* ── Orch panel ── */
.orch-room-row { display:flex; align-items:flex-start; padding:12px 0; }
.orch-side { display:flex; flex-direction:column; align-items:center; gap:12px; padding-top:20px; width:40px; flex-shrink:0; }
.orch-badge { display:flex; flex-direction:column; align-items:center; gap:3px; opacity:0.3; transition:opacity 0.3s; }
.orch-badge.alive { opacity:1; }
.orch-badge.alive .orch-badge-label { color:#4a6ac0; }
.orch-badge.alive .orch-badge-dot { background:#4ade80; }
.orch-badge.toolcall .orch-badge-dot { background:#fb923c; animation:orch-blink 0.2s step-end infinite; }
.orch-badge.waiting .orch-badge-dot { background:#60a5fa; }
.orch-badge.talking .orch-badge-label{ color:#facc15; }
.orch-badge.talking .orch-badge-dot { background:#facc15; animation:orch-blink 0.6s step-end infinite; }
.orch-badge-emoji { font-size:16px; line-height:1; }
.orch-badge-label { font-size:7px; color:var(--hint); letter-spacing:0.05em; text-transform:uppercase; }
.orch-badge-dot { width:4px; height:4px; border-radius:50%; background:var(--hint); }
@keyframes orch-blink { 50% { opacity:0; } }
.orch-canvas-wrap { flex:1; min-width:0; border-radius:12px; overflow:hidden; background:#060810; }
.orch-canvas-wrap canvas { image-rendering:pixelated; image-rendering:crisp-edges; display:block; width:100%; aspect-ratio:1/1; }
.orch-status { text-align:center; font-size:11px; color:var(--hint); padding:6px 0 4px; }
.orch-dot { display:inline-block; width:6px; height:6px; border-radius:50%; background:var(--hint); margin-right:4px; vertical-align:middle; }
.orch-dot.on { background:#4ade80; }
/* Session graph tree */
.session-tree { padding:0; margin:0; list-style:none; }
.session-tree-node { position:relative; padding:4px 0 4px 20px; font-size:13px; }
.session-tree-node::before {
content:''; position:absolute; left:0; top:0; bottom:0;
border-left:1px solid var(--secondary-bg);
}
.session-tree-node::after {
content:''; position:absolute; left:0; top:14px; width:16px;
border-top:1px solid var(--secondary-bg);
}
.session-tree-node:last-child::before { height:14px; }
.session-tree-children { padding-left:20px; margin:0; list-style:none; }
.session-tree-label { font-weight:600; }
.session-tree-meta { color:var(--hint); font-size:11px; margin-left:6px; }
.session-tree-icon { font-size:10px; margin-right:4px; }
.session-tree-icon.active { color:var(--done); }
.session-tree-icon.completed { color:var(--hint); }

View file

@ -0,0 +1,8 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
environment: 'happy-dom',
include: ['src/**/*.test.js'],
},
});

View file

@ -2,18 +2,33 @@ package miniapp
import (
"embed"
"html/template"
"io/fs"
"net/http"
"net/http/httputil"
"net/url"
"strings"
"sync"
"github.com/sipeed/picoclaw/pkg/orch"
)
//go:embed static/index.html static/map.js
//go:generate bun run --cwd frontend build
//go:embed static
var staticFS embed.FS
var (
miniappStaticFS = mustMiniappStaticFS()
miniappTemplate = template.Must(template.ParseFS(staticFS, "static/index.html"))
)
func mustMiniappStaticFS() fs.FS {
sub, err := fs.Sub(staticFS, "static")
if err != nil {
panic("miniapp: failed to create static sub filesystem: " + err.Error())
}
return sub
}
// Handler serves the Mini App HTML and API endpoints.
type Handler struct {
provider DataProvider
@ -68,6 +83,8 @@ func (h *Handler) SetOrchBroadcaster(b *orch.Broadcaster) {
// RegisterRoutes registers Mini App routes on the given mux.
func (h *Handler) RegisterRoutes(mux *http.ServeMux) {
mux.HandleFunc("/miniapp", h.serveIndex)
mux.HandleFunc("/miniapp/index.html", h.serveIndex)
mux.HandleFunc("/miniapp/", h.serveStatic)
mux.HandleFunc("/miniapp/api/skills", h.requireAuth(h.apiSkills))
mux.HandleFunc("/miniapp/api/plan", h.requireAuth(h.apiPlan))
mux.HandleFunc("/miniapp/api/session", h.requireAuth(h.apiSession))
@ -84,30 +101,27 @@ func (h *Handler) RegisterRoutes(mux *http.ServeMux) {
mux.HandleFunc("/miniapp/api/logs/snapshot", h.requireAuth(h.apiLogsSnapshot))
mux.HandleFunc("/miniapp/api/logs/snapshot/", h.requireAuth(h.apiLogsSnapshotDownload))
mux.HandleFunc("/miniapp/api/orchestration/ws", h.requireAuth(h.wsOrchestration))
mux.HandleFunc("/miniapp/map.js", h.serveMapJS)
mux.HandleFunc("/miniapp/dev/console", h.apiDevConsole)
mux.HandleFunc("/miniapp/dev/", h.serveDevProxy)
}
func (h *Handler) serveIndex(w http.ResponseWriter, r *http.Request) {
data, err := staticFS.ReadFile("static/index.html")
if err != nil {
http.Error(w, "not found", http.StatusNotFound)
w.Header().Set("Content-Type", "text/html; charset=utf-8")
data := struct {
OrchEnabled bool
}{
OrchEnabled: h.orchBroadcaster != nil,
}
if err := miniappTemplate.Execute(w, data); err != nil {
http.Error(w, "failed to render template", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
if h.orchBroadcaster != nil {
data = []byte(strings.Replace(string(data), "</head>", "<script>var ORCH_ENABLED=true;</script></head>", 1))
}
w.Write(data)
}
func (h *Handler) serveMapJS(w http.ResponseWriter, r *http.Request) {
data, err := staticFS.ReadFile("static/map.js")
if err != nil {
http.Error(w, "not found", http.StatusNotFound)
func (h *Handler) serveStatic(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/miniapp/" {
h.serveIndex(w, r)
return
}
w.Header().Set("Content-Type", "application/javascript; charset=utf-8")
w.Write(data)
http.StripPrefix("/miniapp/", http.FileServer(http.FS(miniappStaticFS))).ServeHTTP(w, r)
}

View file

@ -23,6 +23,7 @@ import (
"time"
gitpkg "github.com/sipeed/picoclaw/pkg/git"
"github.com/sipeed/picoclaw/pkg/orch"
"github.com/sipeed/picoclaw/pkg/skills"
"github.com/sipeed/picoclaw/pkg/stats"
)
@ -227,6 +228,60 @@ func testInitData() string {
}, testBotToken)
}
func TestMiniApp_IndexTemplateInjectsOrchFlag(t *testing.T) {
notifier := NewStateNotifier()
h := NewHandler(&mockDataProvider{}, &mockSender{}, testBotToken, notifier, nil, "")
mux := http.NewServeMux()
h.RegisterRoutes(mux)
w := httptest.NewRecorder()
mux.ServeHTTP(w, httptest.NewRequest("GET", "/miniapp", nil))
if w.Code != http.StatusOK {
t.Fatalf("expected 200, got %d", w.Code)
}
if !strings.Contains(w.Body.String(), "window.ORCH_ENABLED = false;") {
t.Fatalf("expected ORCH_ENABLED=false in rendered template")
}
h.SetOrchBroadcaster(orch.NewBroadcaster())
w2 := httptest.NewRecorder()
mux.ServeHTTP(w2, httptest.NewRequest("GET", "/miniapp", nil))
if w2.Code != http.StatusOK {
t.Fatalf("expected 200 with broadcaster, got %d", w2.Code)
}
if !strings.Contains(w2.Body.String(), "window.ORCH_ENABLED = true;") {
t.Fatalf("expected ORCH_ENABLED=true in rendered template")
}
}
func TestMiniApp_StaticFileServerServesAssets(t *testing.T) {
notifier := NewStateNotifier()
h := NewHandler(&mockDataProvider{}, &mockSender{}, testBotToken, notifier, nil, "")
mux := http.NewServeMux()
h.RegisterRoutes(mux)
tests := []struct {
path string
want string
}{
{path: "/miniapp/map-preview.html", want: "Orchestration Room"},
{path: "/miniapp/dist/map.js", want: "MAP_POSITIONS"},
{path: "/miniapp/dist/app.js", want: "renderLogs"},
}
for _, tc := range tests {
t.Run(tc.path, func(t *testing.T) {
w := httptest.NewRecorder()
mux.ServeHTTP(w, httptest.NewRequest("GET", tc.path, nil))
if w.Code != http.StatusOK {
t.Fatalf("expected 200 for %s, got %d", tc.path, w.Code)
}
if !strings.Contains(w.Body.String(), tc.want) {
t.Fatalf("expected %q in %s", tc.want, tc.path)
}
})
}
}
func TestSSE_AuthRequired(t *testing.T) {
notifier := NewStateNotifier()
h := NewHandler(&mockDataProvider{}, &mockSender{}, testBotToken, notifier, nil, "")

1230
pkg/miniapp/static/dist/app.css vendored Normal file

File diff suppressed because it is too large Load diff

1459
pkg/miniapp/static/dist/app.js vendored Normal file

File diff suppressed because it is too large Load diff

212
pkg/miniapp/static/dist/map.js vendored Normal file
View file

@ -0,0 +1,212 @@
// map.js — Orchestration Room
//
// External asset: drop map.png (320×320px) next to index.html to replace
// the procedural fallback. Character positions (MAP_POSITIONS) are defined
// in canvas-pixel coordinates and remain valid regardless of which rendering
// path is used — just make sure your map.png matches them.
//
// Usage:
// loadMapAsset(function() { drawMap(ctx); }); // call once on init
// drawMap(ctx); // call each frame
// ─── Character home positions (px, canvas 320×320) ─────────────────────────
//
// ┌──────────────────────────────┐
// │ [conductor desk] │ y ≈ 2050
// │ 👑(160,58) 👩‍💼(108,58) │
// │ [carpet] │
// │ [WS1] [WS2] [WS3] │ y ≈ 80100
// │ 🔍40 💻144 📊248 │ y = 106
// │ [meeting area] │ y ≈ 130192
// │ [WS4] [WS5] │ y ≈ 200220
// │ 🔧40 🎯144 │ y = 222
// │ 🚪(160,308) │ door
// └──────────────────────────────┘
var MAP_POSITIONS = {
door: { x: 160, y: 314 }, // entry / exit point
conductor: { x: 160, y: 58 },
secretary: { x: 108, y: 58 },
heartbeat: { x: 230, y: 58 }, // pigeon messenger — periodic heartbeat agent
meeting: { x: 160, y: 161 }, // neutral zone for conversations
stations: [
{ x: 40, y: 106 }, // S0 scout
{ x: 144, y: 106 }, // S1 analyst
{ x: 248, y: 106 }, // S2 coder
{ x: 40, y: 222 }, // S3 worker
{ x: 144, y: 222 }, // S4 coordinator
],
};
// ─── Asset loading ──────────────────────────────────────────────────────────
var _mapImage = null;
// Call once before first draw. cb() is invoked when ready (image or fallback).
function loadMapAsset(cb) {
var img = new Image();
img.onload = function() { _mapImage = img; cb(); };
img.onerror = function() { cb(); }; // no map.png → use fallback
img.src = './map.png';
}
// ─── Public draw entry point ────────────────────────────────────────────────
function drawMap(ctx) {
ctx.imageSmoothingEnabled = false;
if (_mapImage) {
ctx.drawImage(_mapImage, 0, 0, 320, 320);
} else {
_drawMapFallback(ctx);
}
}
// ─── Procedural fallback ────────────────────────────────────────────────────
var _C = {
wallDark: '#0c1018',
wallHighlight: '#252d3f',
floorA: '#171b2c',
floorB: '#1b2033',
carpetBase: '#1a2050',
carpetBorder: '#2a3480',
deskBack: '#2c3e6b',
deskTop: '#3a50a0',
deskEdge: '#4a6ac0',
deskShadow: '#1a2448',
monitorFrame: '#070b14',
monitorBlue: '#1040a0',
monitorGlow: '#4488ff',
wsBase: '#162818',
wsTop: '#1e3822',
wsEdge: '#2a5030',
termGlow: '#00dd55',
rugFill: '#1c2248',
rugBorder: '#283070',
doorMid: '#8a5818',
doorLight: '#a06820',
doorGold: '#c8940a',
};
function _r(ctx, color, x, y, w, h, alpha) {
ctx.globalAlpha = alpha === undefined ? 1 : alpha;
ctx.fillStyle = color;
ctx.fillRect(x, y, w, h);
ctx.globalAlpha = 1;
}
function _b(ctx, color, x, y, w, h) {
ctx.strokeStyle = color;
ctx.lineWidth = 1;
ctx.strokeRect(x + 0.5, y + 0.5, w - 1, h - 1);
}
function _dot(ctx, color, x, y) {
ctx.fillStyle = color;
ctx.fillRect(x, y, 2, 2);
}
function _workstation(ctx, x, y) {
_r(ctx, _C.wsBase, x, y, 48, 20);
_r(ctx, _C.wsTop, x, y, 48, 8);
_r(ctx, _C.wsEdge, x, y, 2, 20);
_r(ctx, _C.wsEdge, x+46, y, 2, 20);
_r(ctx, _C.wsEdge, x, y, 48, 2);
// terminal screen
_r(ctx, _C.monitorFrame, x+16, y+2, 16, 12);
_r(ctx, '#041008', x+17, y+3, 14, 10);
_r(ctx, '#003315', x+18, y+4, 12, 8);
_r(ctx, _C.termGlow, x+20, y+6, 8, 3);
_dot(ctx, '#00ff88', x+22, y+6);
}
function _drawMapFallback(ctx) {
var T = 16;
// floor tiles
for (var ty = 0; ty < 20; ty++) {
for (var tx = 0; tx < 20; tx++) {
ctx.fillStyle = (tx + ty) % 2 === 0 ? _C.floorA : _C.floorB;
ctx.fillRect(tx * T, ty * T, T, T);
}
}
// conductor carpet
_r(ctx, _C.carpetBase, 16, 16, 288, 50);
_b(ctx, _C.carpetBorder, 18, 18, 284, 46);
// conductor desk
_r(ctx, _C.deskBack, 96, 20, 128, 30);
_r(ctx, _C.deskTop, 96, 20, 128, 12);
_r(ctx, _C.deskEdge, 96, 20, 128, 2);
_r(ctx, _C.deskEdge, 96, 20, 2, 30);
_r(ctx, _C.deskEdge, 222, 20, 2, 30);
_r(ctx, _C.deskShadow,96,48, 128, 4);
// monitor
_r(ctx, _C.monitorFrame, 138, 22, 44, 14);
_r(ctx, _C.monitorBlue, 140, 23, 40, 12);
_r(ctx, _C.monitorGlow, 156, 26, 8, 6);
_r(ctx, '#6699ff', 158, 27, 4, 3);
// workstations
_workstation(ctx, 16, 80); // S0
_workstation(ctx, 128, 80); // S1 (x+24 = 152 ≈ 144 center)
_workstation(ctx, 224, 80); // S2
_workstation(ctx, 16, 200); // S3
_workstation(ctx, 128, 200); // S4
// meeting rug
_r(ctx, _C.rugFill, 64, 130, 192, 62, 0.55);
_b(ctx, _C.rugBorder, 66, 132, 188, 58);
_b(ctx, '#202860', 70, 136, 180, 50);
// bulletin board (left wall)
_r(ctx, '#2c1a06', 18, 148, 36, 44);
_r(ctx, '#3a2508', 20, 150, 32, 40);
_r(ctx, '#cc9900', 22, 153, 12, 8);
_r(ctx, '#dd8800', 22, 164, 10, 6);
_r(ctx, '#bb7700', 34, 155, 13, 8);
_r(ctx, '#ccaa00', 33, 165, 11, 6);
_dot(ctx, '#ff4444', 28, 153);
_dot(ctx, '#44aaff', 41, 158);
_dot(ctx, '#44ff88', 27, 165);
// server rack (right wall)
_r(ctx, '#111122', 285, 80, 18, 112);
_r(ctx, '#181830', 287, 82, 14, 108);
for (var i = 0; i < 10; i++) {
var ry = 85 + i * 10;
_r(ctx, '#0a0a12', 288, ry, 12, 8);
var lc = ['#00ff44','#0044ff','#ff3300','#111111'][i % 4];
_r(ctx, lc, 296, ry + 2, 3, 4);
}
// walls (drawn last to cover any overruns)
_r(ctx, _C.wallDark, 0, 0, 320, 16);
_r(ctx, _C.wallHighlight, 0, 14, 320, 2);
_r(ctx, _C.wallDark, 0, 0, 16, 320);
_r(ctx, _C.wallHighlight,14, 0, 2, 320);
_r(ctx, _C.wallDark, 304, 0, 16, 320);
_r(ctx, _C.wallHighlight,304, 0, 2, 320);
_r(ctx, _C.wallDark, 0, 304, 144, 16);
_r(ctx, _C.wallDark, 176, 304, 144, 16);
_r(ctx, _C.wallHighlight, 0, 304, 144, 2);
_r(ctx, _C.wallHighlight,176, 304, 144, 2);
// door
_r(ctx, '#0a0808', 144, 292, 32, 12); // outside (dark)
_r(ctx, _C.doorMid, 144, 280, 32, 24);
_r(ctx, _C.doorLight, 144, 280, 32, 3);
_r(ctx, _C.doorLight, 144, 280, 3, 24);
_r(ctx, _C.doorLight, 173, 280, 3, 24);
_r(ctx, '#4a2408', 146, 284, 12, 16); // door panels
_r(ctx, '#4a2408', 162, 284, 12, 16);
_r(ctx, _C.doorGold, 170, 291, 5, 5); // handle
}
// Expose map helpers for app.js runtime.
globalThis.MAP_POSITIONS = MAP_POSITIONS;
globalThis.loadMapAsset = loadMapAsset;
globalThis.drawMap = drawMap;

File diff suppressed because it is too large Load diff

View file

@ -96,7 +96,7 @@
.demo-bar span { color: #3a4a80; }
.demo-bar span.on { color: #4ade80; }
</style>
<script src="map.js"></script>
<script src="dist/map.js"></script>
</head>
<body>
@ -456,3 +456,4 @@ loadMapAsset(function() {
</script>
</body>
</html>

View file

@ -72,3 +72,34 @@ Fields 表示バグが検出されずに ship された前科あり。
- `ORCH_ENABLED` の注入がテンプレートまたはビルド時変数で動作
- `renderLogs()` のユニットテストが CI で実行される
- `go build ./...` が変わらず動作 (embed パスの整合性)
## 作業報告 (2026-03-05)
- 実装完了: タスク 1 / 2 / 3
- 主要変更:
- `pkg/miniapp/miniapp.go`
- `//go:embed static` に変更
- `/miniapp/``http.FileServer(http.FS(...))` で一括配信
- `/miniapp` / `/miniapp/index.html``html/template` で描画し `OrchEnabled` を注入
- 旧 `serveMapJS` 個別ルートを削除
- フロントエンド分離
- `pkg/miniapp/frontend/src/``app.js` / `styles.css` / `map.js` / `logs_view.js` を配置
- `pkg/miniapp/static/index.html` はテンプレート + 外部アセット参照へ縮小
- `pkg/miniapp/static/dist/``app.js` / `app.css` / `map.js` を生成
- バンドル導線
- `pkg/miniapp/frontend/build.mjs` + `package.json` を追加
- `go generate ./...``bun run --cwd frontend build` が実行されるよう接続
- Log viewer 強化
- `logs_view.js` へレンダリングロジックを抽出
- フィルタ/ページネーションを追加
- `vitest + happy-dom` による unit test を追加 (`logs_view.test.js`)
- CI 更新 (`.github/workflows/pr.yml`)
- Bun セットアップを追加
- frontend の `pnpm test` を追加
- 検証結果:
- `bun run --cwd pkg/miniapp/frontend build`: 成功
- `pnpm --dir pkg/miniapp/frontend test`: 成功
- `go generate ./...`: 成功
- `go test ./pkg/miniapp/...`: 成功
- `go test ./...`: 一部既存の環境依存テストが Windows 環境で失敗TASK-4差分外