diff --git a/.gitea/workflows/deploy-dev.yml b/.gitea/workflows/deploy-dev.yml index b43482f..8fd7c4c 100644 --- a/.gitea/workflows/deploy-dev.yml +++ b/.gitea/workflows/deploy-dev.yml @@ -169,11 +169,8 @@ jobs: run: | set -e echo "Building on VPS (SSD)..." - CACHE_ARG="" - if podman image exists nas-dashboard-dev:latest 2>/dev/null; then - CACHE_ARG="--cache-from nas-dashboard-dev:latest" - fi - podman build $CACHE_ARG -t nas-dashboard-dev:latest dashboard/ + # Podman caches layers automatically; no --cache-from needed + podman build -t nas-dashboard-dev:latest dashboard/ - name: Transfer image to NAS run: | @@ -190,6 +187,12 @@ jobs: needs: [build-image-dev, frontend-tests] env: SECRET_KEY: test-secret-key-for-ci-environment-32chars-minimum + container: + volumes: + - /var/packages/ContainerManager/target/usr/bin/docker:/usr/bin/docker + - /var/packages/ContainerManager/target/usr/bin/docker-compose:/usr/lib/docker/cli-plugins/docker-compose + - /volume1/docker/nas-dashboard:/volume1/docker/nas-dashboard + steps: - name: Checkout repository run: | @@ -210,24 +213,23 @@ jobs: GITEA_DB_PASSWORD: ${{ secrets.GITEA_DB_PASSWORD }} run: | set -u - DOCKER="ssh nasts /volume1/@appstore/ContainerManager/usr/bin/docker" - COMPOSE="ssh nasts /volume1/@appstore/ContainerManager/usr/bin/docker-compose" + DOCKER="/volume1/@appstore/ContainerManager/usr/bin/docker" + COMPOSE="/usr/lib/docker/cli-plugins/docker-compose" NAS_VOL="/volume1/docker/nas-dashboard" - # Sync compose file to NAS - ssh nasts "mkdir -p $NAS_VOL" - scp dashboard/docker-compose.dev.yml "nasts:$NAS_VOL/docker-compose.dev.yml" + # Sync compose file to NAS (already done in previous step, just in case) + mkdir -p $NAS_VOL # Stop existing container if running - $COMPOSE -f docker-compose.dev.yml -p nas-dashboard-dev down 2>/dev/null || true + $COMPOSE -f $NAS_VOL/docker-compose.dev.yml -p nas-dashboard-dev down 2>/dev/null || true - # Ensure network exists (create only if it doesn't exist) + # Ensure network exists if ! $DOCKER network inspect nas-dashboard_internal >/dev/null 2>&1; then $DOCKER network create nas-dashboard_internal fi # Deploy - ssh nasts "cd $NAS_VOL && GITEA_TOKEN=$GITEA_TOKEN SECRET_KEY=$SECRET_KEY /volume1/@appstore/ContainerManager/usr/bin/docker-compose -f docker-compose.dev.yml -p nas-dashboard-dev up -d --pull never" 2>&1 || true + cd $NAS_VOL && GITEA_TOKEN=$GITEA_TOKEN SECRET_KEY=$SECRET_KEY $COMPOSE -f docker-compose.dev.yml -p nas-dashboard-dev up -d --pull never 2>&1 || true if ! $DOCKER container inspect nas-dashboard-dev >/dev/null 2>&1; then echo "Compose failed to create container..." @@ -244,7 +246,7 @@ jobs: - name: Wait for container to be healthy run: | - DOCKER="ssh nasts /volume1/@appstore/ContainerManager/usr/bin/docker" + DOCKER="/volume1/@appstore/ContainerManager/usr/bin/docker" echo "Waiting for container to be healthy..." for i in {1..30}; do STATUS=$($DOCKER inspect --format='{{.State.Health.Status}}' nas-dashboard-dev 2>/dev/null || echo "starting") @@ -264,7 +266,7 @@ jobs: - name: Run smoke tests run: | - DOCKER="ssh nasts /volume1/@appstore/ContainerManager/usr/bin/docker" + DOCKER="/volume1/@appstore/ContainerManager/usr/bin/docker" echo "=== Running smoke tests via docker exec ===" # Test 1: Health check diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index bc74eb9..aecd938 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -14,7 +14,7 @@ concurrency: jobs: backend-tests: name: Backend Tests - runs-on: vps + runs-on: ubuntu-latest env: SECRET_KEY: test-secret-key-for-ci-environment-32chars-minimum @@ -22,7 +22,7 @@ jobs: - name: Checkout repository run: | if [ ! -d .git ]; then - git clone --depth=1 "http://100.78.131.124:3300/${GITHUB_REPOSITORY}.git" . + git clone --depth=1 "http://172.21.0.1:3300/${GITHUB_REPOSITORY}.git" . fi - name: Setup Python @@ -172,39 +172,40 @@ jobs: name: Build Main Image runs-on: vps needs: [backend-tests] + steps: - name: Checkout repository run: | if [ ! -d .git ]; then git clone --depth=1 "http://100.78.131.124:3300/${GITHUB_REPOSITORY}.git" . fi - - name: Build main Docker image + + - name: Build Docker image run: | set -e echo "Building on VPS (SSD)..." - CACHE_ARG="" - if podman image exists nas-dashboard:latest 2>/dev/null; then - CACHE_ARG="--cache-from nas-dashboard:latest" - fi - podman build $CACHE_ARG -t nas-dashboard:latest dashboard/ + # Podman caches layers automatically; no --cache-from needed + podman build -t nas-dashboard:latest dashboard/ + - name: Transfer image to NAS run: | set -e - echo "Streaming main image to NAS..." + echo "Streaming image to NAS..." podman save nas-dashboard:latest | ssh nasts "/volume1/@appstore/ContainerManager/usr/bin/docker load 2>&1" - echo "Main image transferred successfully" + echo "Retagging image..." + ssh nasts "/volume1/@appstore/ContainerManager/usr/bin/docker tag localhost/nas-dashboard:latest nas-dashboard:latest 2>&1 && /volume1/@appstore/ContainerManager/usr/bin/docker image rm localhost/nas-dashboard:latest 2>&1" + echo "Image transferred and retagged successfully" deploy: name: Deploy to Production runs-on: nas - if: always() + if: always() && needs.build-image-main.result == 'success' needs: [build-image-main, frontend-tests] env: SECRET_KEY: test-secret-key-for-ci-environment-32chars-minimum GITEA_DB_PASSWORD: nyM3P4v1aYAsT7zfUtdguImNVHgFltCKCY/OjzWZVRE= container: network: gitea_gitea - options: --add-host=host.docker.internal:host-gateway --dns 192.168.31.1 --dns 8.8.8.8 volumes: - /var/packages/ContainerManager/target/usr/bin/docker:/usr/bin/docker - /volume1/docker/nas-dashboard:/nas-dashboard @@ -241,7 +242,7 @@ jobs: if ! docker container inspect nas-dashboard >/dev/null 2>&1; then echo "Compose failed, stripping networks and retrying..." docker rm -f docker-socket-proxy nas-dashboard 2>/dev/null || true - # ... (python script for stripping networks) ... + sed '/^networks:/,$d' /nas-dashboard/docker-compose.yml > /nas-dashboard/prod-ci.yml docker compose -f /nas-dashboard/prod-ci.yml up -d --pull never 2>&1 || true fi @@ -304,7 +305,13 @@ jobs: # Verify network connections echo "=== Network connections ===" - docker inspect nas-dashboard --format '{{json .NetworkSettings.Networks}}' | python3 -c 'import json,sys;nets=json.load(sys.stdin);[print(f" Connected to {n}") for n in nets];assert "nas-dashboard_internal" in nets, "Missing nas-dashboard_internal";print("All network connections established")' + if docker inspect nas-dashboard --format '{{json .NetworkSettings.Networks}}' | grep -q 'nas-dashboard_internal'; then + echo " Connected to nas-dashboard_internal" + echo "All network connections established" + else + echo "❌ Missing nas-dashboard_internal network" + exit 1 + fi # Wait for container to be healthy echo "Waiting for container to be healthy..." diff --git a/.gitignore b/.gitignore index 24a9957..98383bf 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,4 @@ venv/ .env.local .env.production dashboard/backend/.vite/ +dashboard/frontend/coverage/ diff --git a/dashboard/frontend/coverage/base.css b/dashboard/frontend/coverage/base.css deleted file mode 100644 index f418035..0000000 --- a/dashboard/frontend/coverage/base.css +++ /dev/null @@ -1,224 +0,0 @@ -body, html { - margin:0; padding: 0; - height: 100%; -} -body { - font-family: Helvetica Neue, Helvetica, Arial; - font-size: 14px; - color:#333; -} -.small { font-size: 12px; } -*, *:after, *:before { - -webkit-box-sizing:border-box; - -moz-box-sizing:border-box; - box-sizing:border-box; - } -h1 { font-size: 20px; margin: 0;} -h2 { font-size: 14px; } -pre { - font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace; - margin: 0; - padding: 0; - -moz-tab-size: 2; - -o-tab-size: 2; - tab-size: 2; -} -a { color:#0074D9; text-decoration:none; } -a:hover { text-decoration:underline; } -.strong { font-weight: bold; } -.space-top1 { padding: 10px 0 0 0; } -.pad2y { padding: 20px 0; } -.pad1y { padding: 10px 0; } -.pad2x { padding: 0 20px; } -.pad2 { padding: 20px; } -.pad1 { padding: 10px; } -.space-left2 { padding-left:55px; } -.space-right2 { padding-right:20px; } -.center { text-align:center; } -.clearfix { display:block; } -.clearfix:after { - content:''; - display:block; - height:0; - clear:both; - visibility:hidden; - } -.fl { float: left; } -@media only screen and (max-width:640px) { - .col3 { width:100%; max-width:100%; } - .hide-mobile { display:none!important; } -} - -.quiet { - color: #7f7f7f; - color: rgba(0,0,0,0.5); -} -.quiet a { opacity: 0.7; } - -.fraction { - font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace; - font-size: 10px; - color: #555; - background: #E8E8E8; - padding: 4px 5px; - border-radius: 3px; - vertical-align: middle; -} - -div.path a:link, div.path a:visited { color: #333; } -table.coverage { - border-collapse: collapse; - margin: 10px 0 0 0; - padding: 0; -} - -table.coverage td { - margin: 0; - padding: 0; - vertical-align: top; -} -table.coverage td.line-count { - text-align: right; - padding: 0 5px 0 20px; -} -table.coverage td.line-coverage { - text-align: right; - padding-right: 10px; - min-width:20px; -} - -table.coverage td span.cline-any { - display: inline-block; - padding: 0 5px; - width: 100%; -} -.missing-if-branch { - display: inline-block; - margin-right: 5px; - border-radius: 3px; - position: relative; - padding: 0 4px; - background: #333; - color: yellow; -} - -.skip-if-branch { - display: none; - margin-right: 10px; - position: relative; - padding: 0 4px; - background: #ccc; - color: white; -} -.missing-if-branch .typ, .skip-if-branch .typ { - color: inherit !important; -} -.coverage-summary { - border-collapse: collapse; - width: 100%; -} -.coverage-summary tr { border-bottom: 1px solid #bbb; } -.keyline-all { border: 1px solid #ddd; } -.coverage-summary td, .coverage-summary th { padding: 10px; } -.coverage-summary tbody { border: 1px solid #bbb; } -.coverage-summary td { border-right: 1px solid #bbb; } -.coverage-summary td:last-child { border-right: none; } -.coverage-summary th { - text-align: left; - font-weight: normal; - white-space: nowrap; -} -.coverage-summary th.file { border-right: none !important; } -.coverage-summary th.pct { } -.coverage-summary th.pic, -.coverage-summary th.abs, -.coverage-summary td.pct, -.coverage-summary td.abs { text-align: right; } -.coverage-summary td.file { white-space: nowrap; } -.coverage-summary td.pic { min-width: 120px !important; } -.coverage-summary tfoot td { } - -.coverage-summary .sorter { - height: 10px; - width: 7px; - display: inline-block; - margin-left: 0.5em; - background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent; -} -.coverage-summary .sorted .sorter { - background-position: 0 -20px; -} -.coverage-summary .sorted-desc .sorter { - background-position: 0 -10px; -} -.status-line { height: 10px; } -/* yellow */ -.cbranch-no { background: yellow !important; color: #111; } -/* dark red */ -.red.solid, .status-line.low, .low .cover-fill { background:#C21F39 } -.low .chart { border:1px solid #C21F39 } -.highlighted, -.highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{ - background: #C21F39 !important; -} -/* medium red */ -.cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE } -/* light red */ -.low, .cline-no { background:#FCE1E5 } -/* light green */ -.high, .cline-yes { background:rgb(230,245,208) } -/* medium green */ -.cstat-yes { background:rgb(161,215,106) } -/* dark green */ -.status-line.high, .high .cover-fill { background:rgb(77,146,33) } -.high .chart { border:1px solid rgb(77,146,33) } -/* dark yellow (gold) */ -.status-line.medium, .medium .cover-fill { background: #f9cd0b; } -.medium .chart { border:1px solid #f9cd0b; } -/* light yellow */ -.medium { background: #fff4c2; } - -.cstat-skip { background: #ddd; color: #111; } -.fstat-skip { background: #ddd; color: #111 !important; } -.cbranch-skip { background: #ddd !important; color: #111; } - -span.cline-neutral { background: #eaeaea; } - -.coverage-summary td.empty { - opacity: .5; - padding-top: 4px; - padding-bottom: 4px; - line-height: 1; - color: #888; -} - -.cover-fill, .cover-empty { - display:inline-block; - height: 12px; -} -.chart { - line-height: 0; -} -.cover-empty { - background: white; -} -.cover-full { - border-right: none !important; -} -pre.prettyprint { - border: none !important; - padding: 0 !important; - margin: 0 !important; -} -.com { color: #999 !important; } -.ignore-none { color: #999; font-weight: normal; } - -.wrapper { - min-height: 100%; - height: auto !important; - height: 100%; - margin: 0 auto -48px; -} -.footer, .push { - height: 48px; -} diff --git a/dashboard/frontend/coverage/block-navigation.js b/dashboard/frontend/coverage/block-navigation.js deleted file mode 100644 index 530d1ed..0000000 --- a/dashboard/frontend/coverage/block-navigation.js +++ /dev/null @@ -1,87 +0,0 @@ -/* eslint-disable */ -var jumpToCode = (function init() { - // Classes of code we would like to highlight in the file view - var missingCoverageClasses = ['.cbranch-no', '.cstat-no', '.fstat-no']; - - // Elements to highlight in the file listing view - var fileListingElements = ['td.pct.low']; - - // We don't want to select elements that are direct descendants of another match - var notSelector = ':not(' + missingCoverageClasses.join('):not(') + ') > '; // becomes `:not(a):not(b) > ` - - // Selector that finds elements on the page to which we can jump - var selector = - fileListingElements.join(', ') + - ', ' + - notSelector + - missingCoverageClasses.join(', ' + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b` - - // The NodeList of matching elements - var missingCoverageElements = document.querySelectorAll(selector); - - var currentIndex; - - function toggleClass(index) { - missingCoverageElements - .item(currentIndex) - .classList.remove('highlighted'); - missingCoverageElements.item(index).classList.add('highlighted'); - } - - function makeCurrent(index) { - toggleClass(index); - currentIndex = index; - missingCoverageElements.item(index).scrollIntoView({ - behavior: 'smooth', - block: 'center', - inline: 'center' - }); - } - - function goToPrevious() { - var nextIndex = 0; - if (typeof currentIndex !== 'number' || currentIndex === 0) { - nextIndex = missingCoverageElements.length - 1; - } else if (missingCoverageElements.length > 1) { - nextIndex = currentIndex - 1; - } - - makeCurrent(nextIndex); - } - - function goToNext() { - var nextIndex = 0; - - if ( - typeof currentIndex === 'number' && - currentIndex < missingCoverageElements.length - 1 - ) { - nextIndex = currentIndex + 1; - } - - makeCurrent(nextIndex); - } - - return function jump(event) { - if ( - document.getElementById('fileSearch') === document.activeElement && - document.activeElement != null - ) { - // if we're currently focused on the search input, we don't want to navigate - return; - } - - switch (event.which) { - case 78: // n - case 74: // j - goToNext(); - break; - case 66: // b - case 75: // k - case 80: // p - goToPrevious(); - break; - } - }; -})(); -window.addEventListener('keydown', jumpToCode); diff --git a/dashboard/frontend/coverage/favicon.png b/dashboard/frontend/coverage/favicon.png deleted file mode 100644 index c1525b8..0000000 Binary files a/dashboard/frontend/coverage/favicon.png and /dev/null differ diff --git a/dashboard/frontend/coverage/index.html b/dashboard/frontend/coverage/index.html deleted file mode 100644 index 1f0658f..0000000 --- a/dashboard/frontend/coverage/index.html +++ /dev/null @@ -1,176 +0,0 @@ - - - - - - Code coverage report for All files - - - - - - - - - -
-
-

All files

-
- -
- 4.5% - Statements - 172/3818 -
- - -
- 53.16% - Branches - 42/79 -
- - -
- 29.31% - Functions - 17/58 -
- - -
- 4.5% - Lines - 172/3818 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FileStatementsBranchesFunctionsLines
src -
-
0%0/1820%0/20%0/20%0/182
src/components -
-
0%0/3010%0/10%0/10%0/301
src/components/opc -
-
0%0/3930%0/50%0/50%0/393
src/lib -
-
22.06%124/56284.78%39/4651.72%15/2922.06%124/562
src/routes -
-
2.01%48/238012%3/259.52%2/212.01%48/2380
-
-
-
- - - - - - - - \ No newline at end of file diff --git a/dashboard/frontend/coverage/lcov-report/base.css b/dashboard/frontend/coverage/lcov-report/base.css deleted file mode 100644 index f418035..0000000 --- a/dashboard/frontend/coverage/lcov-report/base.css +++ /dev/null @@ -1,224 +0,0 @@ -body, html { - margin:0; padding: 0; - height: 100%; -} -body { - font-family: Helvetica Neue, Helvetica, Arial; - font-size: 14px; - color:#333; -} -.small { font-size: 12px; } -*, *:after, *:before { - -webkit-box-sizing:border-box; - -moz-box-sizing:border-box; - box-sizing:border-box; - } -h1 { font-size: 20px; margin: 0;} -h2 { font-size: 14px; } -pre { - font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace; - margin: 0; - padding: 0; - -moz-tab-size: 2; - -o-tab-size: 2; - tab-size: 2; -} -a { color:#0074D9; text-decoration:none; } -a:hover { text-decoration:underline; } -.strong { font-weight: bold; } -.space-top1 { padding: 10px 0 0 0; } -.pad2y { padding: 20px 0; } -.pad1y { padding: 10px 0; } -.pad2x { padding: 0 20px; } -.pad2 { padding: 20px; } -.pad1 { padding: 10px; } -.space-left2 { padding-left:55px; } -.space-right2 { padding-right:20px; } -.center { text-align:center; } -.clearfix { display:block; } -.clearfix:after { - content:''; - display:block; - height:0; - clear:both; - visibility:hidden; - } -.fl { float: left; } -@media only screen and (max-width:640px) { - .col3 { width:100%; max-width:100%; } - .hide-mobile { display:none!important; } -} - -.quiet { - color: #7f7f7f; - color: rgba(0,0,0,0.5); -} -.quiet a { opacity: 0.7; } - -.fraction { - font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace; - font-size: 10px; - color: #555; - background: #E8E8E8; - padding: 4px 5px; - border-radius: 3px; - vertical-align: middle; -} - -div.path a:link, div.path a:visited { color: #333; } -table.coverage { - border-collapse: collapse; - margin: 10px 0 0 0; - padding: 0; -} - -table.coverage td { - margin: 0; - padding: 0; - vertical-align: top; -} -table.coverage td.line-count { - text-align: right; - padding: 0 5px 0 20px; -} -table.coverage td.line-coverage { - text-align: right; - padding-right: 10px; - min-width:20px; -} - -table.coverage td span.cline-any { - display: inline-block; - padding: 0 5px; - width: 100%; -} -.missing-if-branch { - display: inline-block; - margin-right: 5px; - border-radius: 3px; - position: relative; - padding: 0 4px; - background: #333; - color: yellow; -} - -.skip-if-branch { - display: none; - margin-right: 10px; - position: relative; - padding: 0 4px; - background: #ccc; - color: white; -} -.missing-if-branch .typ, .skip-if-branch .typ { - color: inherit !important; -} -.coverage-summary { - border-collapse: collapse; - width: 100%; -} -.coverage-summary tr { border-bottom: 1px solid #bbb; } -.keyline-all { border: 1px solid #ddd; } -.coverage-summary td, .coverage-summary th { padding: 10px; } -.coverage-summary tbody { border: 1px solid #bbb; } -.coverage-summary td { border-right: 1px solid #bbb; } -.coverage-summary td:last-child { border-right: none; } -.coverage-summary th { - text-align: left; - font-weight: normal; - white-space: nowrap; -} -.coverage-summary th.file { border-right: none !important; } -.coverage-summary th.pct { } -.coverage-summary th.pic, -.coverage-summary th.abs, -.coverage-summary td.pct, -.coverage-summary td.abs { text-align: right; } -.coverage-summary td.file { white-space: nowrap; } -.coverage-summary td.pic { min-width: 120px !important; } -.coverage-summary tfoot td { } - -.coverage-summary .sorter { - height: 10px; - width: 7px; - display: inline-block; - margin-left: 0.5em; - background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent; -} -.coverage-summary .sorted .sorter { - background-position: 0 -20px; -} -.coverage-summary .sorted-desc .sorter { - background-position: 0 -10px; -} -.status-line { height: 10px; } -/* yellow */ -.cbranch-no { background: yellow !important; color: #111; } -/* dark red */ -.red.solid, .status-line.low, .low .cover-fill { background:#C21F39 } -.low .chart { border:1px solid #C21F39 } -.highlighted, -.highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{ - background: #C21F39 !important; -} -/* medium red */ -.cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE } -/* light red */ -.low, .cline-no { background:#FCE1E5 } -/* light green */ -.high, .cline-yes { background:rgb(230,245,208) } -/* medium green */ -.cstat-yes { background:rgb(161,215,106) } -/* dark green */ -.status-line.high, .high .cover-fill { background:rgb(77,146,33) } -.high .chart { border:1px solid rgb(77,146,33) } -/* dark yellow (gold) */ -.status-line.medium, .medium .cover-fill { background: #f9cd0b; } -.medium .chart { border:1px solid #f9cd0b; } -/* light yellow */ -.medium { background: #fff4c2; } - -.cstat-skip { background: #ddd; color: #111; } -.fstat-skip { background: #ddd; color: #111 !important; } -.cbranch-skip { background: #ddd !important; color: #111; } - -span.cline-neutral { background: #eaeaea; } - -.coverage-summary td.empty { - opacity: .5; - padding-top: 4px; - padding-bottom: 4px; - line-height: 1; - color: #888; -} - -.cover-fill, .cover-empty { - display:inline-block; - height: 12px; -} -.chart { - line-height: 0; -} -.cover-empty { - background: white; -} -.cover-full { - border-right: none !important; -} -pre.prettyprint { - border: none !important; - padding: 0 !important; - margin: 0 !important; -} -.com { color: #999 !important; } -.ignore-none { color: #999; font-weight: normal; } - -.wrapper { - min-height: 100%; - height: auto !important; - height: 100%; - margin: 0 auto -48px; -} -.footer, .push { - height: 48px; -} diff --git a/dashboard/frontend/coverage/lcov-report/block-navigation.js b/dashboard/frontend/coverage/lcov-report/block-navigation.js deleted file mode 100644 index 530d1ed..0000000 --- a/dashboard/frontend/coverage/lcov-report/block-navigation.js +++ /dev/null @@ -1,87 +0,0 @@ -/* eslint-disable */ -var jumpToCode = (function init() { - // Classes of code we would like to highlight in the file view - var missingCoverageClasses = ['.cbranch-no', '.cstat-no', '.fstat-no']; - - // Elements to highlight in the file listing view - var fileListingElements = ['td.pct.low']; - - // We don't want to select elements that are direct descendants of another match - var notSelector = ':not(' + missingCoverageClasses.join('):not(') + ') > '; // becomes `:not(a):not(b) > ` - - // Selector that finds elements on the page to which we can jump - var selector = - fileListingElements.join(', ') + - ', ' + - notSelector + - missingCoverageClasses.join(', ' + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b` - - // The NodeList of matching elements - var missingCoverageElements = document.querySelectorAll(selector); - - var currentIndex; - - function toggleClass(index) { - missingCoverageElements - .item(currentIndex) - .classList.remove('highlighted'); - missingCoverageElements.item(index).classList.add('highlighted'); - } - - function makeCurrent(index) { - toggleClass(index); - currentIndex = index; - missingCoverageElements.item(index).scrollIntoView({ - behavior: 'smooth', - block: 'center', - inline: 'center' - }); - } - - function goToPrevious() { - var nextIndex = 0; - if (typeof currentIndex !== 'number' || currentIndex === 0) { - nextIndex = missingCoverageElements.length - 1; - } else if (missingCoverageElements.length > 1) { - nextIndex = currentIndex - 1; - } - - makeCurrent(nextIndex); - } - - function goToNext() { - var nextIndex = 0; - - if ( - typeof currentIndex === 'number' && - currentIndex < missingCoverageElements.length - 1 - ) { - nextIndex = currentIndex + 1; - } - - makeCurrent(nextIndex); - } - - return function jump(event) { - if ( - document.getElementById('fileSearch') === document.activeElement && - document.activeElement != null - ) { - // if we're currently focused on the search input, we don't want to navigate - return; - } - - switch (event.which) { - case 78: // n - case 74: // j - goToNext(); - break; - case 66: // b - case 75: // k - case 80: // p - goToPrevious(); - break; - } - }; -})(); -window.addEventListener('keydown', jumpToCode); diff --git a/dashboard/frontend/coverage/lcov-report/favicon.png b/dashboard/frontend/coverage/lcov-report/favicon.png deleted file mode 100644 index c1525b8..0000000 Binary files a/dashboard/frontend/coverage/lcov-report/favicon.png and /dev/null differ diff --git a/dashboard/frontend/coverage/lcov-report/index.html b/dashboard/frontend/coverage/lcov-report/index.html deleted file mode 100644 index dff8ecd..0000000 --- a/dashboard/frontend/coverage/lcov-report/index.html +++ /dev/null @@ -1,176 +0,0 @@ - - - - - - Code coverage report for All files - - - - - - - - - -
-
-

All files

-
- -
- 4.5% - Statements - 172/3818 -
- - -
- 53.16% - Branches - 42/79 -
- - -
- 29.31% - Functions - 17/58 -
- - -
- 4.5% - Lines - 172/3818 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FileStatementsBranchesFunctionsLines
src -
-
0%0/1820%0/20%0/20%0/182
src/components -
-
0%0/3010%0/10%0/10%0/301
src/components/opc -
-
0%0/3930%0/50%0/50%0/393
src/lib -
-
22.06%124/56284.78%39/4651.72%15/2922.06%124/562
src/routes -
-
2.01%48/238012%3/259.52%2/212.01%48/2380
-
-
-
- - - - - - - - \ No newline at end of file diff --git a/dashboard/frontend/coverage/lcov-report/prettify.css b/dashboard/frontend/coverage/lcov-report/prettify.css deleted file mode 100644 index b317a7c..0000000 --- a/dashboard/frontend/coverage/lcov-report/prettify.css +++ /dev/null @@ -1 +0,0 @@ -.pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee} diff --git a/dashboard/frontend/coverage/lcov-report/prettify.js b/dashboard/frontend/coverage/lcov-report/prettify.js deleted file mode 100644 index b322523..0000000 --- a/dashboard/frontend/coverage/lcov-report/prettify.js +++ /dev/null @@ -1,2 +0,0 @@ -/* eslint-disable */ -window.PR_SHOULD_USE_CONTINUATION=true;(function(){var h=["break,continue,do,else,for,if,return,while"];var u=[h,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"];var p=[u,"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"];var l=[p,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"];var x=[p,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"];var R=[x,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"];var r="all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes";var w=[p,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"];var s="caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END";var I=[h,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"];var f=[h,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"];var H=[h,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"];var A=[l,R,w,s+I,f,H];var e=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/;var C="str";var z="kwd";var j="com";var O="typ";var G="lit";var L="pun";var F="pln";var m="tag";var E="dec";var J="src";var P="atn";var n="atv";var N="nocode";var M="(?:^^\\.?|[+-]|\\!|\\!=|\\!==|\\#|\\%|\\%=|&|&&|&&=|&=|\\(|\\*|\\*=|\\+=|\\,|\\-=|\\->|\\/|\\/=|:|::|\\;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|\\?|\\@|\\[|\\^|\\^=|\\^\\^|\\^\\^=|\\{|\\||\\|=|\\|\\||\\|\\|=|\\~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\\s*";function k(Z){var ad=0;var S=false;var ac=false;for(var V=0,U=Z.length;V122)){if(!(al<65||ag>90)){af.push([Math.max(65,ag)|32,Math.min(al,90)|32])}if(!(al<97||ag>122)){af.push([Math.max(97,ag)&~32,Math.min(al,122)&~32])}}}}af.sort(function(av,au){return(av[0]-au[0])||(au[1]-av[1])});var ai=[];var ap=[NaN,NaN];for(var ar=0;arat[0]){if(at[1]+1>at[0]){an.push("-")}an.push(T(at[1]))}}an.push("]");return an.join("")}function W(al){var aj=al.source.match(new RegExp("(?:\\[(?:[^\\x5C\\x5D]|\\\\[\\s\\S])*\\]|\\\\u[A-Fa-f0-9]{4}|\\\\x[A-Fa-f0-9]{2}|\\\\[0-9]+|\\\\[^ux0-9]|\\(\\?[:!=]|[\\(\\)\\^]|[^\\x5B\\x5C\\(\\)\\^]+)","g"));var ah=aj.length;var an=[];for(var ak=0,am=0;ak=2&&ai==="["){aj[ak]=X(ag)}else{if(ai!=="\\"){aj[ak]=ag.replace(/[a-zA-Z]/g,function(ao){var ap=ao.charCodeAt(0);return"["+String.fromCharCode(ap&~32,ap|32)+"]"})}}}}return aj.join("")}var aa=[];for(var V=0,U=Z.length;V=0;){S[ac.charAt(ae)]=Y}}var af=Y[1];var aa=""+af;if(!ag.hasOwnProperty(aa)){ah.push(af);ag[aa]=null}}ah.push(/[\0-\uffff]/);V=k(ah)})();var X=T.length;var W=function(ah){var Z=ah.sourceCode,Y=ah.basePos;var ad=[Y,F];var af=0;var an=Z.match(V)||[];var aj={};for(var ae=0,aq=an.length;ae=5&&"lang-"===ap.substring(0,5);if(am&&!(ai&&typeof ai[1]==="string")){am=false;ap=J}if(!am){aj[ag]=ap}}var ab=af;af+=ag.length;if(!am){ad.push(Y+ab,ap)}else{var al=ai[1];var ak=ag.indexOf(al);var ac=ak+al.length;if(ai[2]){ac=ag.length-ai[2].length;ak=ac-al.length}var ar=ap.substring(5);B(Y+ab,ag.substring(0,ak),W,ad);B(Y+ab+ak,al,q(ar,al),ad);B(Y+ab+ac,ag.substring(ac),W,ad)}}ah.decorations=ad};return W}function i(T){var W=[],S=[];if(T.tripleQuotedStrings){W.push([C,/^(?:\'\'\'(?:[^\'\\]|\\[\s\S]|\'{1,2}(?=[^\']))*(?:\'\'\'|$)|\"\"\"(?:[^\"\\]|\\[\s\S]|\"{1,2}(?=[^\"]))*(?:\"\"\"|$)|\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$))/,null,"'\""])}else{if(T.multiLineStrings){W.push([C,/^(?:\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$)|\`(?:[^\\\`]|\\[\s\S])*(?:\`|$))/,null,"'\"`"])}else{W.push([C,/^(?:\'(?:[^\\\'\r\n]|\\.)*(?:\'|$)|\"(?:[^\\\"\r\n]|\\.)*(?:\"|$))/,null,"\"'"])}}if(T.verbatimStrings){S.push([C,/^@\"(?:[^\"]|\"\")*(?:\"|$)/,null])}var Y=T.hashComments;if(Y){if(T.cStyleComments){if(Y>1){W.push([j,/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,null,"#"])}else{W.push([j,/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\r\n]*)/,null,"#"])}S.push([C,/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,null])}else{W.push([j,/^#[^\r\n]*/,null,"#"])}}if(T.cStyleComments){S.push([j,/^\/\/[^\r\n]*/,null]);S.push([j,/^\/\*[\s\S]*?(?:\*\/|$)/,null])}if(T.regexLiterals){var X=("/(?=[^/*])(?:[^/\\x5B\\x5C]|\\x5C[\\s\\S]|\\x5B(?:[^\\x5C\\x5D]|\\x5C[\\s\\S])*(?:\\x5D|$))+/");S.push(["lang-regex",new RegExp("^"+M+"("+X+")")])}var V=T.types;if(V){S.push([O,V])}var U=(""+T.keywords).replace(/^ | $/g,"");if(U.length){S.push([z,new RegExp("^(?:"+U.replace(/[\s,]+/g,"|")+")\\b"),null])}W.push([F,/^\s+/,null," \r\n\t\xA0"]);S.push([G,/^@[a-z_$][a-z_$@0-9]*/i,null],[O,/^(?:[@_]?[A-Z]+[a-z][A-Za-z_$@0-9]*|\w+_t\b)/,null],[F,/^[a-z_$][a-z_$@0-9]*/i,null],[G,new RegExp("^(?:0x[a-f0-9]+|(?:\\d(?:_\\d+)*\\d*(?:\\.\\d*)?|\\.\\d\\+)(?:e[+\\-]?\\d+)?)[a-z]*","i"),null,"0123456789"],[F,/^\\[\s\S]?/,null],[L,/^.[^\s\w\.$@\'\"\`\/\#\\]*/,null]);return g(W,S)}var K=i({keywords:A,hashComments:true,cStyleComments:true,multiLineStrings:true,regexLiterals:true});function Q(V,ag){var U=/(?:^|\s)nocode(?:\s|$)/;var ab=/\r\n?|\n/;var ac=V.ownerDocument;var S;if(V.currentStyle){S=V.currentStyle.whiteSpace}else{if(window.getComputedStyle){S=ac.defaultView.getComputedStyle(V,null).getPropertyValue("white-space")}}var Z=S&&"pre"===S.substring(0,3);var af=ac.createElement("LI");while(V.firstChild){af.appendChild(V.firstChild)}var W=[af];function ae(al){switch(al.nodeType){case 1:if(U.test(al.className)){break}if("BR"===al.nodeName){ad(al);if(al.parentNode){al.parentNode.removeChild(al)}}else{for(var an=al.firstChild;an;an=an.nextSibling){ae(an)}}break;case 3:case 4:if(Z){var am=al.nodeValue;var aj=am.match(ab);if(aj){var ai=am.substring(0,aj.index);al.nodeValue=ai;var ah=am.substring(aj.index+aj[0].length);if(ah){var ak=al.parentNode;ak.insertBefore(ac.createTextNode(ah),al.nextSibling)}ad(al);if(!ai){al.parentNode.removeChild(al)}}}break}}function ad(ak){while(!ak.nextSibling){ak=ak.parentNode;if(!ak){return}}function ai(al,ar){var aq=ar?al.cloneNode(false):al;var ao=al.parentNode;if(ao){var ap=ai(ao,1);var an=al.nextSibling;ap.appendChild(aq);for(var am=an;am;am=an){an=am.nextSibling;ap.appendChild(am)}}return aq}var ah=ai(ak.nextSibling,0);for(var aj;(aj=ah.parentNode)&&aj.nodeType===1;){ah=aj}W.push(ah)}for(var Y=0;Y=S){ah+=2}if(V>=ap){Z+=2}}}var t={};function c(U,V){for(var S=V.length;--S>=0;){var T=V[S];if(!t.hasOwnProperty(T)){t[T]=U}else{if(window.console){console.warn("cannot override language handler %s",T)}}}}function q(T,S){if(!(T&&t.hasOwnProperty(T))){T=/^\s*]*(?:>|$)/],[j,/^<\!--[\s\S]*?(?:-\->|$)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],[L,/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]),["default-markup","htm","html","mxml","xhtml","xml","xsl"]);c(g([[F,/^[\s]+/,null," \t\r\n"],[n,/^(?:\"[^\"]*\"?|\'[^\']*\'?)/,null,"\"'"]],[[m,/^^<\/?[a-z](?:[\w.:-]*\w)?|\/?>$/i],[P,/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^>\'\"\s]*(?:[^>\'\"\s\/]|\/(?=\s)))/],[L,/^[=<>\/]+/],["lang-js",/^on\w+\s*=\s*\"([^\"]+)\"/i],["lang-js",/^on\w+\s*=\s*\'([^\']+)\'/i],["lang-js",/^on\w+\s*=\s*([^\"\'>\s]+)/i],["lang-css",/^style\s*=\s*\"([^\"]+)\"/i],["lang-css",/^style\s*=\s*\'([^\']+)\'/i],["lang-css",/^style\s*=\s*([^\"\'>\s]+)/i]]),["in.tag"]);c(g([],[[n,/^[\s\S]+/]]),["uq.val"]);c(i({keywords:l,hashComments:true,cStyleComments:true,types:e}),["c","cc","cpp","cxx","cyc","m"]);c(i({keywords:"null,true,false"}),["json"]);c(i({keywords:R,hashComments:true,cStyleComments:true,verbatimStrings:true,types:e}),["cs"]);c(i({keywords:x,cStyleComments:true}),["java"]);c(i({keywords:H,hashComments:true,multiLineStrings:true}),["bsh","csh","sh"]);c(i({keywords:I,hashComments:true,multiLineStrings:true,tripleQuotedStrings:true}),["cv","py"]);c(i({keywords:s,hashComments:true,multiLineStrings:true,regexLiterals:true}),["perl","pl","pm"]);c(i({keywords:f,hashComments:true,multiLineStrings:true,regexLiterals:true}),["rb"]);c(i({keywords:w,cStyleComments:true,regexLiterals:true}),["js"]);c(i({keywords:r,hashComments:3,cStyleComments:true,multilineStrings:true,tripleQuotedStrings:true,regexLiterals:true}),["coffee"]);c(g([],[[C,/^[\s\S]+/]]),["regex"]);function d(V){var U=V.langExtension;try{var S=a(V.sourceNode);var T=S.sourceCode;V.sourceCode=T;V.spans=S.spans;V.basePos=0;q(U,T)(V);D(V)}catch(W){if("console" in window){console.log(W&&W.stack?W.stack:W)}}}function y(W,V,U){var S=document.createElement("PRE");S.innerHTML=W;if(U){Q(S,U)}var T={langExtension:V,numberLines:U,sourceNode:S};d(T);return S.innerHTML}function b(ad){function Y(af){return document.getElementsByTagName(af)}var ac=[Y("pre"),Y("code"),Y("xmp")];var T=[];for(var aa=0;aa=0){var ah=ai.match(ab);var am;if(!ah&&(am=o(aj))&&"CODE"===am.tagName){ah=am.className.match(ab)}if(ah){ah=ah[1]}var al=false;for(var ak=aj.parentNode;ak;ak=ak.parentNode){if((ak.tagName==="pre"||ak.tagName==="code"||ak.tagName==="xmp")&&ak.className&&ak.className.indexOf("prettyprint")>=0){al=true;break}}if(!al){var af=aj.className.match(/\blinenums\b(?::(\d+))?/);af=af?af[1]&&af[1].length?+af[1]:true:false;if(af){Q(aj,af)}S={langExtension:ah,sourceNode:aj,numberLines:af};d(S)}}}if(X]*(?:>|$)/],[PR.PR_COMMENT,/^<\!--[\s\S]*?(?:-\->|$)/],[PR.PR_PUNCTUATION,/^(?:<[%?]|[%?]>)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-handlebars",/^]*type\s*=\s*['"]?text\/x-handlebars-template['"]?\b[^>]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i],[PR.PR_DECLARATION,/^{{[#^>/]?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{&?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{{>?\s*[\w.][^}]*}}}/],[PR.PR_COMMENT,/^{{![^}]*}}/]]),["handlebars","hbs"]);PR.registerLangHandler(PR.createSimpleLexer([[PR.PR_PLAIN,/^[ \t\r\n\f]+/,null," \t\r\n\f"]],[[PR.PR_STRING,/^\"(?:[^\n\r\f\\\"]|\\(?:\r\n?|\n|\f)|\\[\s\S])*\"/,null],[PR.PR_STRING,/^\'(?:[^\n\r\f\\\']|\\(?:\r\n?|\n|\f)|\\[\s\S])*\'/,null],["lang-css-str",/^url\(([^\)\"\']*)\)/i],[PR.PR_KEYWORD,/^(?:url|rgb|\!important|@import|@page|@media|@charset|inherit)(?=[^\-\w]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|(?:\\[0-9a-f]+ ?))(?:[_a-z0-9\-]|\\(?:\\[0-9a-f]+ ?))*)\s*:/i],[PR.PR_COMMENT,/^\/\*[^*]*\*+(?:[^\/*][^*]*\*+)*\//],[PR.PR_COMMENT,/^(?:)/],[PR.PR_LITERAL,/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],[PR.PR_LITERAL,/^#(?:[0-9a-f]{3}){1,2}/i],[PR.PR_PLAIN,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i],[PR.PR_PUNCTUATION,/^[^\s\w\'\"]+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_KEYWORD,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_STRING,/^[^\)\"\']+/]]),["css-str"]); diff --git a/dashboard/frontend/coverage/lcov-report/sort-arrow-sprite.png b/dashboard/frontend/coverage/lcov-report/sort-arrow-sprite.png deleted file mode 100644 index 6ed6831..0000000 Binary files a/dashboard/frontend/coverage/lcov-report/sort-arrow-sprite.png and /dev/null differ diff --git a/dashboard/frontend/coverage/lcov-report/sorter.js b/dashboard/frontend/coverage/lcov-report/sorter.js deleted file mode 100644 index 4ed70ae..0000000 --- a/dashboard/frontend/coverage/lcov-report/sorter.js +++ /dev/null @@ -1,210 +0,0 @@ -/* eslint-disable */ -var addSorting = (function() { - 'use strict'; - var cols, - currentSort = { - index: 0, - desc: false - }; - - // returns the summary table element - function getTable() { - return document.querySelector('.coverage-summary'); - } - // returns the thead element of the summary table - function getTableHeader() { - return getTable().querySelector('thead tr'); - } - // returns the tbody element of the summary table - function getTableBody() { - return getTable().querySelector('tbody'); - } - // returns the th element for nth column - function getNthColumn(n) { - return getTableHeader().querySelectorAll('th')[n]; - } - - function onFilterInput() { - const searchValue = document.getElementById('fileSearch').value; - const rows = document.getElementsByTagName('tbody')[0].children; - - // Try to create a RegExp from the searchValue. If it fails (invalid regex), - // it will be treated as a plain text search - let searchRegex; - try { - searchRegex = new RegExp(searchValue, 'i'); // 'i' for case-insensitive - } catch (error) { - searchRegex = null; - } - - for (let i = 0; i < rows.length; i++) { - const row = rows[i]; - let isMatch = false; - - if (searchRegex) { - // If a valid regex was created, use it for matching - isMatch = searchRegex.test(row.textContent); - } else { - // Otherwise, fall back to the original plain text search - isMatch = row.textContent - .toLowerCase() - .includes(searchValue.toLowerCase()); - } - - row.style.display = isMatch ? '' : 'none'; - } - } - - // loads the search box - function addSearchBox() { - var template = document.getElementById('filterTemplate'); - var templateClone = template.content.cloneNode(true); - templateClone.getElementById('fileSearch').oninput = onFilterInput; - template.parentElement.appendChild(templateClone); - } - - // loads all columns - function loadColumns() { - var colNodes = getTableHeader().querySelectorAll('th'), - colNode, - cols = [], - col, - i; - - for (i = 0; i < colNodes.length; i += 1) { - colNode = colNodes[i]; - col = { - key: colNode.getAttribute('data-col'), - sortable: !colNode.getAttribute('data-nosort'), - type: colNode.getAttribute('data-type') || 'string' - }; - cols.push(col); - if (col.sortable) { - col.defaultDescSort = col.type === 'number'; - colNode.innerHTML = - colNode.innerHTML + ''; - } - } - return cols; - } - // attaches a data attribute to every tr element with an object - // of data values keyed by column name - function loadRowData(tableRow) { - var tableCols = tableRow.querySelectorAll('td'), - colNode, - col, - data = {}, - i, - val; - for (i = 0; i < tableCols.length; i += 1) { - colNode = tableCols[i]; - col = cols[i]; - val = colNode.getAttribute('data-value'); - if (col.type === 'number') { - val = Number(val); - } - data[col.key] = val; - } - return data; - } - // loads all row data - function loadData() { - var rows = getTableBody().querySelectorAll('tr'), - i; - - for (i = 0; i < rows.length; i += 1) { - rows[i].data = loadRowData(rows[i]); - } - } - // sorts the table using the data for the ith column - function sortByIndex(index, desc) { - var key = cols[index].key, - sorter = function(a, b) { - a = a.data[key]; - b = b.data[key]; - return a < b ? -1 : a > b ? 1 : 0; - }, - finalSorter = sorter, - tableBody = document.querySelector('.coverage-summary tbody'), - rowNodes = tableBody.querySelectorAll('tr'), - rows = [], - i; - - if (desc) { - finalSorter = function(a, b) { - return -1 * sorter(a, b); - }; - } - - for (i = 0; i < rowNodes.length; i += 1) { - rows.push(rowNodes[i]); - tableBody.removeChild(rowNodes[i]); - } - - rows.sort(finalSorter); - - for (i = 0; i < rows.length; i += 1) { - tableBody.appendChild(rows[i]); - } - } - // removes sort indicators for current column being sorted - function removeSortIndicators() { - var col = getNthColumn(currentSort.index), - cls = col.className; - - cls = cls.replace(/ sorted$/, '').replace(/ sorted-desc$/, ''); - col.className = cls; - } - // adds sort indicators for current column being sorted - function addSortIndicators() { - getNthColumn(currentSort.index).className += currentSort.desc - ? ' sorted-desc' - : ' sorted'; - } - // adds event listeners for all sorter widgets - function enableUI() { - var i, - el, - ithSorter = function ithSorter(i) { - var col = cols[i]; - - return function() { - var desc = col.defaultDescSort; - - if (currentSort.index === i) { - desc = !currentSort.desc; - } - sortByIndex(i, desc); - removeSortIndicators(); - currentSort.index = i; - currentSort.desc = desc; - addSortIndicators(); - }; - }; - for (i = 0; i < cols.length; i += 1) { - if (cols[i].sortable) { - // add the click event handler on the th so users - // dont have to click on those tiny arrows - el = getNthColumn(i).querySelector('.sorter').parentElement; - if (el.addEventListener) { - el.addEventListener('click', ithSorter(i)); - } else { - el.attachEvent('onclick', ithSorter(i)); - } - } - } - } - // adds sorting functionality to the UI - return function() { - if (!getTable()) { - return; - } - cols = loadColumns(); - loadData(); - addSearchBox(); - addSortIndicators(); - enableUI(); - }; -})(); - -window.addEventListener('load', addSorting); diff --git a/dashboard/frontend/coverage/lcov-report/src/App.svelte.html b/dashboard/frontend/coverage/lcov-report/src/App.svelte.html deleted file mode 100644 index 1992d98..0000000 --- a/dashboard/frontend/coverage/lcov-report/src/App.svelte.html +++ /dev/null @@ -1,787 +0,0 @@ - - - - - - Code coverage report for src/App.svelte - - - - - - - - - -
-
-

All files / src App.svelte

-
- -
- 0% - Statements - 0/177 -
- - -
- 0% - Branches - 0/1 -
- - -
- 0% - Functions - 0/1 -
- - -
- 0% - Lines - 0/177 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -178 -179 -180 -181 -182 -183 -184 -185 -186 -187 -188 -189 -190 -191 -192 -193 -194 -195 -196 -197 -198 -199 -200 -201 -202 -203 -204 -205 -206 -207 -208 -209 -210 -211 -212 -213 -214 -215 -216 -217 -218 -219 -220 -221 -222 -223 -224 -225 -226 -227 -228 -229 -230 -231 -232 -233 -234 -235  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  - 
<script>
-  import Sidebar from "./components/Sidebar.svelte";
-  import Dashboard from "./routes/Dashboard.svelte";
-  import Docker from "./routes/Docker.svelte";
-  import Gitea from "./routes/Gitea.svelte";
-  import Files from "./routes/Files.svelte";
-  import Terminal from "./routes/Terminal.svelte";
-  import OpenClaw from "./routes/OpenClaw.svelte";
-  import Settings from "./routes/Settings.svelte";
-  import ChatSummary from "./routes/ChatSummary.svelte";
-  import Conversations from "./routes/Conversations.svelte";
-  import Security from "./routes/Security.svelte";
-  import LiteLLM from "./routes/LiteLLM.svelte";
-  import CcConnect from "./routes/CcConnect.svelte";
-  import InfoEngine from "./routes/InfoEngine.svelte";
-  import OPC from "./routes/OPC.svelte";
-  import Transmission from "./routes/Transmission.svelte";
-  import Login from "./routes/Login.svelte";
-  import { onMount } from "svelte";
-  import { getToken, checkAuth, setToken, currentUser, setCurrentUser, getPreferences, savePreferences, tryRefreshSession, logout as logoutSession } from "./lib/api.js";
- 
-  const KNOWN_PAGES = new Set([
-    "dashboard",
-    "docker",
-    "gitea",
-    "files",
-    "terminal",
-    "openclaw",
-    "chat-digest",
-    "conversations",
-    "settings",
-    "security",
-    "litellm",
-    "cc-connect",
-    "info-engine",
-    "opc",
-    "transmission",
-  ]);
- 
-  let page = $state("dashboard");
-  let dark = $state(false);
-  let sidebarOpen = $state(false);
-  let authorized = $state(false);
-  let loading = $state(true);
-  let userRole = $state("");
-  let allowedPages = $state([]);
-  let allowedSidebarLinks = $state("*");
-  let sidebarOrder = $state(null);
-  let orderSaveTimer = null;
- 
-  function getRequestedPage() {
-    const requestedPage = new URLSearchParams(window.location.search).get("page");
-    return KNOWN_PAGES.has(requestedPage) ? requestedPage : null;
-  }
- 
-  function canOpenPage(pageId) {
-    if (!pageId || !KNOWN_PAGES.has(pageId)) return false;
-    if (pageId === "settings") return userRole === "admin";
-    if (["litellm", "cc-connect", "info-engine"].includes(pageId)) return hasPageAccess("dashboard");
-    return hasPageAccess(pageId);
-  }
- 
-  function syncPageQuery(pageId) {
-    const url = new URL(window.location.href);
-    if (pageId === "dashboard") url.searchParams.delete("page");
-    else url.searchParams.set("page", pageId);
-    if (pageId !== "terminal") url.searchParams.delete("host");
-    history.replaceState({}, "", `${url.pathname}${url.search}${url.hash}`);
-  }
- 
-  async function fetchUserInfo() {
-    try {
-      const data = await checkAuth();
-      setCurrentUser(data);
-      userRole = data.role || "admin";
-      allowedPages = data.pages || "*";
-      allowedSidebarLinks = data.sidebar_links || "*";
-      // Fetch sidebar preferences
-      try {
-        const prefs = await getPreferences();
-        sidebarOrder = prefs.sidebar_order || null;
-      } catch {}
-    } catch (e) {
-      console.error("Failed to fetch user info:", e);
-    }
-  }
- 
-  function handleOrderChange(newOrder) {
-    sidebarOrder = newOrder;
-    clearTimeout(orderSaveTimer);
-    orderSaveTimer = setTimeout(() => {
-      savePreferences({ sidebar_order: newOrder }).catch((e) => {
-        console.error("Failed to save sidebar order:", e);
-      });
-    }, 500);
-  }
- 
-  function hasPageAccess(pageId) {
-    if (allowedPages === "*") return true;
-    return Array.isArray(allowedPages) && allowedPages.includes(pageId);
-  }
- 
-  onMount(async () => {
-    // Theme init
-    if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
-      dark = true;
-      document.documentElement.classList.add('dark');
-    } else {
-      dark = false;
-      document.documentElement.classList.remove('dark');
-    }
- 
-    // Try proxy auth first (Authelia forward-auth sets Remote-User header)
-    try {
-      const proxyRes = await fetch("/api/auth/proxy", { credentials: "same-origin" });
-      if (proxyRes.ok) {
-        const data = await proxyRes.json();
-        setToken(data.access_token);
-        authorized = true;
-        await fetchUserInfo();
-        const requestedPage = getRequestedPage();
-        if (canOpenPage(requestedPage)) page = requestedPage;
-        loading = false;
-        return;
-      }
-    } catch (e) {
-      // Proxy auth not available, fall through to regular auth
-    }
- 
-    // Regular token auth check — always try refresh to get fresh cookies
-    const refreshed = await tryRefreshSession();
-    if (!refreshed && !getToken()) {
-      loading = false;
-      authorized = false;
-      return;
-    }
- 
-    try {
-        await fetchUserInfo();
-        authorized = true;
-        const requestedPage = getRequestedPage();
-        if (canOpenPage(requestedPage)) page = requestedPage;
-    } catch (e) {
-        console.error("Auth check failed:", e);
-        setToken("");
-        authorized = false;
-    } finally {
-        loading = false;
-    }
-  });
- 
-  $effect(() => {
-    if (!authorized || loading || !canOpenPage(page)) return;
-    syncPageQuery(page);
-  });
- 
-  function toggleTheme() {
-    dark = !dark;
-    if (dark) {
-       document.documentElement.classList.add("dark");
-       localStorage.theme = 'dark';
-    } else {
-       document.documentElement.classList.remove("dark");
-       localStorage.theme = 'light';
-    }
-  }
- 
-  async function logout() {
-      try {
-        await logoutSession();
-      } catch (e) {
-        console.error("Logout failed:", e);
-      }
-      setToken("");
-      window.location.reload();
-  }
-</script>
- 
-{#if loading}
-  <div class="flex h-screen items-center justify-center bg-surface-50 dark:bg-surface-950">
-    <div class="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600"></div>
-  </div>
-{:else if !authorized}
-  <Login />
-{:else}
-  <div class="flex h-screen overflow-hidden bg-surface-50 dark:bg-surface-900">
-    <Sidebar bind:page {dark} {toggleTheme} {logout} bind:sidebarOpen {allowedPages} {allowedSidebarLinks} {userRole} {sidebarOrder} onOrderChange={handleOrderChange} />
-    <main class="flex-1 overflow-auto">
-      <div class="max-w-[1400px] mx-auto p-6 lg:p-8">
-        <!-- Mobile hamburger -->
-        <button class="md:hidden mb-4 p-2 rounded-lg text-surface-600 dark:text-surface-300 hover:bg-surface-200 dark:hover:bg-surface-700" onclick={() => sidebarOpen = true} aria-label="Open menu">
-          <svg class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M4 6h16M4 12h16M4 18h16" /></svg>
-        </button>
-        {#if page === "dashboard" && hasPageAccess("dashboard")}
-          <Dashboard />
-        {:else if page === "docker" && hasPageAccess("docker")}
-          <Docker />
-        {:else if page === "gitea" && hasPageAccess("gitea")}
-          <Gitea />
-        {:else if page === "files" && hasPageAccess("files")}
-          <Files />
-        {:else if page === "openclaw" && hasPageAccess("openclaw")}
-          <OpenClaw />
-        {:else if page === "chat-digest" && hasPageAccess("chat-digest")}
-          <ChatSummary />
-        {:else if page === "conversations" && hasPageAccess("conversations")}
-          <Conversations />
-        {:else if page === "settings" && userRole === "admin"}
-          <Settings />
-        {:else if page === "security" && hasPageAccess("security")}
-          <Security />
-        {:else if page === "litellm" && hasPageAccess("dashboard")}
-          <LiteLLM />
-        {:else if page === "cc-connect" && hasPageAccess("dashboard")}
-          <CcConnect />
-        {:else if page === "info-engine" && hasPageAccess("dashboard")}
-          <InfoEngine />
-        {:else if page === "opc" && hasPageAccess("opc")}
-          <OPC />
-        {:else if page === "transmission" && hasPageAccess("transmission")}
-          <Transmission />
-        {:else if page !== "terminal"}
-          <Dashboard />
-        {/if}
- 
-        {#if hasPageAccess("terminal")}
-          <div class:hidden={page !== "terminal"}>
-            <Terminal />
-          </div>
-        {/if}
-      </div>
-    </main>
-  </div>
-{/if}
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/dashboard/frontend/coverage/lcov-report/src/components/Sidebar.svelte.html b/dashboard/frontend/coverage/lcov-report/src/components/Sidebar.svelte.html deleted file mode 100644 index a57a7a0..0000000 --- a/dashboard/frontend/coverage/lcov-report/src/components/Sidebar.svelte.html +++ /dev/null @@ -1,1339 +0,0 @@ - - - - - - Code coverage report for src/components/Sidebar.svelte - - - - - - - - - -
-
-

All files / src/components Sidebar.svelte

-
- -
- 0% - Statements - 0/301 -
- - -
- 0% - Branches - 0/1 -
- - -
- 0% - Functions - 0/1 -
- - -
- 0% - Lines - 0/301 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -178 -179 -180 -181 -182 -183 -184 -185 -186 -187 -188 -189 -190 -191 -192 -193 -194 -195 -196 -197 -198 -199 -200 -201 -202 -203 -204 -205 -206 -207 -208 -209 -210 -211 -212 -213 -214 -215 -216 -217 -218 -219 -220 -221 -222 -223 -224 -225 -226 -227 -228 -229 -230 -231 -232 -233 -234 -235 -236 -237 -238 -239 -240 -241 -242 -243 -244 -245 -246 -247 -248 -249 -250 -251 -252 -253 -254 -255 -256 -257 -258 -259 -260 -261 -262 -263 -264 -265 -266 -267 -268 -269 -270 -271 -272 -273 -274 -275 -276 -277 -278 -279 -280 -281 -282 -283 -284 -285 -286 -287 -288 -289 -290 -291 -292 -293 -294 -295 -296 -297 -298 -299 -300 -301 -302 -303 -304 -305 -306 -307 -308 -309 -310 -311 -312 -313 -314 -315 -316 -317 -318 -319 -320 -321 -322 -323 -324 -325 -326 -327 -328 -329 -330 -331 -332 -333 -334 -335 -336 -337 -338 -339 -340 -341 -342 -343 -344 -345 -346 -347 -348 -349 -350 -351 -352 -353 -354 -355 -356 -357 -358 -359 -360 -361 -362 -363 -364 -365 -366 -367 -368 -369 -370 -371 -372 -373 -374 -375 -376 -377 -378 -379 -380 -381 -382 -383 -384 -385 -386 -387 -388 -389 -390 -391 -392 -393 -394 -395 -396 -397 -398 -399 -400 -401 -402 -403 -404 -405 -406 -407 -408 -409 -410 -411 -412 -413 -414 -415 -416 -417 -418 -419  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  - 
<script>
-  let { page = $bindable("dashboard"), dark = false, toggleTheme = () => {}, logout = () => {}, sidebarOpen = $bindable(false), allowedPages = "*", allowedSidebarLinks = "*", userRole = "admin", sidebarOrder = null, onOrderChange = () => {} } = $props();
- 
-  function canAccess(id) {
-    if (!id) return true;
-    if (allowedPages === "*") return true;
-    if (!Array.isArray(allowedPages)) return false;
-    if (id === "info-engine") return allowedPages.includes("dashboard");
-    return allowedPages.includes(id);
-  }
- 
-  function canSeeSidebarLink(item) {
-    if (allowedSidebarLinks === "*") return true;
-    // Check by label or id
-    const key = item.label || item.id;
-    return Array.isArray(allowedSidebarLinks) && allowedSidebarLinks.includes(key);
-  }
- 
-  const defaultLinks = [
-    { id: "dashboard", label: "Overview", icon: "grid" },
-    { id: "info-engine", label: "Info Engine", icon: "sparkles" },
-    { id: "litellm", label: "LiteLLM", icon: "bolt" },
-    { id: "opc", label: "OPC", icon: "kanban" },
-    { id: "docker", label: "Docker", icon: "box" },
-    { id: "files", label: "Files", icon: "folder" },
-    { id: "terminal", label: "Terminal", icon: "terminal" },
-    { id: "security", label: "Security", icon: "shield" },
-  ];
- 
-  let lanIp = $state("192.168.31.222");
-  let tsIp = $state("100.78.131.124");
-  let lanReachable = $state(false);
-  let serviceUrls = $state({});
- 
-  if (typeof window !== "undefined") {
-    fetch("/api/client-ip").then(r => r.json()).then(d => {
-      if (d.lan) lanReachable = true;
-    }).catch(() => {});
- 
-    // Fetch external service URLs from backend config
-    fetch("/api/system/external-services").then(r => r.json()).then(d => {
-      if (d.lan_ip) lanIp = d.lan_ip;
-      if (d.ts_ip) tsIp = d.ts_ip;
-      if (d.services) serviceUrls = d.services;
-    }).catch(() => {});
-  }
- 
-  const defaultMedia = [
-    { label: "Navidrome", remoteHref: "https://music.jimmygan.com:8443", icon: "music" },
-    { label: "Jellyfin", remoteHref: "https://media.jimmygan.com:8443", icon: "play" },
-    { label: "Audiobookshelf", remoteHref: "https://books.jimmygan.com:8443", icon: "headphones" },
-    { label: "Immich", remoteHref: "https://photos.jimmygan.com:8443", icon: "image" },
-    { id: "transmission", label: "Transmission", icon: "download" },
-  ];
- 
-  const defaultTools = [
-    { id: "openclaw", label: "OpenClaw", icon: "openclaw" },
-    { id: "cc-connect", label: "cc-connect", icon: "users" },
-    { id: "chat-digest", label: "Chat Digest", icon: "chat" },
-    { id: "conversations", label: "Code Sessions", icon: "code" },
-    { id: "gitea", label: "Repos", icon: "git" },
-    { label: "Gitea Web", remoteHref: "https://git.jimmygan.com:8443", icon: "git", external: true },
-    { label: "Stirling PDF", remoteHref: "https://pdf.jimmygan.com:8443", icon: "pdf", external: true },
-    { label: "Vaultwarden", remoteHref: "https://vault.jimmygan.com:8443", icon: "lock", external: true },
-    { label: "n8n", remoteHref: "https://n8n.jimmygan.com:8443", icon: "n8n", external: true },
-    { label: "Speedtest", remoteHref: "https://speed.jimmygan.com:8443", icon: "speedtest", external: true },
-  ];
- 
-  const defaultTools2 = [];
- 
-  function itemKey(item) { return item.id || item.label; }
- 
-  // All items indexed by key for cross-category lookup
-  const allItems = new Map([...defaultLinks, ...defaultMedia, ...defaultTools, ...defaultTools2].map(i => [itemKey(i), i]));
- 
-  // Build category lists from saved order, supporting cross-category moves
-  const categories = ["main", "media", "tools", "tools2"];
-  const categoryDefaults = { main: defaultLinks, media: defaultMedia, tools: defaultTools, tools2: defaultTools2 };
- 
-  function buildLists(savedOrder) {
-    if (!savedOrder) return { main: [...defaultLinks], media: [...defaultMedia], tools: [...defaultTools], tools2: [...defaultTools2] };
- 
-    const used = new Set();
-    const result = { main: [], media: [], tools: [], tools2: [] };
- 
-    for (const cat of categories) {
-      const keys = savedOrder[cat] || [];
-      for (const k of keys) {
-        if (allItems.has(k) && !used.has(k)) {
-          result[cat].push(allItems.get(k));
-          used.add(k);
-        }
-      }
-    }
- 
-    // Append any new items not in saved order to their default category
-    for (const cat of categories) {
-      for (const item of categoryDefaults[cat]) {
-        if (!used.has(itemKey(item))) {
-          result[cat].push(item);
-          used.add(itemKey(item));
-        }
-      }
-    }
- 
-    return result;
-  }
- 
-  let categoryLists = $derived(buildLists(sidebarOrder));
-  let links = $derived(categoryLists.main);
-  let media = $derived(categoryLists.media);
-  let tools = $derived(categoryLists.tools);
-  let tools2 = $derived(categoryLists.tools2);
- 
-  // Collapsed state for categories (persisted in sidebarOrder)
-  let collapsed = $state(sidebarOrder?.collapsed ? { ...sidebarOrder.collapsed } : {});
- 
-  // Drag-and-drop state
-  let dragCategory = $state(null);
-  let dragIndex = $state(-1);
-  let dropCategory = $state(null);
-  let dropIndex = $state(-1);
- 
-  function handleDragStart(category, index, e) {
-    dragCategory = category;
-    dragIndex = index;
-    e.dataTransfer.effectAllowed = "move";
-    e.dataTransfer.setData("text/plain", "");
-  }
- 
-  function handleDragOver(category, index, e) {
-    e.preventDefault();
-    e.dataTransfer.dropEffect = "move";
-    dropCategory = category;
-    dropIndex = index;
-  }
- 
-  function handleDragEnd() {
-    dragCategory = null;
-    dragIndex = -1;
-    dropCategory = null;
-    dropIndex = -1;
-  }
- 
-  function handleDrop(category, index, e) {
-    e.preventDefault();
-    if (dragCategory === null) { handleDragEnd(); return; }
-    if (dragCategory === category && dragIndex === index) { handleDragEnd(); return; }
-    const lists = { main: [...links], media: [...media], tools: [...tools], tools2: [...tools2] };
-    const [moved] = lists[dragCategory].splice(dragIndex, 1);
-    lists[category].splice(index, 0, moved);
-    const newOrder = {
-      main: lists.main.map(itemKey),
-      media: lists.media.map(itemKey),
-      tools: lists.tools.map(itemKey),
-      tools2: lists.tools2.map(itemKey),
-      collapsed,
-    };
-    onOrderChange(newOrder);
-    handleDragEnd();
-  }
- 
-  // Allow dropping at end of a category (on the section divider/header area)
-  function handleCategoryDragOver(category, e) {
-    e.preventDefault();
-    e.dataTransfer.dropEffect = "move";
-    dropCategory = category;
-    dropIndex = -1;
-  }
- 
-  function handleCategoryDrop(category, e) {
-    e.preventDefault();
-    if (dragCategory === null) { handleDragEnd(); return; }
-    const lists = { main: [...links], media: [...media], tools: [...tools], tools2: [...tools2] };
-    const [moved] = lists[dragCategory].splice(dragIndex, 1);
-    lists[category].push(moved);
-    const newOrder = {
-      main: lists.main.map(itemKey),
-      media: lists.media.map(itemKey),
-      tools: lists.tools.map(itemKey),
-      tools2: lists.tools2.map(itemKey),
-      collapsed,
-    };
-    onOrderChange(newOrder);
-    handleDragEnd();
-  }
- 
-  function toPublicHttpsUrl(href) {
-    if (typeof window === "undefined") return href;
-    if (!href) return href;
- 
-    const isStandardHttps = location.protocol === "https:" && (!location.port || location.port === "443");
-    if (!isStandardHttps) return href;
- 
-    try {
-      const url = new URL(href);
-      if (url.protocol === "https:" && url.port === "8443") {
-        url.port = "";
-        return url.toString();
-      }
-      return href;
-    } catch {
-      return href;
-    }
-  }
- 
-  function extHref(svc) {
-    // Look up service URL from fetched config by label (lowercased, underscored)
-    const key = svc.label ? svc.label.toLowerCase().replace(/\s+/g, "_") : "";
-    const configUrl = serviceUrls[key];
-    const remoteHref = configUrl || svc.remoteHref;
- 
-    if (lanReachable && svc.port) return `http://${lanIp}:${svc.port}`;
-    if (remoteHref) return lanReachable ? remoteHref : toPublicHttpsUrl(remoteHref);
-    if (svc.port) {
-      const host = /^\d+\.\d+\.\d+\.\d+$/.test(location.hostname) ? location.hostname : tsIp;
-      return `http://${host}:${svc.port}`;
-    }
-    return svc.href;
-  }
- 
-  function navigate(id) {
-    page = id;
-    sidebarOpen = false;
-  }
- 
-  function dragClasses(category, index) {
-    if (dragCategory === category && index === dragIndex) return "opacity-30";
-    if (dropCategory === category && index === dropIndex) return "border-t-2 border-primary-500";
-    return "";
-  }
- 
-  function toggleCollapse(cat) {
-    collapsed[cat] = !collapsed[cat];
-    const newOrder = {
-      main: links.map(itemKey),
-      media: media.map(itemKey),
-      tools: tools.map(itemKey),
-      tools2: tools2.map(itemKey),
-      collapsed,
-    };
-    onOrderChange(newOrder);
-  }
-</script>
- 
-{#snippet iconSvg(icon)}
-  <span class="w-5 h-5 flex items-center justify-center">
-    {#if icon === "grid"}
-      <svg class="w-[18px] h-[18px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zm10 0a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zm10 0a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" /></svg>
-    {:else if icon === "sparkles"}
-      <svg class="w-[18px] h-[18px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M12 3l1.8 4.2L18 9l-4.2 1.8L12 15l-1.8-4.2L6 9l4.2-1.8L12 3zm6 11l.9 2.1L21 17l-2.1.9L18 20l-.9-2.1L15 17l2.1-.9L18 14zM5 14l.9 2.1L8 17l-2.1.9L5 20l-.9-2.1L2 17l2.1-.9L5 14z" /></svg>
-    {:else if icon === "box"}
-      <svg class="w-[18px] h-[18px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" /></svg>
-    {:else if icon === "bolt"}
-      <svg class="w-[18px] h-[18px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M13 3L4 14h6l-1 7 9-11h-6l1-7z" /></svg>
-    {:else if icon === "folder"}
-      <svg class="w-[18px] h-[18px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" /></svg>
-    {:else if icon === "terminal"}
-      <svg class="w-[18px] h-[18px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" /></svg>
-    {:else if icon === "shield"}
-      <svg class="w-[18px] h-[18px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" /></svg>
-    {:else if icon === "kanban"}
-      <svg class="w-[18px] h-[18px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2zm8 0h-2a2 2 0 00-2 2v8a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2z" /></svg>
-    {:else if icon === "music"}
-      <svg class="w-[16px] h-[16px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M9 19V6l12-3v13M9 19c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zm12-3c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2z" /></svg>
-    {:else if icon === "play"}
-      <svg class="w-[16px] h-[16px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" /><path stroke-linecap="round" stroke-linejoin="round" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
-    {:else if icon === "headphones"}
-      <svg class="w-[16px] h-[16px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M3 18v-6a9 9 0 0118 0v6M3 18a3 3 0 003 3h0a3 3 0 003-3v-2a3 3 0 00-3-3h0a3 3 0 00-3 3v2zm18 0a3 3 0 01-3 3h0a3 3 0 01-3-3v-2a3 3 0 013-3h0a3 3 0 013 3v2z" /></svg>
-    {:else if icon === "image"}
-      <svg class="w-[16px] h-[16px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" /></svg>
-    {:else if icon === "download"}
-      <svg class="w-[16px] h-[16px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M9 19l3 3m0 0l3-3m-3 3V10" /></svg>
-    {:else if icon === "openclaw"}
-      <svg class="w-[16px] h-[16px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z" /></svg>
-    {:else if icon === "chat"}
-      <svg class="w-[16px] h-[16px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M17 8h2a2 2 0 012 2v6a2 2 0 01-2 2h-2v4l-4-4H9a2 2 0 01-2-2v-1m0-3V6a2 2 0 012-2h8a2 2 0 012 2v3a2 2 0 01-2 2H9l-4 4V9z" /></svg>
-    {:else if icon === "code"}
-      <svg class="w-[16px] h-[16px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" /></svg>
-    {:else if icon === "git"}
-      <svg class="w-[16px] h-[16px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" /></svg>
-    {:else if icon === "pdf"}
-      <svg class="w-[16px] h-[16px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" /></svg>
-    {:else if icon === "lock"}
-      <svg class="w-[16px] h-[16px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" /></svg>
-    {:else if icon === "speedtest"}
-      <svg class="w-[16px] h-[16px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M13 10V3L4 14h7v7l9-11h-7z" /></svg>
-    {:else if icon === "users"}
-      <svg class="w-[16px] h-[16px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" /></svg>
-    {:else if icon === "n8n"}
-      <svg class="w-[16px] h-[16px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M4 6h16M4 12h16M4 18h16" /><circle cx="8" cy="6" r="1.5" fill="currentColor"/><circle cx="16" cy="12" r="1.5" fill="currentColor"/><circle cx="10" cy="18" r="1.5" fill="currentColor"/></svg>
-    {/if}
-  </span>
-{/snippet}
- 
-<!-- Mobile overlay -->
-{#if sidebarOpen}
-  <button class="fixed inset-0 bg-black/40 z-40 md:hidden" onclick={() => sidebarOpen = false} aria-label="Close sidebar"></button>
-{/if}
- 
-<aside class="fixed inset-y-0 left-0 z-50 w-[240px] h-screen flex flex-col shrink-0 border-r border-surface-200 dark:border-surface-700 bg-white dark:bg-surface-800 transition-transform duration-200 md:static md:translate-x-0 {sidebarOpen ? 'translate-x-0' : '-translate-x-full'}">
-  <!-- Logo -->
-  <div class="px-5 py-5 flex items-center gap-3">
-    <div class="w-8 h-8 rounded-lg bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center shadow-md">
-      <svg class="w-4 h-4 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2" /></svg>
-    </div>
-    <div>
-      <h1 class="text-sm font-bold tracking-tight text-surface-900 dark:text-white">NAS Dashboard</h1>
-      <p class="text-[10px] text-surface-400 font-medium">DS224+ · Synology</p>
-    </div>
-  </div>
- 
-  <!-- Nav Links -->
-  <nav class="flex-1 px-3 mt-2 overflow-y-auto">
-    {#each [["main", "Main", links], ["media", "Media", media], ["tools", "Tools", tools], ["tools2", "Tools 2", tools2]] as [cat, label, items], ci}
-      {#if ci > 0}
-        <div class="my-4 border-t border-surface-200 dark:border-surface-700"></div>
-      {/if}
-      <button
-        class="w-full text-left text-[10px] font-semibold uppercase tracking-wider text-surface-400 px-3 mb-2 flex items-center justify-between {cat !== 'main' ? 'cursor-pointer' : ''}"
-        ondragover={(e) => handleCategoryDragOver(cat, e)}
-        ondrop={(e) => handleCategoryDrop(cat, e)}
-        onclick={() => cat !== 'main' && toggleCollapse(cat)}
-        disabled={cat === 'main'}
-      >
-        {label}
-        {#if cat !== "main"}
-          <svg class="w-3 h-3 opacity-30 hover:opacity-50 transition-all duration-150 {collapsed[cat] ? '-rotate-90' : ''}" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7" /></svg>
-        {/if}
-      </button>
-      {#if cat === "main" || !collapsed[cat]}
-      {#if items.length === 0}
-        <div
-          class="px-3 py-3 mx-1 rounded-lg border border-dashed text-[11px] text-center transition-colors duration-150
-            {dropCategory === cat ? 'border-primary-400 bg-primary-50 dark:bg-primary-900/20 text-primary-500' : 'border-surface-300 dark:border-surface-600 text-surface-400'}"
-          ondragover={(e) => handleCategoryDragOver(cat, e)}
-          ondrop={(e) => handleCategoryDrop(cat, e)}
-        >
-          Drop items here
-        </div>
-      {/if}
-      {#each items as item, i}
-        {#if canAccess(item.id) && canSeeSidebarLink(item)}
-          {#if item.external || item.remoteHref || item.port}
-            <a
-              href={extHref(item)}
-              target="_blank"
-              rel="noopener"
-              draggable="true"
-              ondragstart={(e) => handleDragStart(cat, i, e)}
-              ondragover={(e) => handleDragOver(cat, i, e)}
-              ondrop={(e) => handleDrop(cat, i, e)}
-              ondragend={handleDragEnd}
-              class="w-full text-left px-3 py-2 rounded-lg text-[13px] font-medium flex items-center gap-2.5 mb-0.5 text-surface-500 hover:bg-surface-100 dark:hover:bg-surface-700 hover:text-surface-700 dark:hover:text-surface-200 dark:text-surface-400 transition-all duration-150 group {dragClasses(cat, i)}"
-            >
-              <svg class="w-3 h-3 opacity-0 group-hover:opacity-30 -ml-1 mr-0.5 shrink-0 cursor-grab" fill="currentColor" viewBox="0 0 24 24"><circle cx="9" cy="6" r="2"/><circle cx="15" cy="6" r="2"/><circle cx="9" cy="12" r="2"/><circle cx="15" cy="12" r="2"/><circle cx="9" cy="18" r="2"/><circle cx="15" cy="18" r="2"/></svg>
-              {@render iconSvg(item.icon)}
-              {item.label}
-              <svg class="w-3 h-3 ml-auto opacity-40" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" /></svg>
-            </a>
-          {:else}
-            <button
-              draggable="true"
-              ondragstart={(e) => handleDragStart(cat, i, e)}
-              ondragover={(e) => handleDragOver(cat, i, e)}
-              ondrop={(e) => handleDrop(cat, i, e)}
-              ondragend={handleDragEnd}
-              class="w-full text-left px-3 py-2.5 rounded-lg text-[13px] font-medium flex items-center gap-2.5 mb-0.5 transition-all duration-150 group
-                {page === item.id
-                  ? 'bg-primary-50 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300 shadow-sm'
-                  : 'text-surface-500 dark:text-surface-400 hover:bg-surface-100 dark:hover:bg-surface-700 hover:text-surface-700 dark:hover:text-surface-200'
-                } {dragClasses(cat, i)}"
-              onclick={() => navigate(item.id)}
-            >
-              <svg class="w-3 h-3 opacity-0 group-hover:opacity-30 -ml-1 mr-0.5 shrink-0 cursor-grab" fill="currentColor" viewBox="0 0 24 24"><circle cx="9" cy="6" r="2"/><circle cx="15" cy="6" r="2"/><circle cx="9" cy="12" r="2"/><circle cx="15" cy="12" r="2"/><circle cx="9" cy="18" r="2"/><circle cx="15" cy="18" r="2"/></svg>
-              {@render iconSvg(item.icon)}
-              {item.label}
-            </button>
-          {/if}
-        {/if}
-      {/each}
-      {/if}
-    {/each}
-  </nav>
- 
-  <!-- Bottom actions -->
-  <div class="px-3 pb-3 pt-2 border-t border-surface-200 dark:border-surface-700">
-    {#if userRole === "admin"}
-    <button
-      class="w-full px-3 py-2 rounded-lg text-[13px] font-medium flex items-center gap-2.5 text-surface-500 dark:text-surface-400 hover:bg-surface-100 dark:hover:bg-surface-700 transition-all duration-150 mb-1 {page === 'settings' ? 'bg-primary-50 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300 shadow-sm' : ''}"
-      onclick={() => navigate('settings')}
-    >
-      <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" /><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /></svg>
-      Settings
-    </button>
-    {/if}
-    <div class="flex items-center gap-1">
-      <button
-        onclick={logout}
-        class="flex-1 px-3 py-2 rounded-lg text-[13px] font-medium flex items-center gap-2.5 text-surface-500 dark:text-surface-400 hover:bg-surface-100 dark:hover:bg-surface-700 transition-all duration-150 hover:text-red-600 dark:hover:text-red-400"
-      >
-        <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" /></svg>
-        Sign out
-      </button>
-      <button
-        onclick={toggleTheme}
-        class="p-2 rounded-lg text-surface-400 hover:bg-surface-100 dark:hover:bg-surface-700 hover:text-surface-600 dark:hover:text-surface-300 transition-all duration-150"
-        title={dark ? 'Light Mode' : 'Dark Mode'}
-      >
-        {#if dark}
-          <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" /></svg>
-        {:else}
-          <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" /></svg>
-        {/if}
-      </button>
-    </div>
-  </div>
-</aside>
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/dashboard/frontend/coverage/lcov-report/src/components/index.html b/dashboard/frontend/coverage/lcov-report/src/components/index.html deleted file mode 100644 index 61324a1..0000000 --- a/dashboard/frontend/coverage/lcov-report/src/components/index.html +++ /dev/null @@ -1,116 +0,0 @@ - - - - - - Code coverage report for src/components - - - - - - - - - -
-
-

All files src/components

-
- -
- 0% - Statements - 0/301 -
- - -
- 0% - Branches - 0/1 -
- - -
- 0% - Functions - 0/1 -
- - -
- 0% - Lines - 0/301 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FileStatementsBranchesFunctionsLines
Sidebar.svelte -
-
0%0/3010%0/10%0/10%0/301
-
-
-
- - - - - - - - \ No newline at end of file diff --git a/dashboard/frontend/coverage/lcov-report/src/components/opc/AgentPanel.svelte.html b/dashboard/frontend/coverage/lcov-report/src/components/opc/AgentPanel.svelte.html deleted file mode 100644 index f3301f0..0000000 --- a/dashboard/frontend/coverage/lcov-report/src/components/opc/AgentPanel.svelte.html +++ /dev/null @@ -1,688 +0,0 @@ - - - - - - Code coverage report for src/components/opc/AgentPanel.svelte - - - - - - - - - -
-
-

All files / src/components/opc AgentPanel.svelte

-
- -
- 0% - Statements - 0/117 -
- - -
- 0% - Branches - 0/1 -
- - -
- 0% - Functions - 0/1 -
- - -
- 0% - Lines - 0/117 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -178 -179 -180 -181 -182 -183 -184 -185 -186 -187 -188 -189 -190 -191 -192 -193 -194 -195 -196 -197 -198 -199 -200 -201 -202  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  - 
<script>
-  import { onMount } from "svelte";
-  import { getAgents, getExecutions } from "../../lib/opc-api.js";
- 
-  let agents = $state([]);
-  let executions = $state([]);
-  let loading = $state(true);
-  let selectedAgent = $state(null);
- 
-  async function loadData() {
-    loading = true;
-    try {
-      const [agentsRes, executionsRes] = await Promise.all([
-        getAgents(),
-        getExecutions({ limit: 20 })
-      ]);
-      agents = agentsRes.items || [];
-      executions = executionsRes.items || [];
-    } catch (e) {
-      console.error("Failed to load agent data:", e);
-    } finally {
-      loading = false;
-    }
-  }
- 
-  function getAgentIcon(agentId) {
-    const icons = {
-      pm: "📋",
-      cto: "💻",
-      coo: "⚙️",
-      ceo: "🎯",
-      marketing: "📢",
-      social_media: "📱"
-    };
-    return icons[agentId] || "🤖";
-  }
- 
-  function getStatusColor(status) {
-    const colors = {
-      pending: "bg-slate-100 text-slate-700",
-      running: "bg-blue-100 text-blue-700",
-      completed: "bg-emerald-100 text-emerald-700",
-      failed: "bg-rose-100 text-rose-700",
-      pending_approval: "bg-amber-100 text-amber-700"
-    };
-    return colors[status] || colors.pending;
-  }
- 
-  function getAgentExecutions(agentId) {
-    return executions.filter(e => e.agent_id === agentId);
-  }
- 
-  function formatTimestamp(timestamp) {
-    if (!timestamp) return "N/A";
-    return new Date(timestamp).toLocaleString();
-  }
- 
-  onMount(() => {
-    loadData();
-    // Refresh every 10 seconds
-    const interval = setInterval(loadData, 10000);
-    return () => clearInterval(interval);
-  });
-</script>
- 
-<div class="space-y-6">
-  <h2 class="text-xl font-semibold text-surface-900 dark:text-surface-100">
-    AI Agents
-  </h2>
- 
-  {#if loading}
-    <div class="text-surface-600 dark:text-surface-400">Loading agents...</div>
-  {:else}
-    <!-- Agent Cards -->
-    <div class="grid grid-cols-1 md:grid-cols-3 gap-4">
-      {#each agents as agent}
-        {@const agentExecs = getAgentExecutions(agent.id)}
-        {@const runningExecs = agentExecs.filter(e => e.status === "running")}
-        {@const completedExecs = agentExecs.filter(e => e.status === "completed")}
- 
-        <button
-          onclick={() => selectedAgent = selectedAgent?.id === agent.id ? null : agent}
-          class="bg-white dark:bg-surface-800 rounded-lg p-4 border-2 transition-all text-left hover:shadow-lg {selectedAgent?.id === agent.id ? 'border-primary-500' : 'border-surface-200 dark:border-surface-700'}"
-        >
-          <!-- Agent Header -->
-          <div class="flex items-start justify-between mb-3">
-            <div class="flex items-center gap-2">
-              <span class="text-3xl">{getAgentIcon(agent.id)}</span>
-              <div>
-                <h3 class="font-semibold text-surface-900 dark:text-surface-100">
-                  {agent.name}
-                </h3>
-                <p class="text-xs text-surface-600 dark:text-surface-400">
-                  {agent.role}
-                </p>
-              </div>
-            </div>
-            {#if runningExecs.length > 0}
-              <span class="flex items-center gap-1 text-xs text-blue-600 dark:text-blue-400">
-                <span class="animate-pulse">●</span>
-                Working
-              </span>
-            {:else}
-              <span class="text-xs text-surface-500">Idle</span>
-            {/if}
-          </div>
- 
-          <!-- Agent Stats -->
-          <div class="grid grid-cols-2 gap-2 text-sm">
-            <div class="bg-surface-50 dark:bg-surface-900 rounded p-2">
-              <div class="text-xs text-surface-600 dark:text-surface-400">Total Tasks</div>
-              <div class="text-lg font-semibold text-surface-900 dark:text-surface-100">
-                {agentExecs.length}
-              </div>
-            </div>
-            <div class="bg-surface-50 dark:bg-surface-900 rounded p-2">
-              <div class="text-xs text-surface-600 dark:text-surface-400">Completed</div>
-              <div class="text-lg font-semibold text-emerald-600 dark:text-emerald-400">
-                {completedExecs.length}
-              </div>
-            </div>
-          </div>
- 
-          <!-- Capabilities -->
-          <div class="mt-3">
-            <div class="text-xs text-surface-600 dark:text-surface-400 mb-1">Capabilities:</div>
-            <div class="flex flex-wrap gap-1">
-              {#each agent.capabilities.slice(0, 3) as capability}
-                <span class="text-xs px-2 py-0.5 rounded bg-primary-50 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300">
-                  {capability.replace(/_/g, ' ')}
-                </span>
-              {/each}
-              {#if agent.capabilities.length > 3}
-                <span class="text-xs text-surface-500">+{agent.capabilities.length - 3}</span>
-              {/if}
-            </div>
-          </div>
-        </button>
-      {/each}
-    </div>
- 
-    <!-- Execution Log -->
-    <div class="bg-white dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700">
-      <div class="p-4 border-b border-surface-200 dark:border-surface-700">
-        <h3 class="font-semibold text-surface-900 dark:text-surface-100">
-          {selectedAgent ? `${selectedAgent.name} Execution Log` : "Recent Executions"}
-        </h3>
-      </div>
- 
-      <div class="divide-y divide-surface-200 dark:divide-surface-700 max-h-96 overflow-y-auto">
-        {#each (selectedAgent ? getAgentExecutions(selectedAgent.id) : executions) as execution}
-          <div class="p-4 hover:bg-surface-50 dark:hover:bg-surface-900/50">
-            <div class="flex items-start justify-between mb-2">
-              <div class="flex items-center gap-2">
-                <span class="text-xl">{getAgentIcon(execution.agent_id)}</span>
-                <div>
-                  <div class="font-medium text-surface-900 dark:text-surface-100">
-                    Task #{execution.task_id}
-                  </div>
-                  <div class="text-xs text-surface-600 dark:text-surface-400">
-                    {formatTimestamp(execution.started_at)}
-                  </div>
-                </div>
-              </div>
-              <span class="text-xs px-2 py-1 rounded {getStatusColor(execution.status)}">
-                {execution.status.replace(/_/g, ' ')}
-              </span>
-            </div>
- 
-            {#if execution.output_result?.reasoning}
-              <p class="text-sm text-surface-700 dark:text-surface-300 mb-2">
-                {execution.output_result.reasoning}
-              </p>
-            {/if}
- 
-            {#if execution.actions_proposed && execution.actions_proposed.length > 0}
-              <div class="text-xs text-surface-600 dark:text-surface-400">
-                Actions: {execution.actions_proposed.length}
-                {#each execution.actions_proposed.slice(0, 2) as action}
-                  <span class="ml-2 px-1.5 py-0.5 rounded bg-surface-100 dark:bg-surface-700">
-                    {action.type}
-                  </span>
-                {/each}
-              </div>
-            {/if}
- 
-            {#if execution.error_message}
-              <div class="mt-2 text-xs text-rose-600 dark:text-rose-400">
-                Error: {execution.error_message}
-              </div>
-            {/if}
-          </div>
-        {:else}
-          <div class="p-8 text-center text-surface-500">
-            No executions yet
-          </div>
-        {/each}
-      </div>
-    </div>
-  {/if}
-</div>
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/dashboard/frontend/coverage/lcov-report/src/components/opc/KanbanBoard.svelte.html b/dashboard/frontend/coverage/lcov-report/src/components/opc/KanbanBoard.svelte.html deleted file mode 100644 index eef3bc8..0000000 --- a/dashboard/frontend/coverage/lcov-report/src/components/opc/KanbanBoard.svelte.html +++ /dev/null @@ -1,205 +0,0 @@ - - - - - - Code coverage report for src/components/opc/KanbanBoard.svelte - - - - - - - - - -
-
-

All files / src/components/opc KanbanBoard.svelte

-
- -
- 0% - Statements - 0/26 -
- - -
- 0% - Branches - 0/1 -
- - -
- 0% - Functions - 0/1 -
- - -
- 0% - Lines - 0/26 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  - 
<script>
-  import KanbanColumn from "./KanbanColumn.svelte";
-  import { moveTask } from "../../lib/opc-api.js";
- 
-  let { tasks = [], agents = [], onEdit, onDelete, onAssign, onTaskMoved } = $props();
- 
-  const columns = [
-    { id: "parking_lot", title: "Parking Lot", status: "parking_lot" },
-    { id: "in_progress", title: "In Progress", status: "in_progress" },
-    { id: "done", title: "Done", status: "done" }
-  ];
- 
-  function getTasksByStatus(status) {
-    return tasks.filter(t => t.status === status);
-  }
- 
-  async function handleDrop(taskId, newStatus) {
-    try {
-      await moveTask(taskId, newStatus);
-      onTaskMoved?.(taskId, newStatus);
-    } catch (e) {
-      alert("Failed to move task: " + e.message);
-    }
-  }
-</script>
- 
-<div class="grid grid-cols-1 md:grid-cols-3 gap-4 h-full">
-  {#each columns as column}
-    <KanbanColumn
-      title={column.title}
-      status={column.status}
-      tasks={getTasksByStatus(column.status)}
-      {agents}
-      {onEdit}
-      {onDelete}
-      {onAssign}
-      onDrop={handleDrop}
-    />
-  {/each}
-</div>
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/dashboard/frontend/coverage/lcov-report/src/components/opc/KanbanColumn.svelte.html b/dashboard/frontend/coverage/lcov-report/src/components/opc/KanbanColumn.svelte.html deleted file mode 100644 index d87f4e5..0000000 --- a/dashboard/frontend/coverage/lcov-report/src/components/opc/KanbanColumn.svelte.html +++ /dev/null @@ -1,340 +0,0 @@ - - - - - - Code coverage report for src/components/opc/KanbanColumn.svelte - - - - - - - - - -
-
-

All files / src/components/opc KanbanColumn.svelte

-
- -
- 0% - Statements - 0/46 -
- - -
- 0% - Branches - 0/1 -
- - -
- 0% - Functions - 0/1 -
- - -
- 0% - Lines - 0/46 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  - 
<script>
-  import TaskCard from "./TaskCard.svelte";
- 
-  let {
-    title,
-    status,
-    tasks = [],
-    agents = [],
-    onEdit,
-    onDelete,
-    onAssign,
-    onDrop
-  } = $props();
- 
-  const columnColors = {
-    parking_lot: "bg-slate-100 dark:bg-slate-800 border-slate-300",
-    in_progress: "bg-blue-50 dark:bg-blue-900/20 border-blue-300",
-    done: "bg-emerald-50 dark:bg-emerald-900/20 border-emerald-300"
-  };
- 
-  function handleDragOver(e) {
-    e.preventDefault();
-    e.currentTarget.classList.add("ring-2", "ring-primary-400");
-  }
- 
-  function handleDragLeave(e) {
-    e.currentTarget.classList.remove("ring-2", "ring-primary-400");
-  }
- 
-  function handleDrop(e) {
-    e.preventDefault();
-    e.currentTarget.classList.remove("ring-2", "ring-primary-400");
- 
-    const taskId = parseInt(e.dataTransfer.getData("taskId"));
-    const fromStatus = e.dataTransfer.getData("fromStatus");
- 
-    if (fromStatus !== status) {
-      onDrop?.(taskId, status);
-    }
-  }
- 
-  function handleDragStart(e, task) {
-    e.dataTransfer.setData("taskId", task.id);
-    e.dataTransfer.setData("fromStatus", task.status);
-    e.dataTransfer.effectAllowed = "move";
-  }
-</script>
- 
-<div class="flex flex-col h-full">
-  <!-- Column header -->
-  <div class="flex items-center justify-between mb-3 pb-2 border-b border-surface-300 dark:border-surface-600">
-    <h3 class="font-semibold text-surface-900 dark:text-surface-100">
-      {title}
-    </h3>
-    <span class="text-sm text-surface-600 dark:text-surface-400 bg-surface-200 dark:bg-surface-700 px-2 py-0.5 rounded-full">
-      {tasks.length}
-    </span>
-  </div>
- 
-  <!-- Drop zone -->
-  <div
-    class="flex-1 min-h-[200px] p-2 rounded-lg border-2 border-dashed transition-all {columnColors[status] || columnColors.parking_lot}"
-    ondragover={handleDragOver}
-    ondragleave={handleDragLeave}
-    ondrop={handleDrop}
-  >
-    {#if tasks.length === 0}
-      <div class="flex items-center justify-center h-32 text-surface-400 dark:text-surface-500 text-sm">
-        Drop tasks here
-      </div>
-    {:else}
-      {#each tasks as task (task.id)}
-        <div draggable="true" ondragstart={(e) => handleDragStart(e, task)}>
-          <TaskCard
-            {task}
-            {agents}
-            {onEdit}
-            {onDelete}
-            {onAssign}
-          />
-        </div>
-      {/each}
-    {/if}
-  </div>
-</div>
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/dashboard/frontend/coverage/lcov-report/src/components/opc/TaskCard.svelte.html b/dashboard/frontend/coverage/lcov-report/src/components/opc/TaskCard.svelte.html deleted file mode 100644 index 59abd16..0000000 --- a/dashboard/frontend/coverage/lcov-report/src/components/opc/TaskCard.svelte.html +++ /dev/null @@ -1,592 +0,0 @@ - - - - - - Code coverage report for src/components/opc/TaskCard.svelte - - - - - - - - - -
-
-

All files / src/components/opc TaskCard.svelte

-
- -
- 0% - Statements - 0/94 -
- - -
- 0% - Branches - 0/1 -
- - -
- 0% - Functions - 0/1 -
- - -
- 0% - Lines - 0/94 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  - 
<script>
-  import { deleteTask, assignTask, getTaskTime } from "../../lib/opc-api.js";
- 
-  let { task, agents = [], onEdit, onDelete, onAssign } = $props();
- 
-  let showActions = $state(false);
-  let timeInfo = $state(null);
- 
-  const priorityColors = {
-    low: "bg-slate-200 text-slate-700",
-    medium: "bg-blue-100 text-blue-700",
-    high: "bg-amber-100 text-amber-700",
-    urgent: "bg-rose-100 text-rose-700"
-  };
- 
-  async function loadTimeInfo() {
-    if (task.status === "in_progress") {
-      try {
-        timeInfo = await getTaskTime(task.id);
-      } catch (e) {
-        console.error("Failed to load time info:", e);
-      }
-    }
-  }
- 
-  $effect(() => {
-    loadTimeInfo();
-  });
- 
-  async function handleDelete() {
-    if (confirm(`Delete task "${task.title}"?`)) {
-      try {
-        await deleteTask(task.id);
-        onDelete?.(task.id);
-      } catch (e) {
-        alert("Failed to delete task: " + e.message);
-      }
-    }
-  }
- 
-  async function handleAssignAgent(agentId) {
-    try {
-      await assignTask(task.id, agentId, "agent");
-      onAssign?.(task.id, agentId, "agent");
-    } catch (e) {
-      alert("Failed to assign agent: " + e.message);
-    }
-  }
- 
-  function getAgentIcon(agentId) {
-    const icons = {
-      pm: "📋",
-      cto: "💻",
-      coo: "⚙️",
-      ceo: "🎯",
-      marketing: "📢",
-      social_media: "📱"
-    };
-    return icons[agentId] || "🤖";
-  }
-</script>
- 
-<div
-  class="bg-white dark:bg-surface-800 rounded-lg p-3 mb-2 shadow-sm border border-surface-200 dark:border-surface-700 cursor-move hover:shadow-md transition-shadow"
-  onmouseenter={() => showActions = true}
-  onmouseleave={() => showActions = false}
->
-  <!-- Priority badge -->
-  <div class="flex items-start justify-between mb-2">
-    <span class="text-xs px-2 py-0.5 rounded {priorityColors[task.priority] || priorityColors.medium}">
-      {task.priority}
-    </span>
-    {#if showActions}
-      <div class="flex gap-1">
-        <button
-          onclick={() => onEdit?.(task)}
-          class="text-xs text-surface-600 hover:text-primary-600 dark:text-surface-400"
-          title="Edit"
-        >
-          ✏️
-        </button>
-        <button
-          onclick={handleDelete}
-          class="text-xs text-surface-600 hover:text-rose-600 dark:text-surface-400"
-          title="Delete"
-        >
-          🗑️
-        </button>
-      </div>
-    {/if}
-  </div>
- 
-  <!-- Title -->
-  <h4 class="font-medium text-surface-900 dark:text-surface-100 mb-1 text-sm">
-    {task.title}
-  </h4>
- 
-  <!-- Description preview -->
-  {#if task.description}
-    <p class="text-xs text-surface-600 dark:text-surface-400 mb-2 line-clamp-2">
-      {task.description}
-    </p>
-  {/if}
- 
-  <!-- Tags -->
-  {#if task.tags && task.tags.length > 0}
-    <div class="flex flex-wrap gap-1 mb-2">
-      {#each task.tags as tag}
-        <span class="text-xs px-1.5 py-0.5 rounded bg-surface-100 dark:bg-surface-700 text-surface-700 dark:text-surface-300">
-          {tag}
-        </span>
-      {/each}
-    </div>
-  {/if}
- 
-  <!-- Assignee and time -->
-  <div class="flex items-center justify-between text-xs text-surface-600 dark:text-surface-400">
-    <div class="flex items-center gap-1">
-      {#if task.assigned_to}
-        {#if task.assigned_type === "agent"}
-          <span title={task.assigned_to}>
-            {getAgentIcon(task.assigned_to)}
-          </span>
-        {:else}
-          <span>👤 {task.assigned_to}</span>
-        {/if}
-      {:else}
-        <button
-          onclick={() => showActions = true}
-          class="text-surface-500 hover:text-primary-600"
-          title="Assign"
-        >
-          Unassigned
-        </button>
-      {/if}
-    </div>
- 
-    {#if timeInfo && timeInfo.total_hours > 0}
-      <span class="text-xs text-surface-500">
-        ⏱️ {timeInfo.total_hours}h
-      </span>
-    {/if}
-  </div>
- 
-  <!-- Due date -->
-  {#if task.due_date}
-    <div class="text-xs text-surface-500 mt-1">
-      📅 {new Date(task.due_date).toLocaleDateString()}
-    </div>
-  {/if}
- 
-  <!-- Quick assign to agent (when hovering) -->
-  {#if showActions && !task.assigned_to}
-    <div class="mt-2 pt-2 border-t border-surface-200 dark:border-surface-700">
-      <div class="text-xs text-surface-600 mb-1">Assign to agent:</div>
-      <div class="flex gap-1">
-        {#each agents as agent}
-          <button
-            onclick={() => handleAssignAgent(agent.id)}
-            class="text-lg hover:scale-110 transition-transform"
-            title={agent.name}
-          >
-            {getAgentIcon(agent.id)}
-          </button>
-        {/each}
-      </div>
-    </div>
-  {/if}
-</div>
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/dashboard/frontend/coverage/lcov-report/src/components/opc/TaskModal.svelte.html b/dashboard/frontend/coverage/lcov-report/src/components/opc/TaskModal.svelte.html deleted file mode 100644 index aa7ab89..0000000 --- a/dashboard/frontend/coverage/lcov-report/src/components/opc/TaskModal.svelte.html +++ /dev/null @@ -1,835 +0,0 @@ - - - - - - Code coverage report for src/components/opc/TaskModal.svelte - - - - - - - - - -
-
-

All files / src/components/opc TaskModal.svelte

-
- -
- 0% - Statements - 0/110 -
- - -
- 0% - Branches - 0/1 -
- - -
- 0% - Functions - 0/1 -
- - -
- 0% - Lines - 0/110 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -178 -179 -180 -181 -182 -183 -184 -185 -186 -187 -188 -189 -190 -191 -192 -193 -194 -195 -196 -197 -198 -199 -200 -201 -202 -203 -204 -205 -206 -207 -208 -209 -210 -211 -212 -213 -214 -215 -216 -217 -218 -219 -220 -221 -222 -223 -224 -225 -226 -227 -228 -229 -230 -231 -232 -233 -234 -235 -236 -237 -238 -239 -240 -241 -242 -243 -244 -245 -246 -247 -248 -249 -250 -251  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  - 
<script>
-  import { createTask, updateTask } from "../../lib/opc-api.js";
- 
-  let { task = null, agents = [], onClose, onSave } = $props();
- 
-  let isEdit = $state(!!task);
-  let formData = $state({
-    title: task?.title || "",
-    description: task?.description || "",
-    status: task?.status || "parking_lot",
-    priority: task?.priority || "medium",
-    assigned_to: task?.assigned_to || "",
-    assigned_type: task?.assigned_type || "human",
-    tags: task?.tags || [],
-    due_date: task?.due_date ? task.due_date.split("T")[0] : ""
-  });
- 
-  let tagInput = $state("");
-  let saving = $state(false);
- 
-  function addTag() {
-    if (tagInput.trim() && !formData.tags.includes(tagInput.trim())) {
-      formData.tags = [...formData.tags, tagInput.trim()];
-      tagInput = "";
-    }
-  }
- 
-  function removeTag(tag) {
-    formData.tags = formData.tags.filter(t => t !== tag);
-  }
- 
-  async function handleSubmit(e) {
-    e.preventDefault();
-    saving = true;
- 
-    try {
-      const payload = {
-        ...formData,
-        due_date: formData.due_date ? new Date(formData.due_date).toISOString() : null,
-        assigned_to: formData.assigned_to || null
-      };
- 
-      if (isEdit) {
-        await updateTask(task.id, payload);
-      } else {
-        await createTask(payload);
-      }
- 
-      onSave?.();
-      onClose?.();
-    } catch (e) {
-      alert("Failed to save task: " + e.message);
-    } finally {
-      saving = false;
-    }
-  }
-</script>
- 
-<!-- Modal backdrop -->
-<div
-  class="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4"
-  onclick={(e) => e.target === e.currentTarget && onClose?.()}
->
-  <!-- Modal content -->
-  <div class="bg-white dark:bg-surface-800 rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto">
-    <!-- Header -->
-    <div class="flex items-center justify-between p-4 border-b border-surface-200 dark:border-surface-700">
-      <h2 class="text-xl font-semibold text-surface-900 dark:text-surface-100">
-        {isEdit ? "Edit Task" : "Create Task"}
-      </h2>
-      <button
-        onclick={onClose}
-        class="text-surface-500 hover:text-surface-700 dark:hover:text-surface-300"
-      >
-        ✕
-      </button>
-    </div>
- 
-    <!-- Form -->
-    <form onsubmit={handleSubmit} class="p-4 space-y-4">
-      <!-- Title -->
-      <div>
-        <label class="block text-sm font-medium text-surface-700 dark:text-surface-300 mb-1">
-          Title *
-        </label>
-        <input
-          type="text"
-          bind:value={formData.title}
-          required
-          class="w-full px-3 py-2 border border-surface-300 dark:border-surface-600 rounded-lg bg-white dark:bg-surface-900 text-surface-900 dark:text-surface-100"
-          placeholder="Task title"
-        />
-      </div>
- 
-      <!-- Description -->
-      <div>
-        <label class="block text-sm font-medium text-surface-700 dark:text-surface-300 mb-1">
-          Description
-        </label>
-        <textarea
-          bind:value={formData.description}
-          rows="4"
-          class="w-full px-3 py-2 border border-surface-300 dark:border-surface-600 rounded-lg bg-white dark:bg-surface-900 text-surface-900 dark:text-surface-100"
-          placeholder="Task description"
-        ></textarea>
-      </div>
- 
-      <!-- Status and Priority -->
-      <div class="grid grid-cols-2 gap-4">
-        <div>
-          <label class="block text-sm font-medium text-surface-700 dark:text-surface-300 mb-1">
-            Status
-          </label>
-          <select
-            bind:value={formData.status}
-            class="w-full px-3 py-2 border border-surface-300 dark:border-surface-600 rounded-lg bg-white dark:bg-surface-900 text-surface-900 dark:text-surface-100"
-          >
-            <option value="parking_lot">Parking Lot</option>
-            <option value="in_progress">In Progress</option>
-            <option value="done">Done</option>
-          </select>
-        </div>
- 
-        <div>
-          <label class="block text-sm font-medium text-surface-700 dark:text-surface-300 mb-1">
-            Priority
-          </label>
-          <select
-            bind:value={formData.priority}
-            class="w-full px-3 py-2 border border-surface-300 dark:border-surface-600 rounded-lg bg-white dark:bg-surface-900 text-surface-900 dark:text-surface-100"
-          >
-            <option value="low">Low</option>
-            <option value="medium">Medium</option>
-            <option value="high">High</option>
-            <option value="urgent">Urgent</option>
-          </select>
-        </div>
-      </div>
- 
-      <!-- Assignee -->
-      <div class="grid grid-cols-2 gap-4">
-        <div>
-          <label class="block text-sm font-medium text-surface-700 dark:text-surface-300 mb-1">
-            Assign to
-          </label>
-          <select
-            bind:value={formData.assigned_type}
-            class="w-full px-3 py-2 border border-surface-300 dark:border-surface-600 rounded-lg bg-white dark:bg-surface-900 text-surface-900 dark:text-surface-100"
-          >
-            <option value="human">Human</option>
-            <option value="agent">Agent</option>
-          </select>
-        </div>
- 
-        <div>
-          <label class="block text-sm font-medium text-surface-700 dark:text-surface-300 mb-1">
-            {formData.assigned_type === "agent" ? "Agent" : "Username"}
-          </label>
-          {#if formData.assigned_type === "agent"}
-            <select
-              bind:value={formData.assigned_to}
-              class="w-full px-3 py-2 border border-surface-300 dark:border-surface-600 rounded-lg bg-white dark:bg-surface-900 text-surface-900 dark:text-surface-100"
-            >
-              <option value="">Unassigned</option>
-              {#each agents as agent}
-                <option value={agent.id}>{agent.name}</option>
-              {/each}
-            </select>
-          {:else}
-            <input
-              type="text"
-              bind:value={formData.assigned_to}
-              class="w-full px-3 py-2 border border-surface-300 dark:border-surface-600 rounded-lg bg-white dark:bg-surface-900 text-surface-900 dark:text-surface-100"
-              placeholder="Username"
-            />
-          {/if}
-        </div>
-      </div>
- 
-      <!-- Due date -->
-      <div>
-        <label class="block text-sm font-medium text-surface-700 dark:text-surface-300 mb-1">
-          Due Date
-        </label>
-        <input
-          type="date"
-          bind:value={formData.due_date}
-          class="w-full px-3 py-2 border border-surface-300 dark:border-surface-600 rounded-lg bg-white dark:bg-surface-900 text-surface-900 dark:text-surface-100"
-        />
-      </div>
- 
-      <!-- Tags -->
-      <div>
-        <label class="block text-sm font-medium text-surface-700 dark:text-surface-300 mb-1">
-          Tags
-        </label>
-        <div class="flex gap-2 mb-2">
-          <input
-            type="text"
-            bind:value={tagInput}
-            onkeydown={(e) => e.key === "Enter" && (e.preventDefault(), addTag())}
-            class="flex-1 px-3 py-2 border border-surface-300 dark:border-surface-600 rounded-lg bg-white dark:bg-surface-900 text-surface-900 dark:text-surface-100"
-            placeholder="Add tag and press Enter"
-          />
-          <button
-            type="button"
-            onclick={addTag}
-            class="px-4 py-2 bg-surface-200 dark:bg-surface-700 text-surface-700 dark:text-surface-300 rounded-lg hover:bg-surface-300 dark:hover:bg-surface-600"
-          >
-            Add
-          </button>
-        </div>
-        {#if formData.tags.length > 0}
-          <div class="flex flex-wrap gap-2">
-            {#each formData.tags as tag}
-              <span class="inline-flex items-center gap-1 px-2 py-1 bg-primary-100 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300 rounded text-sm">
-                {tag}
-                <button
-                  type="button"
-                  onclick={() => removeTag(tag)}
-                  class="hover:text-primary-900 dark:hover:text-primary-100"
-                >
-                  ✕
-                </button>
-              </span>
-            {/each}
-          </div>
-        {/if}
-      </div>
- 
-      <!-- Actions -->
-      <div class="flex justify-end gap-2 pt-4 border-t border-surface-200 dark:border-surface-700">
-        <button
-          type="button"
-          onclick={onClose}
-          class="px-4 py-2 text-surface-700 dark:text-surface-300 hover:bg-surface-100 dark:hover:bg-surface-700 rounded-lg"
-        >
-          Cancel
-        </button>
-        <button
-          type="submit"
-          disabled={saving}
-          class="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 disabled:opacity-50"
-        >
-          {saving ? "Saving..." : isEdit ? "Update" : "Create"}
-        </button>
-      </div>
-    </form>
-  </div>
-</div>
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/dashboard/frontend/coverage/lcov-report/src/components/opc/index.html b/dashboard/frontend/coverage/lcov-report/src/components/opc/index.html deleted file mode 100644 index c610c13..0000000 --- a/dashboard/frontend/coverage/lcov-report/src/components/opc/index.html +++ /dev/null @@ -1,176 +0,0 @@ - - - - - - Code coverage report for src/components/opc - - - - - - - - - -
-
-

All files src/components/opc

-
- -
- 0% - Statements - 0/393 -
- - -
- 0% - Branches - 0/5 -
- - -
- 0% - Functions - 0/5 -
- - -
- 0% - Lines - 0/393 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FileStatementsBranchesFunctionsLines
AgentPanel.svelte -
-
0%0/1170%0/10%0/10%0/117
KanbanBoard.svelte -
-
0%0/260%0/10%0/10%0/26
KanbanColumn.svelte -
-
0%0/460%0/10%0/10%0/46
TaskCard.svelte -
-
0%0/940%0/10%0/10%0/94
TaskModal.svelte -
-
0%0/1100%0/10%0/10%0/110
-
-
-
- - - - - - - - \ No newline at end of file diff --git a/dashboard/frontend/coverage/lcov-report/src/index.html b/dashboard/frontend/coverage/lcov-report/src/index.html deleted file mode 100644 index e0d864b..0000000 --- a/dashboard/frontend/coverage/lcov-report/src/index.html +++ /dev/null @@ -1,131 +0,0 @@ - - - - - - Code coverage report for src - - - - - - - - - -
-
-

All files src

-
- -
- 0% - Statements - 0/182 -
- - -
- 0% - Branches - 0/2 -
- - -
- 0% - Functions - 0/2 -
- - -
- 0% - Lines - 0/182 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FileStatementsBranchesFunctionsLines
App.svelte -
-
0%0/1770%0/10%0/10%0/177
main.js -
-
0%0/50%0/10%0/10%0/5
-
-
-
- - - - - - - - \ No newline at end of file diff --git a/dashboard/frontend/coverage/lcov-report/src/lib/api.js.html b/dashboard/frontend/coverage/lcov-report/src/lib/api.js.html deleted file mode 100644 index 21ed412..0000000 --- a/dashboard/frontend/coverage/lcov-report/src/lib/api.js.html +++ /dev/null @@ -1,787 +0,0 @@ - - - - - - Code coverage report for src/lib/api.js - - - - - - - - - -
-
-

All files / src/lib api.js

-
- -
- 64.92% - Statements - 124/191 -
- - -
- 90.69% - Branches - 39/43 -
- - -
- 57.69% - Functions - 15/26 -
- - -
- 64.92% - Lines - 124/191 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -178 -179 -180 -181 -182 -183 -184 -185 -186 -187 -188 -189 -190 -191 -192 -193 -194 -195 -196 -197 -198 -199 -200 -201 -202 -203 -204 -205 -206 -207 -208 -209 -210 -211 -212 -213 -214 -215 -216 -217 -218 -219 -220 -221 -222 -223 -224 -225 -226 -227 -228 -229 -230 -231 -232 -233 -234 -2351x -1x -  -1x -1x -  -1x -1x -1x -  -1x -30x -30x -12x -30x -  -1x -5x -5x -  -1x -1x -  -1x -  -3x -3x -3x -  -3x -3x -3x -  -6x -6x -6x -6x -6x -6x -6x -6x -3x -6x -6x -6x -  -1x -5x -5x -5x -5x -5x -2x -2x -2x -  -3x -5x -1x -1x -1x -1x -1x -  -  -  -  -5x -5x -5x -5x -5x -5x -  -20x -20x -20x -  -20x -3x -3x -3x -3x -  -20x -20x -  -20x -20x -  -20x -2x -2x -1x -1x -1x -2x -  -20x -1x -1x -1x -  -20x -2x -2x -2x -2x -2x -16x -20x -4x -4x -4x -20x -  -1x -13x -13x -  -1x -2x -2x -  -1x -1x -1x -  -1x -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -1x -1x -1x -  -1x -1x -1x -  -1x -1x -1x -  -1x -  -  -  -1x -  -  -  -1x -  -  -  -1x -  -  -  -1x -  -  -  -1x -  -  -  -1x -  -  -  -  -  -  -  -  -  -  -1x -  -  -  -1x -1x -1x -  -1x -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  - 
const BASE = "/api";
-let token = "";
- 
-// Current user info populated after auth
-export let currentUser = { username: "", role: "", pages: [] };
- 
-export function setCurrentUser(u) {
-  currentUser = u;
-}
- 
-export function setToken(t) {
-  token = t;
-  if (t) localStorage.setItem("token", t);
-  else localStorage.removeItem("token");
-}
- 
-export function getToken() {
-  return token;
-}
- 
-// Refresh token is cookie-managed server-side.
-export function setRefreshToken() {}
- 
-let refreshPromise = null;
- 
-function getLegacyRefreshToken() {
-  return localStorage.getItem("refresh_token") || "";
-}
- 
-function clearLegacyRefreshToken() {
-  localStorage.removeItem("refresh_token");
-}
- 
-async function requestRefresh(body) {
-  const r = await fetch(BASE + "/auth/refresh", {
-    method: "POST",
-    headers: { "Content-Type": "application/json" },
-    credentials: "same-origin",
-    body: body ? JSON.stringify(body) : undefined,
-  });
-  if (!r.ok) return false;
-  const data = await r.json();
-  setToken(data.access_token || "");
-  return !!data.access_token;
-}
- 
-export async function tryRefreshSession() {
-  if (refreshPromise) return refreshPromise;
-  refreshPromise = (async () => {
-    try {
-      const cookieRefreshed = await requestRefresh();
-      if (cookieRefreshed) {
-        clearLegacyRefreshToken();
-        return true;
-      }
- 
-      const legacyRefreshToken = getLegacyRefreshToken();
-      if (!legacyRefreshToken) return false;
-      const legacyRefreshed = await requestRefresh({ refresh_token: legacyRefreshToken });
-      if (legacyRefreshed) {
-        clearLegacyRefreshToken();
-        return true;
-      }
-      clearLegacyRefreshToken();
-      return false;
-    } catch {
-      return false;
-    } finally {
-      refreshPromise = null;
-    }
-  })();
-  return refreshPromise;
-}
- 
-async function request(path, opts = {}) {
-  const headers = opts.headers || {};
-  if (token) headers["Authorization"] = `Bearer ${token}`;
- 
-  if (opts.json) {
-    headers["Content-Type"] = "application/json";
-    opts.body = JSON.stringify(opts.json);
-    delete opts.json;
-  }
- 
-  const fetchOpts = { ...opts, headers };
-  if (path.startsWith("/auth/")) fetchOpts.credentials = "same-origin";
- 
-  try {
-    let r = await fetch(BASE + path, fetchOpts);
- 
-    if (r.status === 401 && !path.includes("/auth/refresh")) {
-      const refreshed = await tryRefreshSession();
-      if (refreshed) {
-        headers["Authorization"] = `Bearer ${token}`;
-        r = await fetch(BASE + path, fetchOpts);
-      }
-    }
- 
-    if (r.status === 401) {
-      setToken("");
-      throw new Error("Unauthorized");
-    }
- 
-    if (!r.ok) {
-      const error = new Error(`HTTP ${r.status}`);
-      error.status = r.status;
-      try { error.body = await r.json(); } catch {}
-      throw error;
-    }
-    return r.json();
-  } catch (e) {
-    console.error(`API ${opts.method || "GET"} ${path}:`, e);
-    throw e;
-  }
-}
- 
-export function get(path) {
-  return request(path);
-}
- 
-export function post(path, data) {
-  return request(path, { method: "POST", json: data });
-}
- 
-export function del(path) {
-  return request(path, { method: "DELETE" });
-}
- 
-export async function upload(path, file) {
-  const headers = {};
-  if (token) headers["Authorization"] = `Bearer ${token}`;
- 
-  const form = new FormData();
-  form.append("file", file);
- 
-  const r = await fetch(`${BASE}/files/upload?path=${encodeURIComponent(path)}`, {
-    method: "POST",
-    headers,
-    body: form,
-  });
- 
-  if (!r.ok) throw new Error(`HTTP ${r.status}`);
-  return r.json();
-}
- 
-export function login(creds) {
-  return request("/auth/login", { method: "POST", json: creds });
-}
- 
-export function checkAuth() {
-  return request("/auth/me");
-}
- 
-export function logout() {
-  return request("/auth/logout", { method: "POST" });
-}
- 
-export function getPreferences() {
-  return get("/auth/preferences");
-}
- 
-export function savePreferences(data) {
-  return request("/auth/preferences", { method: "PUT", json: data });
-}
- 
-export function getLiteLLMHealth() {
-  return get("/litellm/health");
-}
- 
-export function getCcConnectHealth() {
-  return get("/cc-connect/health");
-}
- 
-export function startCcConnect() {
-  return post("/cc-connect/start");
-}
- 
-export function stopCcConnect() {
-  return post("/cc-connect/stop");
-}
- 
-export function getInfoEngineItems(params = {}) {
-  const query = new URLSearchParams();
-  if (params.limit !== undefined) query.set("limit", String(params.limit));
-  if (params.offset !== undefined) query.set("offset", String(params.offset));
-  if (params.source) query.set("source", params.source);
-  if (params.tag) query.set("tag", params.tag);
-  if (params.since) query.set("since", params.since);
-  const suffix = query.toString() ? `?${query.toString()}` : "";
-  return get(`/info-engine/items${suffix}`);
-}
- 
-export function getInfoEngineItem(id) {
-  return get(`/info-engine/items/${id}`);
-}
- 
-export function put(path, data) {
-  return request(path, { method: "PUT", json: data });
-}
- 
-export async function download(path, filename) {
-  const headers = {};
-  if (token) headers["Authorization"] = `Bearer ${token}`;
- 
-  try {
-    // Get a temporary download token
-    const tokenResponse = await fetch(`${BASE}/files/download-token?path=${encodeURIComponent(path)}`, {
-      method: "POST",
-      headers,
-    });
- 
-    if (!tokenResponse.ok) {
-      const errorText = await tokenResponse.text();
-      console.error(`Download token failed: ${tokenResponse.status} - ${errorText}`);
-      throw new Error(`Download failed: ${tokenResponse.status}`);
-    }
- 
-    const { token: downloadToken } = await tokenResponse.json();
- 
-    // Use hidden iframe for download - avoids page navigation and popup blockers
-    const iframe = document.createElement('iframe');
-    iframe.style.display = 'none';
-    iframe.src = `${BASE}/files/download?token=${downloadToken}`;
-    document.body.appendChild(iframe);
- 
-    // Clean up iframe after download starts
-    setTimeout(() => {
-      document.body.removeChild(iframe);
-    }, 5000);
-  } catch (e) {
-    console.error("Download error:", e);
-    throw e;
-  }
-}
- -
-
- - - - - - - - \ No newline at end of file diff --git a/dashboard/frontend/coverage/lcov-report/src/lib/index.html b/dashboard/frontend/coverage/lcov-report/src/lib/index.html deleted file mode 100644 index 0176ae0..0000000 --- a/dashboard/frontend/coverage/lcov-report/src/lib/index.html +++ /dev/null @@ -1,161 +0,0 @@ - - - - - - Code coverage report for src/lib - - - - - - - - - -
-
-

All files src/lib

-
- -
- 22.06% - Statements - 124/562 -
- - -
- 84.78% - Branches - 39/46 -
- - -
- 51.72% - Functions - 15/29 -
- - -
- 22.06% - Lines - 124/562 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FileStatementsBranchesFunctionsLines
api.js -
-
64.92%124/19190.69%39/4357.69%15/2664.92%124/191
opc-api.js -
-
0%0/640%0/10%0/10%0/64
opc-ws.js -
-
0%0/750%0/10%0/10%0/75
voice.js -
-
0%0/2320%0/10%0/10%0/232
-
-
-
- - - - - - - - \ No newline at end of file diff --git a/dashboard/frontend/coverage/lcov-report/src/lib/opc-api.js.html b/dashboard/frontend/coverage/lcov-report/src/lib/opc-api.js.html deleted file mode 100644 index 8cd5fc5..0000000 --- a/dashboard/frontend/coverage/lcov-report/src/lib/opc-api.js.html +++ /dev/null @@ -1,325 +0,0 @@ - - - - - - Code coverage report for src/lib/opc-api.js - - - - - - - - - -
-
-

All files / src/lib opc-api.js

-
- -
- 0% - Statements - 0/64 -
- - -
- 0% - Branches - 0/1 -
- - -
- 0% - Functions - 0/1 -
- - -
- 0% - Lines - 0/64 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  - 
/**
- * OPC API Client
- */
-import { get, post, put, del } from "./api.js";
- 
-// Tasks
-export async function getTasks(filters = {}) {
-  const params = new URLSearchParams();
-  if (filters.status) params.append("status", filters.status);
-  if (filters.assigned_to) params.append("assigned_to", filters.assigned_to);
-  if (filters.project_id) params.append("project_id", filters.project_id);
-  if (filters.priority) params.append("priority", filters.priority);
-  if (filters.tags) params.append("tags", filters.tags.join(","));
-  if (filters.limit) params.append("limit", filters.limit);
-  if (filters.offset) params.append("offset", filters.offset);
- 
-  const query = params.toString();
-  return get(`/opc/tasks${query ? "?" + query : ""}`);
-}
- 
-export async function createTask(task) {
-  return post("/opc/tasks", task);
-}
- 
-export async function updateTask(taskId, updates) {
-  return put(`/opc/tasks/${taskId}`, updates);
-}
- 
-export async function deleteTask(taskId) {
-  return del(`/opc/tasks/${taskId}`);
-}
- 
-export async function moveTask(taskId, status) {
-  return put(`/opc/tasks/${taskId}/move`, { status });
-}
- 
-export async function assignTask(taskId, assigned_to, assigned_type = "human") {
-  return post(`/opc/tasks/${taskId}/assign`, { assigned_to, assigned_type });
-}
- 
-export async function getTaskHistory(taskId) {
-  return get(`/opc/tasks/${taskId}/history`);
-}
- 
-export async function getTaskTime(taskId) {
-  return get(`/opc/tasks/${taskId}/time`);
-}
- 
-// Agents
-export async function getAgents(enabledOnly = true) {
-  return get(`/opc/agents?enabled_only=${enabledOnly}`);
-}
- 
-export async function getAgent(agentId) {
-  return get(`/opc/agents/${agentId}`);
-}
- 
-export async function triggerAgent(agentId, taskId) {
-  return post(`/opc/agents/${agentId}/execute?task_id=${taskId}`);
-}
- 
-// Executions
-export async function getExecutions(filters = {}) {
-  const params = new URLSearchParams();
-  if (filters.task_id) params.append("task_id", filters.task_id);
-  if (filters.agent_id) params.append("agent_id", filters.agent_id);
-  if (filters.status) params.append("status", filters.status);
-  if (filters.limit) params.append("limit", filters.limit);
- 
-  const query = params.toString();
-  return get(`/opc/executions${query ? "?" + query : ""}`);
-}
- 
-export async function getExecution(executionId) {
-  return get(`/opc/executions/${executionId}`);
-}
- 
-export async function approveExecution(executionId, approved) {
-  return post(`/opc/executions/${executionId}/approve`, { approved });
-}
- -
-
- - - - - - - - \ No newline at end of file diff --git a/dashboard/frontend/coverage/lcov-report/src/lib/opc-ws.js.html b/dashboard/frontend/coverage/lcov-report/src/lib/opc-ws.js.html deleted file mode 100644 index fa89c0d..0000000 --- a/dashboard/frontend/coverage/lcov-report/src/lib/opc-ws.js.html +++ /dev/null @@ -1,352 +0,0 @@ - - - - - - Code coverage report for src/lib/opc-ws.js - - - - - - - - - -
-
-

All files / src/lib opc-ws.js

-
- -
- 0% - Statements - 0/75 -
- - -
- 0% - Branches - 0/1 -
- - -
- 0% - Functions - 0/1 -
- - -
- 0% - Lines - 0/75 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  - 
/**
- * OPC WebSocket Client - Real-time updates
- */
- 
-import { getToken } from "./api.js";
- 
-let ws = null;
-let reconnectTimer = null;
-let listeners = new Set();
- 
-export function connect() {
-  if (ws && ws.readyState === WebSocket.OPEN) {
-    return;
-  }
- 
-  const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
-  const token = getToken();
-  const tokenParam = token ? `?token=${encodeURIComponent(token)}` : "";
-  const wsUrl = `${protocol}//${window.location.host}/ws/opc${tokenParam}`;
- 
-  ws = new WebSocket(wsUrl);
- 
-  ws.onopen = () => {
-    console.log("OPC WebSocket connected");
-    if (reconnectTimer) {
-      clearTimeout(reconnectTimer);
-      reconnectTimer = null;
-    }
- 
-    // Send ping every 30 seconds to keep connection alive
-    const pingInterval = setInterval(() => {
-      if (ws && ws.readyState === WebSocket.OPEN) {
-        ws.send("ping");
-      } else {
-        clearInterval(pingInterval);
-      }
-    }, 30000);
-  };
- 
-  ws.onmessage = (event) => {
-    try {
-      const message = JSON.parse(event.data);
-      notifyListeners(message);
-    } catch (e) {
-      console.error("Failed to parse WebSocket message:", e);
-    }
-  };
- 
-  ws.onerror = (error) => {
-    console.error("WebSocket error:", error);
-  };
- 
-  ws.onclose = () => {
-    console.log("OPC WebSocket disconnected");
-    ws = null;
- 
-    // Reconnect after 5 seconds
-    reconnectTimer = setTimeout(() => {
-      console.log("Reconnecting WebSocket...");
-      connect();
-    }, 5000);
-  };
-}
- 
-export function disconnect() {
-  if (ws) {
-    ws.close();
-    ws = null;
-  }
-  if (reconnectTimer) {
-    clearTimeout(reconnectTimer);
-    reconnectTimer = null;
-  }
-}
- 
-export function subscribe(callback) {
-  listeners.add(callback);
-  return () => listeners.delete(callback);
-}
- 
-function notifyListeners(message) {
-  listeners.forEach((callback) => {
-    try {
-      callback(message);
-    } catch (e) {
-      console.error("Listener error:", e);
-    }
-  });
-}
- -
-
- - - - - - - - \ No newline at end of file diff --git a/dashboard/frontend/coverage/lcov-report/src/lib/voice.js.html b/dashboard/frontend/coverage/lcov-report/src/lib/voice.js.html deleted file mode 100644 index c7a8dc5..0000000 --- a/dashboard/frontend/coverage/lcov-report/src/lib/voice.js.html +++ /dev/null @@ -1,847 +0,0 @@ - - - - - - Code coverage report for src/lib/voice.js - - - - - - - - - -
-
-

All files / src/lib voice.js

-
- -
- 0% - Statements - 0/232 -
- - -
- 0% - Branches - 0/1 -
- - -
- 0% - Functions - 0/1 -
- - -
- 0% - Lines - 0/232 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -178 -179 -180 -181 -182 -183 -184 -185 -186 -187 -188 -189 -190 -191 -192 -193 -194 -195 -196 -197 -198 -199 -200 -201 -202 -203 -204 -205 -206 -207 -208 -209 -210 -211 -212 -213 -214 -215 -216 -217 -218 -219 -220 -221 -222 -223 -224 -225 -226 -227 -228 -229 -230 -231 -232 -233 -234 -235 -236 -237 -238 -239 -240 -241 -242 -243 -244 -245 -246 -247 -248 -249 -250 -251 -252 -253 -254 -255  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  - 
const LANGS = ["en-US", "zh-CN", "zh-HK"];
-const LANG_LABELS = ["EN", "普", "粤"];
-const TALK_MODES = ["auto", "tap", "hold"];
-const MODE_LABELS = { auto: "Auto", tap: "Tap", hold: "Hold" };
-const NOISE = /^[\s│┃┆┇┊┋─━┄┅┈┉═╌╍╔╗╚╝╠╣╦╩╬├┤┬┴┼╭╮╯╰░▒▓█▌▐▀▄⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏◐◑◒◓⣾⣽⣻⢿⡿⣟⣯⣷⠁⠂⠄⡀⢀⠠⠐⠈\-–—_=+*#~<>/.\\|,;:!?(){}\[\]]+$/;
-const SKIP = /shortcuts|\/help|\/exit|press esc|ctrl[+-]|⌘|⎋|❯|^\$\s|^root@|^claude|^jimmy|^\[.*\]$/i;
-const MODE_COMMANDS = {
-  "hold mode": "hold", "tap mode": "tap", "auto mode": "auto",
-  "按住模式": "hold", "点击模式": "tap", "自动模式": "auto",
-};
- 
-export class VoiceSession {
-  constructor(onChange) {
-    this.onChange = onChange;
-    this.ttsEnabled = false;
-    this.speaking = false;
-    this.listening = false;
-    this.sttAvailable = !!(window.SpeechRecognition || window.webkitSpeechRecognition);
-    this.langIdx = 0;
-    this.voiceMode = false;
-    this.transcript = "";
-    this.talkMode = "auto";
-    this.voices = [];
-    this.voiceIdx = -1;
-    this.term = null;
-    this.ws = null;
-    this._lastLine = 0;
-    this._debounce = null;
-    this._recog = null;
-    this._onWriteDispose = null;
-    this._ttsUnlocked = false;
-    this._silenceTimer = null;
-  }
- 
-  get lang() { return LANGS[this.langIdx]; }
-  get langLabel() { return LANG_LABELS[this.langIdx]; }
-  get modeLabel() { return MODE_LABELS[this.talkMode]; }
-  get voiceName() { return this.voices[this.voiceIdx]?.name || "Default"; }
- 
-  loadVoices() {
-    const all = speechSynthesis.getVoices();
-    const prefix = this.lang.split("-")[0];
-    this.voices = all.filter(v => v.lang.startsWith(prefix));
-    // Try to keep current selection
-    if (this.voiceIdx >= this.voices.length) this.voiceIdx = 0;
-    if (!this.voices.length) this.voiceIdx = -1;
-    this._notify();
-  }
- 
-  setVoice(idx) {
-    this.voiceIdx = idx;
-    this._notify();
-  }
- 
-  attach(term, ws) {
-    this.term = term;
-    this.ws = ws;
-    this._lastLine = term.buffer.active.cursorY + term.buffer.active.baseY;
-    this._onWriteDispose = term.onWriteParsed(() => this._onOutput());
-  }
- 
-  _notify() { this.onChange?.(); }
- 
-  // --- Voice Mode ---
-  enterVoiceMode() {
-    this.voiceMode = true;
-    this.ttsEnabled = true;
-    if (!this._ttsUnlocked) {
-      speechSynthesis.speak(new SpeechSynthesisUtterance(""));
-      this._ttsUnlocked = true;
-    }
-    this.loadVoices();
-    speechSynthesis.onvoiceschanged = () => this.loadVoices();
-    if (this.term) {
-      this._lastLine = this.term.buffer.active.baseY + this.term.buffer.active.cursorY;
-    }
-    if (this.talkMode === "auto") this._startSTT(true);
-    this._notify();
-  }
- 
-  exitVoiceMode() {
-    this.voiceMode = false;
-    this.ttsEnabled = false;
-    this.transcript = "";
-    speechSynthesis.cancel();
-    this.speaking = false;
-    this._stopSTT();
-    this._notify();
-  }
- 
-  setTalkMode(mode) {
-    this.talkMode = mode;
-    this._stopSTT();
-    this.transcript = "";
-    if (mode === "auto" && this.voiceMode) this._startSTT(true);
-    this._notify();
-  }
- 
-  cycleTalkMode() {
-    const i = (TALK_MODES.indexOf(this.talkMode) + 1) % TALK_MODES.length;
-    this.setTalkMode(TALK_MODES[i]);
-  }
- 
-  // Hold mode
-  startHold() {
-    if (this.talkMode !== "hold") return;
-    this._startSTT(false);
-  }
- 
-  releaseHold() {
-    if (this.talkMode !== "hold") return;
-    this._sendAndStop();
-  }
- 
-  // Tap mode
-  tapMic() {
-    if (this.talkMode !== "tap") return;
-    if (this.listening) { this._sendAndStop(); }
-    else { this._startSTT(false); }
-  }
- 
-  _sendAndStop() {
-    const text = this.transcript.trim();
-    this._stopSTT();
-    if (text && this.ws?.readyState === 1) this._typeText(text + "\r");
-    this.transcript = "";
-    this._notify();
-  }
- 
-  // --- STT ---
-  _startSTT(continuous) {
-    const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
-    if (!SR) return;
-    speechSynthesis.cancel();
-    this.speaking = false;
-    this._recog = new SR();
-    this._recog.continuous = continuous;
-    this._recog.interimResults = true;
-    this._recog.lang = this.lang;
-    this._finalText = "";
-    this._recog.onresult = (e) => {
-      let interim = "";
-      for (let i = e.resultIndex; i < e.results.length; i++) {
-        const t = e.results[i][0].transcript;
-        if (e.results[i].isFinal) {
-          // Check voice commands
-          const cmd = MODE_COMMANDS[t.trim().toLowerCase()];
-          if (cmd) { this.setTalkMode(cmd); return; }
-          this._finalText += t;
-        } else { interim = t; }
-      }
-      this.transcript = (this._finalText + interim).trim();
-      this._notify();
-      // Auto mode: reset silence timer on new results
-      if (this.talkMode === "auto" && this._finalText) {
-        clearTimeout(this._silenceTimer);
-        this._silenceTimer = setTimeout(() => this._autoSend(), 2000);
-      }
-    };
-    this._recog.onerror = () => { this.listening = false; this._notify(); };
-    this._recog.onend = () => {
-      if (this.listening && continuous) {
-        // Auto mode: send accumulated text, restart
-        if (this.talkMode === "auto" && this._finalText.trim()) this._autoSend();
-        try { this._recog.start(); } catch { this.listening = false; this._notify(); }
-      } else { this.listening = false; this._notify(); }
-    };
-    this._recog.start();
-    this.listening = true;
-    this._notify();
-  }
- 
-  _stopSTT() {
-    this.listening = false;
-    clearTimeout(this._silenceTimer);
-    this._recog?.stop();
-    this._recog = null;
-    this._finalText = "";
-  }
- 
-  _autoSend() {
-    clearTimeout(this._silenceTimer);
-    const text = this._finalText.trim();
-    this._finalText = "";
-    this.transcript = "";
-    if (text && this.ws?.readyState === 1) this._typeText(text + "\r");
-    this._notify();
-  }
- 
-  // --- TTS ---
-  _onOutput() {
-    if (!this.ttsEnabled || this.transcript) return;
-    clearTimeout(this._debounce);
-    this._debounce = setTimeout(() => this._readBuffer(), 2000);
-  }
- 
-  _readBuffer() {
-    const buf = this.term?.buffer?.active;
-    if (!buf || buf.type !== "normal") return;
-    const curLine = buf.baseY + buf.cursorY;
-    const lines = [];
-    for (let y = this._lastLine + 1; y <= curLine; y++) {
-      const row = buf.getLine(y);
-      if (!row) continue;
-      const text = row.translateToString(true).trim();
-      if (text.length >= 4 && !NOISE.test(text) && !SKIP.test(text)) lines.push(text);
-    }
-    this._lastLine = curLine;
-    if (lines.length) this._speak(lines.join(". "));
-  }
- 
-  _speak(text) {
-    if (!text) return;
-    speechSynthesis.cancel();
-    const u = new SpeechSynthesisUtterance(text);
-    u.lang = this.lang;
-    if (this.voices[this.voiceIdx]) u.voice = this.voices[this.voiceIdx];
-    u.onstart = () => { this.speaking = true; this._notify(); };
-    u.onend = () => { this.speaking = false; this._notify(); };
-    u.onerror = () => { this.speaking = false; this._notify(); };
-    speechSynthesis.speak(u);
-  }
- 
-  _typeText(text) {
-    let i = 0;
-    const next = () => {
-      if (i < text.length && this.ws?.readyState === 1) {
-        this.ws.send(new TextEncoder().encode(text[i++]));
-        setTimeout(next, 20);
-      }
-    };
-    next();
-  }
- 
-  cycleLang() {
-    this.langIdx = (this.langIdx + 1) % LANGS.length;
-    this.loadVoices();
-    if (this.listening) {
-      const wasContinuous = this._recog?.continuous;
-      this._stopSTT();
-      this._startSTT(!!wasContinuous);
-    }
-    this._notify();
-  }
- 
-  destroy() {
-    clearTimeout(this._debounce);
-    clearTimeout(this._silenceTimer);
-    this._onWriteDispose?.dispose();
-    this._stopSTT();
-    speechSynthesis.cancel();
-    this.term = null;
-    this.ws = null;
-  }
-}
- -
-
- - - - - - - - \ No newline at end of file diff --git a/dashboard/frontend/coverage/lcov-report/src/main.js.html b/dashboard/frontend/coverage/lcov-report/src/main.js.html deleted file mode 100644 index c6b8749..0000000 --- a/dashboard/frontend/coverage/lcov-report/src/main.js.html +++ /dev/null @@ -1,103 +0,0 @@ - - - - - - Code coverage report for src/main.js - - - - - - - - - -
-
-

All files / src main.js

-
- -
- 0% - Statements - 0/5 -
- - -
- 0% - Branches - 0/1 -
- - -
- 0% - Functions - 0/1 -
- - -
- 0% - Lines - 0/5 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7  -  -  -  -  -  - 
import "./app.css";
-import App from "./App.svelte";
-import { mount } from "svelte";
- 
-const app = mount(App, { target: document.getElementById("app") });
-export default app;
- -
-
- - - - - - - - \ No newline at end of file diff --git a/dashboard/frontend/coverage/lcov-report/src/routes/CcConnect.svelte.html b/dashboard/frontend/coverage/lcov-report/src/routes/CcConnect.svelte.html deleted file mode 100644 index b7914f3..0000000 --- a/dashboard/frontend/coverage/lcov-report/src/routes/CcConnect.svelte.html +++ /dev/null @@ -1,550 +0,0 @@ - - - - - - Code coverage report for src/routes/CcConnect.svelte - - - - - - - - - -
-
-

All files / src/routes CcConnect.svelte

-
- -
- 0% - Statements - 0/87 -
- - -
- 0% - Branches - 0/1 -
- - -
- 0% - Functions - 0/1 -
- - -
- 0% - Lines - 0/87 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  - 
<script>
-  import { onMount } from "svelte";
-  import { getCcConnectHealth, startCcConnect, stopCcConnect } from "../lib/api.js";
- 
-  let loading = $state(true);
-  let health = $state(null);
-  let requestError = $state("");
-  let actionLoading = $state("");
- 
-  async function loadHealth() {
-    loading = true;
-    requestError = "";
-    try {
-      health = await getCcConnectHealth();
-    } catch (e) {
-      requestError = e?.body?.detail || e?.message || "Failed to load cc-connect health";
-      health = null;
-    } finally {
-      loading = false;
-    }
-  }
- 
-  async function runAction(action) {
-    actionLoading = action;
-    requestError = "";
-    try {
-      const result = action === "start" ? await startCcConnect() : await stopCcConnect();
-      if (!result?.ok) {
-        requestError = result?.error || `Failed to ${action} cc-connect`;
-      }
-    } catch (e) {
-      requestError = e?.body?.detail || e?.message || `Failed to ${action} cc-connect`;
-    } finally {
-      actionLoading = "";
-      await loadHealth();
-    }
-  }
- 
-  function statusBadgeClass(status) {
-    if (status === "up") {
-      return "bg-emerald-100 text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-300";
-    }
-    return "bg-rose-100 text-rose-700 dark:bg-rose-900/40 dark:text-rose-300";
-  }
- 
-  function valueOrDash(value) {
-    return value === null || value === undefined || value === "" ? "—" : value;
-  }
- 
-  onMount(loadHealth);
-</script>
- 
-<div class="space-y-6">
-  <div class="flex items-center justify-between">
-    <div>
-      <h1 class="text-2xl font-bold text-surface-900 dark:text-white tracking-tight">cc-connect</h1>
-      <p class="text-sm text-surface-400 mt-1">Mobile bridge operational status</p>
-      <div class="mt-2 flex flex-wrap items-center gap-2 text-[11px] font-medium">
-        <span class="px-2 py-0.5 rounded-full bg-emerald-100 text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-300">up</span>
-        <span class="px-2 py-0.5 rounded-full bg-rose-100 text-rose-700 dark:bg-rose-900/40 dark:text-rose-300">down</span>
-      </div>
-    </div>
-    <div class="flex items-center gap-2">
-      {#if health?.container_status === "running"}
-        <button
-          onclick={() => runAction("stop")}
-          disabled={loading || actionLoading === "stop"}
-          class="px-3 py-1.5 text-xs font-medium text-rose-600 bg-rose-50 hover:bg-rose-100 rounded-lg transition-colors disabled:opacity-50"
-        >
-          {actionLoading === "stop" ? "Stopping..." : "Stop"}
-        </button>
-      {:else}
-        <button
-          onclick={() => runAction("start")}
-          disabled={loading || actionLoading === "start"}
-          class="px-3 py-1.5 text-xs font-medium text-emerald-600 bg-emerald-50 hover:bg-emerald-100 rounded-lg transition-colors disabled:opacity-50"
-        >
-          {actionLoading === "start" ? "Starting..." : "Start"}
-        </button>
-      {/if}
-      <button onclick={loadHealth} disabled={loading || Boolean(actionLoading)} class="px-3 py-1.5 text-xs font-medium text-primary-600 bg-primary-50 hover:bg-primary-100 rounded-lg transition-colors disabled:opacity-50 dark:bg-primary-900/30 dark:text-primary-300">
-        {loading ? "Loading..." : "Refresh"}
-      </button>
-    </div>
-  </div>
- 
-  {#if requestError}
-    <div class="bg-rose-50 dark:bg-rose-900/20 border border-rose-200 dark:border-rose-700 rounded-xl p-4 shadow-sm">
-      <p class="text-sm font-medium text-rose-700 dark:text-rose-300">cc-connect action failed</p>
-      <p class="text-xs text-rose-600 dark:text-rose-400 mt-1">{requestError}</p>
-    </div>
-  {/if}
- 
-  <div class="bg-white dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 p-5 shadow-sm">
-    {#if loading && !health}
-      <div class="space-y-3">
-        <div class="h-6 w-36 rounded bg-surface-100 dark:bg-surface-700 animate-pulse"></div>
-        <div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
-          {#each Array(8) as _}
-            <div class="h-16 rounded-lg bg-surface-100 dark:bg-surface-700 animate-pulse"></div>
-          {/each}
-        </div>
-      </div>
-    {:else if health}
-      <div class="flex items-center justify-between mb-4">
-        <h3 class="text-sm font-semibold text-surface-700 dark:text-surface-200">Health Status</h3>
-        <span class="px-2.5 py-1 text-xs font-semibold rounded-full {statusBadgeClass(health.status)}">{valueOrDash(health.status)}</span>
-      </div>
- 
-      <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
-        <div class="rounded-lg border border-surface-200 dark:border-surface-700 p-3">
-          <p class="text-xs font-medium text-surface-400 uppercase tracking-wider">OK</p>
-          <p class="text-sm font-semibold text-surface-800 dark:text-surface-100 mt-1">{String(Boolean(health.ok))}</p>
-        </div>
- 
-        <div class="rounded-lg border border-surface-200 dark:border-surface-700 p-3">
-          <p class="text-xs font-medium text-surface-400 uppercase tracking-wider">Source</p>
-          <p class="text-sm font-semibold text-surface-800 dark:text-surface-100 mt-1">{valueOrDash(health.source)}</p>
-        </div>
- 
-        <div class="rounded-lg border border-surface-200 dark:border-surface-700 p-3">
-          <p class="text-xs font-medium text-surface-400 uppercase tracking-wider">Container</p>
-          <p class="text-sm font-semibold text-surface-800 dark:text-surface-100 mt-1">{valueOrDash(health.container_name)}</p>
-        </div>
- 
-        <div class="rounded-lg border border-surface-200 dark:border-surface-700 p-3">
-          <p class="text-xs font-medium text-surface-400 uppercase tracking-wider">Container Status</p>
-          <p class="text-sm font-semibold text-surface-800 dark:text-surface-100 mt-1">{valueOrDash(health.container_status)}</p>
-        </div>
- 
-        <div class="rounded-lg border border-surface-200 dark:border-surface-700 p-3">
-          <p class="text-xs font-medium text-surface-400 uppercase tracking-wider">Endpoint</p>
-          <p class="text-sm font-semibold text-surface-800 dark:text-surface-100 mt-1 break-all">{valueOrDash(health.endpoint)}</p>
-        </div>
- 
-        <div class="rounded-lg border border-surface-200 dark:border-surface-700 p-3">
-          <p class="text-xs font-medium text-surface-400 uppercase tracking-wider">HTTP Status</p>
-          <p class="text-sm font-semibold text-surface-800 dark:text-surface-100 mt-1">{valueOrDash(health.http_status)}</p>
-        </div>
- 
-        <div class="rounded-lg border border-surface-200 dark:border-surface-700 p-3">
-          <p class="text-xs font-medium text-surface-400 uppercase tracking-wider">Latency</p>
-          <p class="text-sm font-semibold text-surface-800 dark:text-surface-100 mt-1">{health.latency_ms === null || health.latency_ms === undefined ? "—" : `${health.latency_ms} ms`}</p>
-        </div>
- 
-        <div class="rounded-lg border border-surface-200 dark:border-surface-700 p-3 sm:col-span-2 lg:col-span-2">
-          <p class="text-xs font-medium text-surface-400 uppercase tracking-wider">Error</p>
-          <p class="text-sm font-semibold text-surface-800 dark:text-surface-100 mt-1 break-all">{valueOrDash(health.error)}</p>
-        </div>
-      </div>
-    {:else}
-      <p class="text-sm text-surface-400">No health data available.</p>
-    {/if}
-  </div>
-</div>
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/dashboard/frontend/coverage/lcov-report/src/routes/ChatSummary.svelte.html b/dashboard/frontend/coverage/lcov-report/src/routes/ChatSummary.svelte.html deleted file mode 100644 index 6b02e82..0000000 --- a/dashboard/frontend/coverage/lcov-report/src/routes/ChatSummary.svelte.html +++ /dev/null @@ -1,454 +0,0 @@ - - - - - - Code coverage report for src/routes/ChatSummary.svelte - - - - - - - - - -
-
-

All files / src/routes ChatSummary.svelte

-
- -
- 0% - Statements - 0/91 -
- - -
- 0% - Branches - 0/1 -
- - -
- 0% - Functions - 0/1 -
- - -
- 0% - Lines - 0/91 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  - 
<script>
-  import { onMount } from "svelte";
-  import { get, post } from "../lib/api.js";
- 
-  let dates = $state([]);
-  let selectedDate = $state("");
-  let summary = $state(null);
-  let messages = $state([]);
-  let showMessages = $state(false);
-  let loading = $state(false);
-  let triggering = $state(false);
-  let error = $state("");
- 
-  onMount(async () => {
-    try {
-      dates = await get("/api/chat-summary/dates");
-      if (dates.length) selectDate(dates[0]);
-    } catch (e) {
-      error = e.body?.detail || "Failed to load dates";
-    }
-  });
- 
-  async function selectDate(d) {
-    selectedDate = d;
-    showMessages = false;
-    loading = true;
-    error = "";
-    try {
-      summary = await get(`/api/chat-summary/summary/${d}`);
-    } catch (e) {
-      summary = null;
-      if (e.status !== 404) error = e.body?.detail || "Failed to load summary";
-    } finally {
-      loading = false;
-    }
-  }
- 
-  async function loadMessages() {
-    if (showMessages) { showMessages = false; return; }
-    try {
-      messages = await get(`/api/chat-summary/messages/${selectedDate}`);
-      showMessages = true;
-    } catch (e) {
-      error = "Failed to load messages";
-    }
-  }
- 
-  async function trigger() {
-    triggering = true;
-    try {
-      await post("/api/chat-summary/trigger");
-    } catch (e) {
-      error = "Trigger failed";
-    } finally {
-      triggering = false;
-    }
-  }
- 
-  function markdownToHtml(md) {
-    if (!md) return "";
-    return md
-      .replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")
-      .replace(/^### (.+)$/gm, '<h3 class="text-lg font-semibold mt-4 mb-2">$1</h3>')
-      .replace(/^## (.+)$/gm, '<h2 class="text-xl font-bold mt-5 mb-2">$1</h2>')
-      .replace(/^# (.+)$/gm, '<h1 class="text-2xl font-bold mt-6 mb-3">$1</h1>')
-      .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
-      .replace(/\*(.+?)\*/g, '<em>$1</em>')
-      .replace(/^- (.+)$/gm, '<li class="ml-4">$1</li>')
-      .replace(/\n{2,}/g, '<br><br>')
-      .replace(/\n/g, '<br>');
-  }
-</script>
- 
-<div>
-  <div class="flex items-center justify-between mb-6">
-    <h2 class="text-xl font-bold text-surface-800 dark:text-surface-100">Chat Digest</h2>
-    <button onclick={trigger} disabled={triggering}
-      class="px-3 py-1.5 text-sm font-medium rounded-lg bg-primary-600 text-white hover:bg-primary-700 disabled:opacity-50">
-      {triggering ? "Generating..." : "Generate Now"}
-    </button>
-  </div>
- 
-  {#if error}
-    <div class="mb-4 p-3 rounded-lg bg-red-50 text-red-700 text-sm dark:bg-red-900/20 dark:text-red-400">{error}</div>
-  {/if}
- 
-  <div class="flex gap-2 mb-6 flex-wrap">
-    {#each dates as d}
-      <button onclick={() => selectDate(d)}
-        class="px-3 py-1.5 text-sm rounded-lg transition-colors {selectedDate === d ? 'bg-primary-600 text-white' : 'bg-surface-100 text-surface-600 hover:bg-surface-200 dark:bg-surface-700 dark:text-surface-300 dark:hover:bg-surface-600'}">
-        {d}
-      </button>
-    {/each}
-    {#if !dates.length && !error}
-      <p class="text-sm text-surface-400">No summaries yet.</p>
-    {/if}
-  </div>
- 
-  {#if loading}
-    <div class="flex justify-center py-12"><div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600"></div></div>
-  {:else if summary}
-    <div class="prose dark:prose-invert max-w-none bg-white dark:bg-surface-800 rounded-xl p-6 shadow-sm border border-surface-200 dark:border-surface-700">
-      {@html markdownToHtml(summary.content)}
-    </div>
- 
-    <button onclick={loadMessages}
-      class="mt-4 text-sm text-primary-600 hover:text-primary-700 dark:text-primary-400">
-      {showMessages ? "Hide raw messages" : "Show raw messages"}
-    </button>
- 
-    {#if showMessages}
-      <div class="mt-3 bg-white dark:bg-surface-800 rounded-xl p-4 shadow-sm border border-surface-200 dark:border-surface-700 max-h-96 overflow-y-auto">
-        {#each messages as m}
-          <div class="py-1.5 border-b border-surface-100 dark:border-surface-700 last:border-0 text-sm">
-            <span class="text-surface-400 text-xs">{m.time?.slice(11,16)}</span>
-            <span class="font-medium text-primary-600 dark:text-primary-400 ml-1">[{m.group}] {m.sender}:</span>
-            <span class="text-surface-700 dark:text-surface-300 ml-1">{m.text}</span>
-          </div>
-        {/each}
-      </div>
-    {/if}
-  {/if}
-</div>
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/dashboard/frontend/coverage/lcov-report/src/routes/Conversations.svelte.html b/dashboard/frontend/coverage/lcov-report/src/routes/Conversations.svelte.html deleted file mode 100644 index 91b9cc8..0000000 --- a/dashboard/frontend/coverage/lcov-report/src/routes/Conversations.svelte.html +++ /dev/null @@ -1,1003 +0,0 @@ - - - - - - Code coverage report for src/routes/Conversations.svelte - - - - - - - - - -
-
-

All files / src/routes Conversations.svelte

-
- -
- 0% - Statements - 0/201 -
- - -
- 0% - Branches - 0/1 -
- - -
- 0% - Functions - 0/1 -
- - -
- 0% - Lines - 0/201 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -178 -179 -180 -181 -182 -183 -184 -185 -186 -187 -188 -189 -190 -191 -192 -193 -194 -195 -196 -197 -198 -199 -200 -201 -202 -203 -204 -205 -206 -207 -208 -209 -210 -211 -212 -213 -214 -215 -216 -217 -218 -219 -220 -221 -222 -223 -224 -225 -226 -227 -228 -229 -230 -231 -232 -233 -234 -235 -236 -237 -238 -239 -240 -241 -242 -243 -244 -245 -246 -247 -248 -249 -250 -251 -252 -253 -254 -255 -256 -257 -258 -259 -260 -261 -262 -263 -264 -265 -266 -267 -268 -269 -270 -271 -272 -273 -274 -275 -276 -277 -278 -279 -280 -281 -282 -283 -284 -285 -286 -287 -288 -289 -290 -291 -292 -293 -294 -295 -296 -297 -298 -299 -300 -301 -302 -303 -304 -305 -306 -307  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  - 
<script>
-  import { onMount } from "svelte";
-  import { get, post } from "../lib/api.js";
- 
-  let dates = $state([]);
-  let selectedDate = $state("");
-  let summary = $state(null);
-  let conversations = $state([]);
-  let selectedConversation = $state(null);
-  let messages = $state([]);
-  let searchQuery = $state("");
-  let searchResults = $state([]);
-  let stats = $state(null);
-  let loading = $state(false);
-  let triggering = $state(false);
-  let error = $state("");
-  let view = $state("summary"); // summary, list, detail, search, stats
- 
-  onMount(async () => {
-    try {
-      dates = await get("/conversations/dates");
-      if (dates.length) selectDate(dates[0]);
-      loadStats();
-    } catch (e) {
-      error = e.body?.detail || "Failed to load dates";
-    }
-  });
- 
-  async function selectDate(d) {
-    selectedDate = d;
-    view = "summary";
-    loading = true;
-    error = "";
-    try {
-      summary = await get(`/conversations/summary/${d}`);
-      conversations = await get(`/conversations/list?date=${d}`);
-    } catch (e) {
-      summary = null;
-      if (e.status !== 404) error = e.body?.detail || "Failed to load summary";
-    } finally {
-      loading = false;
-    }
-  }
- 
-  async function selectConversation(sessionId) {
-    view = "detail";
-    loading = true;
-    try {
-      selectedConversation = await get(`/conversations/${sessionId}`);
-      messages = await get(`/conversations/${sessionId}/messages`);
-    } catch (e) {
-      error = "Failed to load conversation";
-    } finally {
-      loading = false;
-    }
-  }
- 
-  async function search() {
-    if (!searchQuery || searchQuery.length < 2) return;
-    view = "search";
-    loading = true;
-    try {
-      searchResults = await get(`/conversations/search?q=${encodeURIComponent(searchQuery)}`);
-    } catch (e) {
-      error = "Search failed";
-    } finally {
-      loading = false;
-    }
-  }
- 
-  async function loadStats() {
-    try {
-      stats = await get("/conversations/stats");
-    } catch (e) {
-      console.error("Failed to load stats", e);
-    }
-  }
- 
-  async function trigger() {
-    triggering = true;
-    try {
-      await post("/conversations/trigger");
-      setTimeout(() => selectDate(selectedDate), 5000);
-    } catch (e) {
-      error = "Trigger failed";
-    } finally {
-      triggering = false;
-    }
-  }
- 
-  function markdownToHtml(md) {
-    if (!md) return "";
-    return md
-      .replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")
-      .replace(/^### (.+)$/gm, '<h3 class="text-lg font-semibold mt-4 mb-2">$1</h3>')
-      .replace(/^## (.+)$/gm, '<h2 class="text-xl font-bold mt-5 mb-2">$1</h2>')
-      .replace(/^# (.+)$/gm, '<h1 class="text-2xl font-bold mt-6 mb-3">$1</h1>')
-      .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
-      .replace(/\*(.+?)\*/g, '<em>$1</em>')
-      .replace(/^- (.+)$/gm, '<li class="ml-4">$1</li>')
-      .replace(/\n{2,}/g, '<br><br>')
-      .replace(/\n/g, '<br>');
-  }
- 
-  function formatTokens(n) {
-    return n ? n.toLocaleString() : "0";
-  }
-</script>
- 
-<div>
-  <div class="flex items-center justify-between mb-6">
-    <h2 class="text-xl font-bold text-surface-800 dark:text-surface-100">Code Sessions</h2>
-    <div class="flex gap-2">
-      <button onclick={() => view = "stats"} class="px-3 py-1.5 text-sm font-medium rounded-lg bg-surface-100 hover:bg-surface-200 dark:bg-surface-700 dark:hover:bg-surface-600 transition-colors">
-        Stats
-      </button>
-      <button onclick={trigger} disabled={triggering}
-        class="px-3 py-1.5 text-sm font-medium rounded-lg bg-primary-600 text-white hover:bg-primary-700 disabled:opacity-50">
-        {triggering ? "Generating..." : "Generate Summary"}
-      </button>
-    </div>
-  </div>
- 
-  {#if error}
-    <div class="mb-4 p-3 rounded-lg bg-red-50 text-red-700 text-sm dark:bg-red-900/20 dark:text-red-400">{error}</div>
-  {/if}
- 
-  <!-- Search Bar -->
-  <div class="mb-4">
-    <input
-      type="text"
-      bind:value={searchQuery}
-      onkeydown={(e) => e.key === "Enter" && search()}
-      placeholder="Search conversations..."
-      class="w-full px-4 py-2 text-sm border border-surface-200 dark:border-surface-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 dark:bg-surface-700 dark:text-white"
-    />
-  </div>
- 
-  <!-- Date Selector -->
-  {#if view !== "search" && view !== "stats"}
-    <div class="flex gap-2 mb-6 flex-wrap">
-      {#each dates as d}
-        <button onclick={() => selectDate(d)}
-          class="px-3 py-1.5 text-sm rounded-lg transition-colors {selectedDate === d ? 'bg-primary-600 text-white' : 'bg-surface-100 text-surface-600 hover:bg-surface-200 dark:bg-surface-700 dark:text-surface-300 dark:hover:bg-surface-600'}">
-          {d}
-        </button>
-      {/each}
-      {#if !dates.length && !error}
-        <p class="text-sm text-surface-400">No conversations yet.</p>
-      {/if}
-    </div>
-  {/if}
- 
-  {#if loading}
-    <div class="flex justify-center py-12"><div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600"></div></div>
-  {:else if view === "summary" && summary}
-    <!-- Summary View -->
-    <div class="mb-4 grid grid-cols-4 gap-3">
-      <div class="bg-white dark:bg-surface-800 rounded-lg p-3 border border-surface-200 dark:border-surface-700">
-        <div class="text-xs text-surface-400">Conversations</div>
-        <div class="text-xl font-bold text-surface-900 dark:text-white">{summary.conversation_count}</div>
-      </div>
-      <div class="bg-white dark:bg-surface-800 rounded-lg p-3 border border-surface-200 dark:border-surface-700">
-        <div class="text-xs text-surface-400">Messages</div>
-        <div class="text-xl font-bold text-surface-900 dark:text-white">{summary.total_messages}</div>
-      </div>
-      <div class="bg-white dark:bg-surface-800 rounded-lg p-3 border border-surface-200 dark:border-surface-700">
-        <div class="text-xs text-surface-400">Tokens</div>
-        <div class="text-xl font-bold text-surface-900 dark:text-white">{formatTokens(summary.total_tokens)}</div>
-      </div>
-      <div class="bg-white dark:bg-surface-800 rounded-lg p-3 border border-surface-200 dark:border-surface-700">
-        <div class="text-xs text-surface-400">Projects</div>
-        <div class="text-sm font-medium text-surface-900 dark:text-white truncate">{summary.projects?.split(',')[0] || 'N/A'}</div>
-      </div>
-    </div>
- 
-    <div class="prose dark:prose-invert max-w-none bg-white dark:bg-surface-800 rounded-xl p-6 shadow-sm border border-surface-200 dark:border-surface-700 mb-4">
-      {@html markdownToHtml(summary.content)}
-    </div>
- 
-    <!-- Conversation List -->
-    <div class="bg-white dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 shadow-sm overflow-hidden">
-      <div class="px-4 py-2 text-xs font-semibold uppercase tracking-wider text-surface-400 border-b border-surface-100 dark:border-surface-700">
-        Conversations ({conversations.length})
-      </div>
-      {#each conversations as conv}
-        <button
-          onclick={() => selectConversation(conv.session_id)}
-          class="w-full px-4 py-3 hover:bg-surface-50 dark:hover:bg-surface-700 border-b border-surface-100 dark:border-surface-700 last:border-0 transition-colors text-left">
-          <div class="flex items-center justify-between">
-            <div class="flex-1 min-w-0">
-              <div class="text-sm font-medium text-surface-700 dark:text-surface-200 truncate">{conv.slug || 'Untitled'}</div>
-              <div class="text-xs text-surface-400 truncate">{conv.project_path?.split('/').pop() || 'unknown'}</div>
-            </div>
-            <div class="flex items-center gap-3 text-xs text-surface-400">
-              <span>{conv.message_count} msgs</span>
-              <span>{formatTokens(conv.total_tokens)} tokens</span>
-            </div>
-          </div>
-        </button>
-      {/each}
-    </div>
- 
-  {:else if view === "detail" && selectedConversation}
-    <!-- Conversation Detail -->
-    <button onclick={() => view = "summary"} class="mb-4 text-sm text-primary-600 hover:text-primary-700">← Back to summary</button>
- 
-    <div class="bg-white dark:bg-surface-800 rounded-xl p-4 border border-surface-200 dark:border-surface-700 mb-4">
-      <h3 class="text-lg font-bold text-surface-900 dark:text-white mb-2">{selectedConversation.slug || 'Untitled'}</h3>
-      <div class="grid grid-cols-2 gap-2 text-sm">
-        <div><span class="text-surface-400">Project:</span> <span class="text-surface-700 dark:text-surface-300">{selectedConversation.project_path?.split('/').pop()}</span></div>
-        <div><span class="text-surface-400">Branch:</span> <span class="text-surface-700 dark:text-surface-300">{selectedConversation.git_branch || 'N/A'}</span></div>
-        <div><span class="text-surface-400">Messages:</span> <span class="text-surface-700 dark:text-surface-300">{selectedConversation.message_count}</span></div>
-        <div><span class="text-surface-400">Tokens:</span> <span class="text-surface-700 dark:text-surface-300">{formatTokens(selectedConversation.total_input_tokens + selectedConversation.total_output_tokens)}</span></div>
-      </div>
-    </div>
- 
-    <!-- Messages -->
-    <div class="space-y-3">
-      {#each messages as msg}
-        <div class="bg-white dark:bg-surface-800 rounded-lg p-4 border border-surface-200 dark:border-surface-700">
-          <div class="flex items-center justify-between mb-2">
-            <span class="text-xs font-semibold uppercase {msg.role === 'user' ? 'text-blue-600' : 'text-green-600'}">{msg.role}</span>
-            <span class="text-xs text-surface-400">{msg.timestamp?.slice(11, 19)}</span>
-          </div>
-          <div class="text-sm text-surface-700 dark:text-surface-300 whitespace-pre-wrap">{msg.content?.slice(0, 500)}{msg.content?.length > 500 ? '...' : ''}</div>
-          {#if msg.tool_calls?.length}
-            <div class="mt-2 pt-2 border-t border-surface-100 dark:border-surface-700">
-              <div class="text-xs font-medium text-surface-400 mb-1">Tools: {msg.tool_calls.map(t => t.name).join(', ')}</div>
-            </div>
-          {/if}
-          {#if msg.input_tokens || msg.output_tokens}
-            <div class="mt-2 text-xs text-surface-400">
-              {msg.input_tokens ? `${msg.input_tokens} in` : ''} {msg.output_tokens ? `${msg.output_tokens} out` : ''}
-            </div>
-          {/if}
-        </div>
-      {/each}
-    </div>
- 
-  {:else if view === "search"}
-    <!-- Search Results -->
-    <button onclick={() => view = "summary"} class="mb-4 text-sm text-primary-600 hover:text-primary-700">← Back</button>
-    <div class="bg-white dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 shadow-sm overflow-hidden">
-      <div class="px-4 py-2 text-xs font-semibold uppercase tracking-wider text-surface-400 border-b border-surface-100 dark:border-surface-700">
-        Search Results ({searchResults.length})
-      </div>
-      {#each searchResults as conv}
-        <button
-          onclick={() => selectConversation(conv.session_id)}
-          class="w-full px-4 py-3 hover:bg-surface-50 dark:hover:bg-surface-700 border-b border-surface-100 dark:border-surface-700 last:border-0 transition-colors text-left">
-          <div class="flex items-center justify-between">
-            <div class="flex-1 min-w-0">
-              <div class="text-sm font-medium text-surface-700 dark:text-surface-200 truncate">{conv.slug || 'Untitled'}</div>
-              <div class="text-xs text-surface-400">{conv.started_at?.slice(0, 10)} • {conv.project_path?.split('/').pop()}</div>
-            </div>
-            <div class="text-xs text-surface-400">{conv.message_count} msgs</div>
-          </div>
-        </button>
-      {/each}
-      {#if searchResults.length === 0}
-        <div class="px-4 py-8 text-center text-sm text-surface-400">No results found</div>
-      {/if}
-    </div>
- 
-  {:else if view === "stats" && stats}
-    <!-- Statistics -->
-    <button onclick={() => view = "summary"} class="mb-4 text-sm text-primary-600 hover:text-primary-700">← Back</button>
-    <div class="grid grid-cols-3 gap-4 mb-6">
-      <div class="bg-white dark:bg-surface-800 rounded-lg p-4 border border-surface-200 dark:border-surface-700">
-        <div class="text-sm text-surface-400">Total Conversations</div>
-        <div class="text-2xl font-bold text-surface-900 dark:text-white">{stats.total_conversations}</div>
-      </div>
-      <div class="bg-white dark:bg-surface-800 rounded-lg p-4 border border-surface-200 dark:border-surface-700">
-        <div class="text-sm text-surface-400">Total Messages</div>
-        <div class="text-2xl font-bold text-surface-900 dark:text-white">{stats.total_messages}</div>
-      </div>
-      <div class="bg-white dark:bg-surface-800 rounded-lg p-4 border border-surface-200 dark:border-surface-700">
-        <div class="text-sm text-surface-400">Total Tokens</div>
-        <div class="text-2xl font-bold text-surface-900 dark:text-white">{formatTokens(stats.total_tokens)}</div>
-      </div>
-    </div>
- 
-    <div class="grid grid-cols-2 gap-4">
-      <div class="bg-white dark:bg-surface-800 rounded-lg p-4 border border-surface-200 dark:border-surface-700">
-        <h3 class="text-sm font-semibold text-surface-700 dark:text-surface-200 mb-3">Top Projects</h3>
-        {#each stats.top_projects as proj}
-          <div class="flex justify-between text-sm py-1">
-            <span class="text-surface-600 dark:text-surface-300 truncate">{proj.path?.split('/').pop()}</span>
-            <span class="text-surface-400">{proj.count}</span>
-          </div>
-        {/each}
-      </div>
- 
-      <div class="bg-white dark:bg-surface-800 rounded-lg p-4 border border-surface-200 dark:border-surface-700">
-        <h3 class="text-sm font-semibold text-surface-700 dark:text-surface-200 mb-3">Top Tools</h3>
-        {#each stats.top_tools as tool}
-          <div class="flex justify-between text-sm py-1">
-            <span class="text-surface-600 dark:text-surface-300">{tool.name}</span>
-            <span class="text-surface-400">{tool.count}</span>
-          </div>
-        {/each}
-      </div>
-    </div>
-  {/if}
-</div>
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/dashboard/frontend/coverage/lcov-report/src/routes/Dashboard.svelte.html b/dashboard/frontend/coverage/lcov-report/src/routes/Dashboard.svelte.html deleted file mode 100644 index da1611f..0000000 --- a/dashboard/frontend/coverage/lcov-report/src/routes/Dashboard.svelte.html +++ /dev/null @@ -1,622 +0,0 @@ - - - - - - Code coverage report for src/routes/Dashboard.svelte - - - - - - - - - -
-
-

All files / src/routes Dashboard.svelte

-
- -
- 0% - Statements - 0/87 -
- - -
- 0% - Branches - 0/1 -
- - -
- 0% - Functions - 0/1 -
- - -
- 0% - Lines - 0/87 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -178 -179 -180  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  - 
<script>
-  import { onMount, onDestroy } from "svelte";
-  import { get } from "../lib/api.js";
- 
-  let containers = $state([]);
-  let repos = $state([]);
-  let stats = $state(null);
-  let loading = $state(true);
- 
-  async function load() {
-    const [c, g, s] = await Promise.all([
-      get("/docker/containers"),
-      get("/gitea/repos"),
-      get("/system/stats"),
-    ]);
-    containers = c || [];
-    repos = Array.isArray(g) ? g : g?.data || [];
-    stats = s;
-    loading = false;
-  }
- 
-  function fmtBytes(b) {
-    if (!b) return "0 B";
-    const u = ["B", "KB", "MB", "GB", "TB"];
-    const i = Math.floor(Math.log(b) / Math.log(1024));
-    return (b / Math.pow(1024, i)).toFixed(1) + " " + u[i];
-  }
- 
-  function pctColor(pct) {
-    if (pct > 90) return "text-rose-500";
-    if (pct > 70) return "text-amber-500";
-    return "text-emerald-500";
-  }
- 
-  let interval;
- 
-  onMount(() => { load(); interval = setInterval(load, 10000); });
-  onDestroy(() => clearInterval(interval));
-</script>
- 
-<div class="space-y-8">
-  <div>
-    <h1 class="text-2xl font-bold text-surface-900 dark:text-white tracking-tight">Overview</h1>
-    <p class="text-sm text-surface-400 mt-1">System status at a glance</p>
-  </div>
- 
-  {#if loading}
-    <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
-      {#each Array(4) as _}
-        <div class="h-[120px] rounded-xl bg-surface-100 dark:bg-surface-800 animate-pulse"></div>
-      {/each}
-    </div>
-  {:else}
-    <!-- Stats Cards -->
-    <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
-      <!-- CPU -->
-      <div class="bg-white dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 p-5 shadow-sm hover:shadow-md transition-shadow duration-200">
-        <div class="flex items-center justify-between mb-3">
-          <p class="text-xs font-semibold uppercase tracking-wider text-surface-400">CPU</p>
-          <div class="w-8 h-8 rounded-lg bg-sky-50 flex items-center justify-center">
-            <svg class="w-4 h-4 text-sky-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2z" /></svg>
-          </div>
-        </div>
-        <p class="text-2xl font-bold {pctColor(stats?.cpu_percent || 0)}">{stats?.cpu_percent || 0}%</p>
-        <p class="text-xs text-surface-400 mt-1">{stats?.cpu_count || 0} cores · Load {stats?.load_avg?.[0] || 0}</p>
-      </div>
- 
-      <!-- Memory -->
-      <div class="bg-white dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 p-5 shadow-sm hover:shadow-md transition-shadow duration-200">
-        <div class="flex items-center justify-between mb-3">
-          <p class="text-xs font-semibold uppercase tracking-wider text-surface-400">Memory</p>
-          <div class="w-8 h-8 rounded-lg bg-violet-50 flex items-center justify-center">
-            <svg class="w-4 h-4 text-violet-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
-          </div>
-        </div>
-        <p class="text-2xl font-bold {pctColor(stats?.memory?.percent || 0)}">{stats?.memory?.percent || 0}%</p>
-        <p class="text-xs text-surface-400 mt-1">{fmtBytes(stats?.memory?.used)} / {fmtBytes(stats?.memory?.total)}</p>
-      </div>
- 
-      <!-- Disk -->
-      <div class="bg-white dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 p-5 shadow-sm hover:shadow-md transition-shadow duration-200">
-        <div class="flex items-center justify-between mb-3">
-          <p class="text-xs font-semibold uppercase tracking-wider text-surface-400">Disk</p>
-          <div class="w-8 h-8 rounded-lg bg-amber-50 flex items-center justify-center">
-            <svg class="w-4 h-4 text-amber-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4" /></svg>
-          </div>
-        </div>
-        <p class="text-2xl font-bold {pctColor(stats?.disk?.percent || 0)}">{stats?.disk?.percent || 0}%</p>
-        <p class="text-xs text-surface-400 mt-1">{fmtBytes(stats?.disk?.free)} free of {fmtBytes(stats?.disk?.total)}</p>
-      </div>
- 
-      <!-- Uptime -->
-      <div class="bg-white dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 p-5 shadow-sm hover:shadow-md transition-shadow duration-200">
-        <div class="flex items-center justify-between mb-3">
-          <p class="text-xs font-semibold uppercase tracking-wider text-surface-400">Uptime</p>
-          <div class="w-8 h-8 rounded-lg bg-emerald-50 flex items-center justify-center">
-            <svg class="w-4 h-4 text-emerald-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M13 10V3L4 14h7v7l9-11h-7z" /></svg>
-          </div>
-        </div>
-        <p class="text-2xl font-bold text-surface-800 dark:text-surface-100">{stats?.uptime || "—"}</p>
-        <p class="text-xs text-surface-400 mt-1 truncate" title={stats?.platform}>{stats?.platform || "—"}</p>
-      </div>
-    </div>
- 
-    <!-- Services Grid -->
-    <div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
-      <!-- Docker Summary -->
-      <div class="bg-white dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 p-5 shadow-sm">
-        <div class="flex items-center justify-between mb-4">
-          <h3 class="text-sm font-semibold text-surface-700 dark:text-surface-200">Docker Containers</h3>
-          <span class="text-xs font-medium text-emerald-600 bg-emerald-50 px-2 py-0.5 rounded-full">{containers.filter(c => c.status === "running").length}/{containers.length} running</span>
-        </div>
-        <div class="space-y-2">
-          {#each containers.slice(0, 5) as c}
-            <div class="flex items-center justify-between py-1.5 text-sm">
-              <div class="flex items-center gap-2">
-                <span class="w-1.5 h-1.5 rounded-full {c.status !== 'running' ? 'bg-surface-300' : c.health === 'unhealthy' ? 'bg-rose-400' : c.health === 'healthy' ? 'bg-emerald-400' : 'bg-sky-400'}"></span>
-                <span class="font-medium text-surface-700 dark:text-surface-200">{c.name}</span>
-              </div>
-              <span class="text-xs text-surface-400 truncate max-w-[180px]">{c.image}</span>
-            </div>
-          {/each}
-          {#if containers.length > 5}
-            <p class="text-xs text-primary-500 font-medium pt-1">+{containers.length - 5} more</p>
-          {/if}
-        </div>
-      </div>
- 
-      <!-- Repos Summary -->
-      <div class="bg-white dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 p-5 shadow-sm">
-        <div class="flex items-center justify-between mb-4">
-          <h3 class="text-sm font-semibold text-surface-700 dark:text-surface-200">Git Repositories</h3>
-          <span class="text-xs font-medium text-primary-600 bg-primary-50 px-2 py-0.5 rounded-full">{repos.length} repos</span>
-        </div>
-        <div class="space-y-2">
-          {#each repos.slice(0, 5) as r}
-            <div class="flex items-center justify-between py-1.5 text-sm">
-              <div class="flex items-center gap-2">
-                <svg class="w-3.5 h-3.5 text-surface-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" /></svg>
-                <span class="font-medium text-surface-700 dark:text-surface-200">{r.name}</span>
-              </div>
-              <span class="text-xs text-surface-400">{r.language || "—"}</span>
-            </div>
-          {/each}
-          {#if repos.length > 5}
-            <p class="text-xs text-primary-500 font-medium pt-1">+{repos.length - 5} more</p>
-          {/if}
-        </div>
-      </div>
-    </div>
- 
-    <!-- Storage Usage -->
-    {#if stats?.volumes?.length}
-      <div class="bg-white dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 p-5 shadow-sm">
-        <h3 class="text-sm font-semibold text-surface-700 dark:text-surface-200 mb-3">Storage Usage</h3>
-        <div class="space-y-3">
-          {#each stats.volumes as vol}
-            <div>
-              <div class="flex justify-between text-xs text-surface-500 mb-1">
-                <span class="font-medium">{vol.mount}</span>
-                <span>{vol.percent}%</span>
-              </div>
-              <div class="w-full h-3 bg-surface-100 dark:bg-surface-700 rounded-full overflow-hidden">
-                <div
-                  class="h-full rounded-full transition-all duration-700 ease-out {vol.percent > 90 ? 'bg-gradient-to-r from-rose-400 to-rose-500' : vol.percent > 70 ? 'bg-gradient-to-r from-amber-400 to-amber-500' : 'bg-gradient-to-r from-emerald-400 to-emerald-500'}"
-                  style="width: {vol.percent}%"
-                ></div>
-              </div>
-              <div class="flex justify-between mt-1 text-xs text-surface-400">
-                <span>{fmtBytes(vol.used)} used</span>
-                <span>{fmtBytes(vol.free)} free</span>
-              </div>
-            </div>
-          {/each}
-        </div>
-      </div>
-    {/if}
-  {/if}
-</div>
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/dashboard/frontend/coverage/lcov-report/src/routes/Docker.svelte.html b/dashboard/frontend/coverage/lcov-report/src/routes/Docker.svelte.html deleted file mode 100644 index a252427..0000000 --- a/dashboard/frontend/coverage/lcov-report/src/routes/Docker.svelte.html +++ /dev/null @@ -1,565 +0,0 @@ - - - - - - Code coverage report for src/routes/Docker.svelte - - - - - - - - - -
-
-

All files / src/routes Docker.svelte

-
- -
- 0% - Statements - 0/94 -
- - -
- 0% - Branches - 0/1 -
- - -
- 0% - Functions - 0/1 -
- - -
- 0% - Lines - 0/94 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  - 
<script>
-  import { onMount } from "svelte";
-  import { get, post } from "../lib/api.js";
- 
-  let containers = $state([]);
-  let loading = $state(true);
-  let error = $state("");
-  let logTarget = $state("");
-  let logContent = $state("");
-  let loadingLogs = $state(false);
-  let actionLoading = $state("");
- 
-  async function load() {
-    loading = true;
-    error = "";
-    try {
-      containers = (await get("/docker/containers")) || [];
-    } catch (e) {
-      error = e.message || "Docker API unavailable";
-      containers = [];
-    }
-    loading = false;
-  }
- 
-  async function action(id, act) {
-    actionLoading = id + act;
-    try {
-      await post(`/docker/containers/${id}/${act}`);
-      await load();
-    } catch (e) {
-      error = e.message || "Action failed";
-    }
-    actionLoading = "";
-  }
- 
-  async function showLogs(id, name) {
-    logTarget = name;
-    loadingLogs = true;
-    try {
-      const r = await get(`/docker/containers/${id}/logs?tail=200`);
-      logContent = r?.logs || "No logs available";
-    } catch (e) {
-      logContent = "Failed to load logs";
-    }
-    loadingLogs = false;
-  }
- 
-  onMount(load);
-</script>
- 
-<div class="space-y-6">
-  <div class="flex items-center justify-between">
-    <div>
-      <h1 class="text-2xl font-bold text-surface-900 dark:text-white tracking-tight">Docker</h1>
-      <p class="text-sm text-surface-400 mt-1">{containers.length} containers · {containers.filter(c => c.status === "running").length} running</p>
-    </div>
-    <button onclick={load} class="text-xs font-medium text-primary-600 bg-primary-50 hover:bg-primary-100 px-3 py-1.5 rounded-lg transition-colors">
-      Refresh
-    </button>
-  </div>
- 
-  {#if loading}
-    <div class="space-y-3">
-      {#each Array(5) as _}
-        <div class="h-[72px] rounded-xl bg-surface-100 dark:bg-surface-800 animate-pulse"></div>
-      {/each}
-    </div>
-  {:else if error}
-    <div class="bg-rose-50 dark:bg-rose-900/20 border border-rose-200 dark:border-rose-800 rounded-xl p-4">
-      <div class="flex items-center justify-between">
-        <div class="flex items-center gap-2">
-          <svg class="w-5 h-5 text-rose-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z" /></svg>
-          <p class="text-sm font-medium text-rose-700 dark:text-rose-300">{error}</p>
-        </div>
-        <button onclick={load} class="text-xs font-medium text-rose-600 bg-rose-100 hover:bg-rose-200 px-3 py-1.5 rounded-lg transition-colors">
-          Retry
-        </button>
-      </div>
-    </div>
-  {:else}
-    <div class="space-y-2">
-      {#each containers as c}
-        <div class="bg-white dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 p-4 flex items-center justify-between shadow-sm hover:shadow-md transition-all duration-200 group">
-          <div class="flex items-center gap-3 min-w-0">
-            <span class="relative flex h-2.5 w-2.5 shrink-0">
-              {#if c.status === "running"}
-                <span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
-                <span class="relative inline-flex rounded-full h-2.5 w-2.5 bg-emerald-500"></span>
-              {:else}
-                <span class="relative inline-flex rounded-full h-2.5 w-2.5 bg-surface-300"></span>
-              {/if}
-            </span>
-            <div class="min-w-0">
-              <p class="text-sm font-semibold text-surface-800 dark:text-surface-100 truncate">{c.name}</p>
-              <p class="text-xs text-surface-400 truncate mt-0.5">{c.image}</p>
-            </div>
-          </div>
-          <div class="flex items-center gap-1.5 opacity-70 group-hover:opacity-100 transition-opacity">
-            {#if c.status === "running"}
-              <button
-                class="text-xs font-medium px-2.5 py-1.5 rounded-lg text-rose-600 bg-rose-50 hover:bg-rose-100 transition-colors disabled:opacity-50"
-                onclick={() => action(c.id, "stop")}
-                disabled={actionLoading === c.id + "stop"}
-              >
-                {actionLoading === c.id + "stop" ? "..." : "Stop"}
-              </button>
-              <button
-                class="text-xs font-medium px-2.5 py-1.5 rounded-lg text-amber-600 bg-amber-50 hover:bg-amber-100 transition-colors disabled:opacity-50"
-                onclick={() => action(c.id, "restart")}
-                disabled={actionLoading === c.id + "restart"}
-              >
-                {actionLoading === c.id + "restart" ? "..." : "Restart"}
-              </button>
-            {:else}
-              <button
-                class="text-xs font-medium px-2.5 py-1.5 rounded-lg text-emerald-600 bg-emerald-50 hover:bg-emerald-100 transition-colors disabled:opacity-50"
-                onclick={() => action(c.id, "start")}
-                disabled={actionLoading === c.id + "start"}
-              >
-                {actionLoading === c.id + "start" ? "..." : "Start"}
-              </button>
-            {/if}
-            <button
-              class="text-xs font-medium px-2.5 py-1.5 rounded-lg text-surface-600 bg-surface-100 hover:bg-surface-200 transition-colors"
-              onclick={() => showLogs(c.id, c.name)}
-            >
-              Logs
-            </button>
-          </div>
-        </div>
-      {/each}
-    </div>
-  {/if}
- 
-  <!-- Log Modal -->
-  {#if logTarget}
-    <div class="fixed inset-0 bg-black/40 backdrop-blur-sm flex items-center justify-center z-50" role="dialog" aria-modal="true" onclick={() => { logTarget = ""; logContent = ""; }} onkeydown={(e) => e.key === "Escape" && (logTarget = "", logContent = "")}>
-      <div class="bg-white dark:bg-surface-800 rounded-2xl shadow-2xl w-[90vw] max-w-4xl max-h-[80vh] flex flex-col" role="document" onclick={(e) => e.stopPropagation()} onkeydown={(e) => e.stopPropagation()}>
-        <div class="flex items-center justify-between px-5 py-4 border-b border-surface-200 dark:border-surface-700">
-          <div>
-            <h3 class="text-sm font-bold text-surface-800 dark:text-surface-100">Container Logs</h3>
-            <p class="text-xs text-surface-400 mt-0.5">{logTarget}</p>
-          </div>
-          <button aria-label="Close logs" class="text-surface-400 hover:text-surface-600 transition-colors" onclick={() => { logTarget = ""; logContent = ""; }}>
-            <svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" /></svg>
-          </button>
-        </div>
-        <div class="flex-1 overflow-auto p-1">
-          {#if loadingLogs}
-            <div class="flex items-center justify-center h-40">
-              <div class="w-6 h-6 border-2 border-primary-500 border-t-transparent rounded-full animate-spin"></div>
-            </div>
-          {:else}
-            <pre class="bg-surface-900 text-emerald-400 text-xs p-4 rounded-xl whitespace-pre-wrap leading-relaxed font-mono min-h-[200px]">{logContent}</pre>
-          {/if}
-        </div>
-      </div>
-    </div>
-  {/if}
-</div>
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/dashboard/frontend/coverage/lcov-report/src/routes/Files.svelte.html b/dashboard/frontend/coverage/lcov-report/src/routes/Files.svelte.html deleted file mode 100644 index cd8884f..0000000 --- a/dashboard/frontend/coverage/lcov-report/src/routes/Files.svelte.html +++ /dev/null @@ -1,616 +0,0 @@ - - - - - - Code coverage report for src/routes/Files.svelte - - - - - - - - - -
-
-

All files / src/routes Files.svelte

-
- -
- 0% - Statements - 0/105 -
- - -
- 0% - Branches - 0/1 -
- - -
- 0% - Functions - 0/1 -
- - -
- 0% - Lines - 0/105 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -178  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  - 
<script>
-  import { onMount } from "svelte";
-  import { get, del, upload, download } from "../lib/api.js";
- 
-  let entries = $state([]);
-  let currentPath = $state("");
-  let loading = $state(true);
-  let uploading = $state(false);
-  let error = $state("");
-  let fileInput;
- 
-  async function browse(path) {
-    loading = true;
-    currentPath = path;
-    const data = await get(`/files/browse?path=${encodeURIComponent(path)}`);
-    entries = data?.entries || [];
-    loading = false;
-  }
- 
-  function goUp() {
-    const parts = currentPath.split("/").filter(Boolean);
-    parts.pop();
-    browse(parts.join("/"));
-  }
- 
-  async function remove(name) {
-    if (!confirm(`Delete "${name}"? This cannot be undone.`)) return;
-    const path = currentPath ? `${currentPath}/${name}` : name;
-    await del(`/files/delete?path=${encodeURIComponent(path)}`);
-    browse(currentPath);
-  }
- 
-  async function handleDownload(name) {
-    const path = currentPath ? `${currentPath}/${name}` : name;
-    error = "";
-    try {
-      await download(path, name);
-    } catch (e) {
-      error = `Failed to download "${name}": ${e.message}`;
-      console.error("Download failed:", e);
-    }
-  }
- 
-  async function handleUpload() {
-    const file = fileInput?.files?.[0];
-    if (!file) return;
-    uploading = true;
-    await upload(currentPath, file);
-    fileInput.value = "";
-    uploading = false;
-    browse(currentPath);
-  }
- 
-  function formatSize(bytes) {
-    if (!bytes) return "—";
-    const u = ["B", "KB", "MB", "GB", "TB"];
-    const i = Math.floor(Math.log(bytes) / Math.log(1024));
-    return (bytes / Math.pow(1024, i)).toFixed(1) + " " + u[i];
-  }
- 
-  function fileIcon(name) {
-    const ext = name.split(".").pop().toLowerCase();
-    if (["jpg", "jpeg", "png", "gif", "webp", "svg"].includes(ext)) return "image";
-    if (["mp4", "mkv", "avi", "mov"].includes(ext)) return "video";
-    if (["mp3", "flac", "wav", "m4a", "ogg"].includes(ext)) return "audio";
-    if (["zip", "tar", "gz", "7z", "rar"].includes(ext)) return "archive";
-    if (["py", "js", "ts", "go", "rs", "sh", "yml", "yaml", "json", "md"].includes(ext)) return "code";
-    return "file";
-  }
- 
-  const crumbs = $derived(currentPath.split("/").filter(Boolean));
- 
-  onMount(() => browse(""));
-</script>
- 
-<div class="space-y-5">
-  <div class="flex items-center justify-between">
-    <div>
-      <h1 class="text-2xl font-bold text-surface-900 dark:text-white tracking-tight">Files</h1>
-      <p class="text-sm text-surface-400 mt-1">/volume1{currentPath ? "/" + currentPath : ""}</p>
-    </div>
-    <div class="flex items-center gap-2">
-      <input type="file" bind:this={fileInput} onchange={handleUpload} class="hidden" />
-      <button
-        onclick={() => fileInput.click()}
-        class="text-xs font-medium text-primary-600 bg-primary-50 hover:bg-primary-100 px-3 py-1.5 rounded-lg transition-colors disabled:opacity-50"
-        disabled={uploading}
-      >
-        {uploading ? "Uploading..." : "Upload"}
-      </button>
-    </div>
-  </div>
- 
-  <!-- Error message -->
-  {#if error}
-    <div class="bg-rose-50 dark:bg-rose-900/20 border border-rose-200 dark:border-rose-800 rounded-lg px-4 py-3 flex items-start gap-3">
-      <svg class="w-5 h-5 text-rose-500 shrink-0 mt-0.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
-        <path stroke-linecap="round" stroke-linejoin="round" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
-      </svg>
-      <div class="flex-1">
-        <p class="text-sm text-rose-700 dark:text-rose-300">{error}</p>
-      </div>
-      <button onclick={() => error = ""} class="text-rose-400 hover:text-rose-600 transition-colors">
-        <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
-          <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
-        </svg>
-      </button>
-    </div>
-  {/if}
- 
-  <!-- Breadcrumbs -->
-  <div class="flex items-center gap-1 text-sm flex-wrap">
-    <button class="text-primary-600 hover:text-primary-700 font-medium transition-colors" onclick={() => browse("")}>volume1</button>
-    {#each crumbs as part, i}
-      <svg class="w-3.5 h-3.5 text-surface-300" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7" /></svg>
-      <button
-        class="text-primary-600 hover:text-primary-700 font-medium transition-colors"
-        onclick={() => browse(crumbs.slice(0, i + 1).join("/"))}
-      >{part}</button>
-    {/each}
-    {#if currentPath}
-      <button
-        class="ml-auto text-xs font-medium text-surface-500 bg-surface-100 dark:bg-surface-700 hover:bg-surface-200 dark:hover:bg-surface-600 px-2.5 py-1 rounded-lg transition-colors flex items-center gap-1"
-        onclick={goUp}
-      >
-        <svg class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M5 10l7-7m0 0l7 7m-7-7v18" /></svg>
-        Up
-      </button>
-    {/if}
-  </div>
- 
-  <!-- File List -->
-  {#if loading}
-    <div class="space-y-1">
-      {#each Array(8) as _}
-        <div class="h-11 rounded-lg bg-surface-100 dark:bg-surface-800 animate-pulse"></div>
-      {/each}
-    </div>
-  {:else}
-    <div class="bg-white dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 shadow-sm overflow-hidden">
-      <!-- Header -->
-      <div class="grid grid-cols-[1fr_100px_100px] px-4 py-2 text-[11px] font-semibold uppercase tracking-wider text-surface-400 border-b border-surface-100 dark:border-surface-700">
-        <span>Name</span>
-        <span class="text-right">Size</span>
-        <span class="text-right">Actions</span>
-      </div>
-      <!-- Entries -->
-      {#each entries as e}
-        <div class="grid grid-cols-[1fr_100px_100px] items-center px-4 py-2.5 hover:bg-surface-50 dark:hover:bg-surface-700 border-b border-surface-100 dark:border-surface-700 last:border-0 transition-colors group">
-          {#if e.is_dir}
-            <button class="flex items-center gap-2.5 text-sm font-medium text-surface-700 dark:text-surface-200 hover:text-primary-600 transition-colors text-left truncate" onclick={() => browse(currentPath ? `${currentPath}/${e.name}` : e.name)}>
-              <svg class="w-4 h-4 text-amber-400 shrink-0" fill="currentColor" viewBox="0 0 20 20"><path d="M2 6a2 2 0 012-2h5l2 2h5a2 2 0 012 2v6a2 2 0 01-2 2H4a2 2 0 01-2-2V6z" /></svg>
-              {e.name}
-            </button>
-          {:else}
-            <span class="flex items-center gap-2.5 text-sm text-surface-600 dark:text-surface-300 truncate">
-              <svg class="w-4 h-4 text-surface-300 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" /></svg>
-              {e.name}
-            </span>
-          {/if}
-          <span class="text-xs text-surface-400 text-right">{e.is_dir ? "—" : formatSize(e.size)}</span>
-          <div class="flex items-center justify-end gap-1 opacity-100 md:opacity-0 md:group-hover:opacity-100 transition-opacity">
-            {#if !e.is_dir}
-              <button onclick={() => handleDownload(e.name)} class="text-[11px] text-primary-500 hover:text-primary-700 font-medium transition-colors">
-                DL
-              </button>
-            {/if}
-            <button class="text-[11px] text-rose-400 hover:text-rose-600 font-medium transition-colors" onclick={() => remove(e.name)}>Del</button>
-          </div>
-        </div>
-      {/each}
-      {#if entries.length === 0}
-        <div class="px-4 py-8 text-center text-sm text-surface-400">Empty directory</div>
-      {/if}
-    </div>
-  {/if}
-</div>
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/dashboard/frontend/coverage/lcov-report/src/routes/Gitea.svelte.html b/dashboard/frontend/coverage/lcov-report/src/routes/Gitea.svelte.html deleted file mode 100644 index da9aded..0000000 --- a/dashboard/frontend/coverage/lcov-report/src/routes/Gitea.svelte.html +++ /dev/null @@ -1,457 +0,0 @@ - - - - - - Code coverage report for src/routes/Gitea.svelte - - - - - - - - - -
-
-

All files / src/routes Gitea.svelte

-
- -
- 0% - Statements - 0/74 -
- - -
- 0% - Branches - 0/1 -
- - -
- 0% - Functions - 0/1 -
- - -
- 0% - Lines - 0/74 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  - 
<script>
-  import { onMount } from "svelte";
-  import { get } from "../lib/api.js";
- 
-  let repos = $state([]);
-  let commits = $state([]);
-  let selectedRepo = $state("");
-  let loading = $state(true);
-  let loadingCommits = $state(false);
- 
-  async function load() {
-    loading = true;
-    const data = await get("/gitea/repos");
-    repos = Array.isArray(data) ? data : data?.data || [];
-    loading = false;
-  }
- 
-  async function showCommits(owner, repo) {
-    selectedRepo = `${owner}/${repo}`;
-    loadingCommits = true;
-    const data = await get(`/gitea/repos/${owner}/${repo}/commits`);
-    commits = Array.isArray(data) ? data : [];
-    loadingCommits = false;
-  }
- 
-  function timeAgo(d) {
-    const s = Math.floor((Date.now() - new Date(d)) / 1000);
-    if (s < 60) return "just now";
-    if (s < 3600) return Math.floor(s / 60) + "m ago";
-    if (s < 86400) return Math.floor(s / 3600) + "h ago";
-    return Math.floor(s / 86400) + "d ago";
-  }
- 
-  const langColors = {
-    Python: "#3572A5", JavaScript: "#f1e05a", Shell: "#89e051", Go: "#00ADD8",
-    TypeScript: "#3178c6", Markdown: "#083fa1", CSS: "#563d7c", HTML: "#e34c26",
-  };
- 
-  onMount(load);
-</script>
- 
-<div class="space-y-6">
-  <div>
-    <h1 class="text-2xl font-bold text-surface-900 dark:text-white tracking-tight">Repositories</h1>
-    <p class="text-sm text-surface-400 mt-1">{repos.length} repositories on Gitea</p>
-  </div>
- 
-  {#if loading}
-    <div class="grid grid-cols-1 md:grid-cols-2 gap-3">
-      {#each Array(4) as _}
-        <div class="h-[100px] rounded-xl bg-surface-100 dark:bg-surface-800 animate-pulse"></div>
-      {/each}
-    </div>
-  {:else}
-    <div class="grid grid-cols-1 md:grid-cols-2 gap-3">
-      {#each repos as r}
-        <button
-          class="bg-white dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 p-4 text-left shadow-sm hover:shadow-md hover:border-primary-200 dark:hover:border-primary-700 transition-all duration-200 group {selectedRepo === `${r.owner?.login}/${r.name}` ? 'ring-2 ring-primary-500 border-primary-300' : ''}"
-          onclick={() => showCommits(r.owner?.login, r.name)}
-        >
-          <div class="flex items-start justify-between">
-            <div class="min-w-0">
-              <p class="text-sm font-semibold text-surface-800 dark:text-surface-100 group-hover:text-primary-600 transition-colors truncate">{r.name}</p>
-              <p class="text-xs text-surface-400 mt-1 line-clamp-1">{r.description || "No description"}</p>
-            </div>
-            {#if r.language}
-              <span class="flex items-center gap-1 text-[11px] text-surface-500 shrink-0 ml-2">
-                <span class="w-2 h-2 rounded-full" style="background: {langColors[r.language] || '#888'}"></span>
-                {r.language}
-              </span>
-            {/if}
-          </div>
-          <p class="text-[11px] text-surface-400 mt-2.5">Updated {timeAgo(r.updated_at)}</p>
-        </button>
-      {/each}
-    </div>
-  {/if}
- 
-  <!-- Commits Panel -->
-  {#if selectedRepo}
-    <div class="bg-white dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 p-5 shadow-sm">
-      <div class="flex items-center justify-between mb-4">
-        <div>
-          <h3 class="text-sm font-bold text-surface-800 dark:text-surface-100">Recent Commits</h3>
-          <p class="text-xs text-surface-400 mt-0.5">{selectedRepo}</p>
-        </div>
-        <button
-          aria-label="Close commits"
-          class="text-xs text-surface-400 hover:text-surface-600 transition-colors"
-          onclick={() => { selectedRepo = ""; commits = []; }}
-        >
-          <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" /></svg>
-        </button>
-      </div>
-      {#if loadingCommits}
-        <div class="space-y-3">
-          {#each Array(3) as _}
-            <div class="h-12 rounded-lg bg-surface-100 animate-pulse"></div>
-          {/each}
-        </div>
-      {:else}
-        <div class="space-y-0">
-          {#each commits.slice(0, 15) as c, i}
-            <div class="flex gap-3 py-2.5 {i < commits.length - 1 ? 'border-b border-surface-100 dark:border-surface-700' : ''}">
-              <div class="flex flex-col items-center pt-1">
-                <div class="w-2 h-2 rounded-full bg-primary-400 shrink-0"></div>
-                {#if i < commits.length - 1}
-                  <div class="w-px flex-1 bg-surface-200 mt-1"></div>
-                {/if}
-              </div>
-              <div class="min-w-0 flex-1">
-                <p class="text-sm text-surface-700 dark:text-surface-200 truncate">{c.commit?.message?.split("\n")[0]}</p>
-                <p class="text-[11px] text-surface-400 mt-0.5">
-                  <span class="font-medium">{c.commit?.author?.name}</span> · {timeAgo(c.commit?.author?.date)}
-                </p>
-              </div>
-              <span class="text-[10px] font-mono text-surface-400 bg-surface-50 dark:bg-surface-700 px-1.5 py-0.5 rounded h-fit shrink-0">{c.sha?.slice(0, 7)}</span>
-            </div>
-          {/each}
-        </div>
-      {/if}
-    </div>
-  {/if}
-</div>
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/dashboard/frontend/coverage/lcov-report/src/routes/InfoEngine.svelte.html b/dashboard/frontend/coverage/lcov-report/src/routes/InfoEngine.svelte.html deleted file mode 100644 index 169bac1..0000000 --- a/dashboard/frontend/coverage/lcov-report/src/routes/InfoEngine.svelte.html +++ /dev/null @@ -1,502 +0,0 @@ - - - - - - Code coverage report for src/routes/InfoEngine.svelte - - - - - - - - - -
-
-

All files / src/routes InfoEngine.svelte

-
- -
- 0% - Statements - 0/92 -
- - -
- 0% - Branches - 0/1 -
- - -
- 0% - Functions - 0/1 -
- - -
- 0% - Lines - 0/92 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  - 
<script>
-  import { onMount } from "svelte";
-  import { getInfoEngineItems, getInfoEngineItem } from "../lib/api.js";
- 
-  let loading = $state(true);
-  let loadingDetail = $state(false);
-  let requestError = $state("");
-  let items = $state([]);
-  let total = $state(0);
-  let selectedId = $state(null);
-  let selectedItem = $state(null);
- 
-  async function loadItems() {
-    loading = true;
-    requestError = "";
-    try {
-      const res = await getInfoEngineItems({ limit: 50, offset: 0 });
-      items = res.items || [];
-      total = res.total || 0;
-      if (items.length > 0) {
-        await selectItem(items[0].id);
-      } else {
-        selectedId = null;
-        selectedItem = null;
-      }
-    } catch (e) {
-      requestError = e?.body?.detail || e?.message || "Failed to load items";
-      items = [];
-      total = 0;
-      selectedId = null;
-      selectedItem = null;
-    } finally {
-      loading = false;
-    }
-  }
- 
-  async function selectItem(id) {
-    selectedId = id;
-    loadingDetail = true;
-    requestError = "";
-    try {
-      selectedItem = await getInfoEngineItem(id);
-    } catch (e) {
-      selectedItem = null;
-      requestError = e?.body?.detail || e?.message || "Failed to load item";
-    } finally {
-      loadingDetail = false;
-    }
-  }
- 
-  function formatDate(value) {
-    if (!value) return "—";
-    try {
-      return new Date(value).toLocaleString();
-    } catch {
-      return value;
-    }
-  }
- 
-  onMount(loadItems);
-</script>
- 
-<div class="space-y-6">
-  <div class="flex items-center justify-between">
-    <div>
-      <h1 class="text-2xl font-bold text-surface-900 dark:text-white tracking-tight">Info Engine</h1>
-      <p class="text-sm text-surface-400 mt-1">Context-aware stream of curated signals</p>
-    </div>
-    <button onclick={loadItems} disabled={loading} class="px-3 py-1.5 text-xs font-medium text-primary-600 bg-primary-50 hover:bg-primary-100 rounded-lg transition-colors disabled:opacity-50 dark:bg-primary-900/30 dark:text-primary-300">
-      {loading ? "Loading..." : "Refresh"}
-    </button>
-  </div>
- 
-  {#if requestError}
-    <div class="bg-rose-50 dark:bg-rose-900/20 border border-rose-200 dark:border-rose-700 rounded-xl p-4 shadow-sm">
-      <p class="text-sm font-medium text-rose-700 dark:text-rose-300">{requestError}</p>
-    </div>
-  {/if}
- 
-  <div class="grid grid-cols-1 lg:grid-cols-3 gap-4">
-    <div class="lg:col-span-1 bg-white dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 shadow-sm">
-      <div class="px-4 py-3 border-b border-surface-200 dark:border-surface-700 text-xs text-surface-400">
-        Latest items ({total})
-      </div>
-      {#if loading}
-        <div class="p-4 space-y-2">
-          {#each Array(6) as _}
-            <div class="h-12 rounded-lg bg-surface-100 dark:bg-surface-700 animate-pulse"></div>
-          {/each}
-        </div>
-      {:else if items.length === 0}
-        <div class="p-4 text-sm text-surface-400">No items yet.</div>
-      {:else}
-        <div class="max-h-[560px] overflow-y-auto p-2">
-          {#each items as item}
-            <button onclick={() => selectItem(item.id)} class="w-full text-left px-3 py-2 rounded-lg mb-1 transition-colors {selectedId === item.id ? 'bg-primary-50 dark:bg-primary-900/30' : 'hover:bg-surface-100 dark:hover:bg-surface-700'}">
-              <p class="text-sm font-semibold text-surface-800 dark:text-surface-100 line-clamp-2">{item.title}</p>
-              <p class="text-xs text-surface-400 mt-1">{item.source}</p>
-            </button>
-          {/each}
-        </div>
-      {/if}
-    </div>
- 
-    <div class="lg:col-span-2 bg-white dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 p-5 shadow-sm">
-      {#if loadingDetail}
-        <div class="space-y-3">
-          <div class="h-7 w-3/4 rounded bg-surface-100 dark:bg-surface-700 animate-pulse"></div>
-          <div class="h-4 w-full rounded bg-surface-100 dark:bg-surface-700 animate-pulse"></div>
-          <div class="h-4 w-5/6 rounded bg-surface-100 dark:bg-surface-700 animate-pulse"></div>
-          <div class="h-24 w-full rounded bg-surface-100 dark:bg-surface-700 animate-pulse"></div>
-        </div>
-      {:else if selectedItem}
-        <h2 class="text-xl font-bold text-surface-900 dark:text-white">{selectedItem.title}</h2>
-        <div class="mt-2 text-xs text-surface-400 space-y-1">
-          <p>Source: {selectedItem.source}</p>
-          <p>Published: {formatDate(selectedItem.published_at)}</p>
-          <p>Collected: {formatDate(selectedItem.collected_at)}</p>
-        </div>
- 
-        {#if selectedItem.tags?.length}
-          <div class="mt-3 flex flex-wrap gap-2">
-            {#each selectedItem.tags as tag}
-              <span class="px-2 py-0.5 text-xs rounded-full bg-surface-100 text-surface-600 dark:bg-surface-700 dark:text-surface-300">{tag}</span>
-            {/each}
-          </div>
-        {/if}
- 
-        <p class="mt-4 text-sm text-surface-700 dark:text-surface-300 whitespace-pre-wrap">{selectedItem.summary || selectedItem.content_text || "No summary available."}</p>
- 
-        <a href={selectedItem.url} target="_blank" rel="noopener" class="inline-flex mt-4 px-3 py-1.5 text-xs font-medium rounded-lg bg-primary-600 text-white hover:bg-primary-700">
-          Open source
-        </a>
-      {:else}
-        <p class="text-sm text-surface-400">Select an item to view details.</p>
-      {/if}
-    </div>
-  </div>
-</div>
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/dashboard/frontend/coverage/lcov-report/src/routes/LiteLLM.svelte.html b/dashboard/frontend/coverage/lcov-report/src/routes/LiteLLM.svelte.html deleted file mode 100644 index 5959fdb..0000000 --- a/dashboard/frontend/coverage/lcov-report/src/routes/LiteLLM.svelte.html +++ /dev/null @@ -1,409 +0,0 @@ - - - - - - Code coverage report for src/routes/LiteLLM.svelte - - - - - - - - - -
-
-

All files / src/routes LiteLLM.svelte

-
- -
- 0% - Statements - 0/58 -
- - -
- 0% - Branches - 0/1 -
- - -
- 0% - Functions - 0/1 -
- - -
- 0% - Lines - 0/58 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  - 
<script>
-  import { onMount } from "svelte";
-  import { getLiteLLMHealth } from "../lib/api.js";
- 
-  let loading = $state(true);
-  let health = $state(null);
-  let requestError = $state("");
- 
-  async function loadHealth() {
-    loading = true;
-    requestError = "";
-    try {
-      health = await getLiteLLMHealth();
-    } catch (e) {
-      requestError = e?.body?.detail || e?.message || "Failed to load LiteLLM health";
-      health = null;
-    } finally {
-      loading = false;
-    }
-  }
- 
-  function statusBadgeClass(status) {
-    if (status === "up") {
-      return "bg-emerald-100 text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-300";
-    }
-    if (status === "auth_required") {
-      return "bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-300";
-    }
-    return "bg-rose-100 text-rose-700 dark:bg-rose-900/40 dark:text-rose-300";
-  }
- 
-  function valueOrDash(value) {
-    return value === null || value === undefined || value === "" ? "—" : value;
-  }
- 
-  onMount(loadHealth);
-</script>
- 
-<div class="space-y-6">
-  <div class="flex items-center justify-between">
-    <div>
-      <h1 class="text-2xl font-bold text-surface-900 dark:text-white tracking-tight">LiteLLM</h1>
-      <p class="text-sm text-surface-400 mt-1">Gateway health and connectivity status</p>
-      <div class="mt-2 flex flex-wrap items-center gap-2 text-[11px] font-medium">
-        <span class="px-2 py-0.5 rounded-full bg-emerald-100 text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-300">up</span>
-        <span class="px-2 py-0.5 rounded-full bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-300">auth_required</span>
-        <span class="px-2 py-0.5 rounded-full bg-rose-100 text-rose-700 dark:bg-rose-900/40 dark:text-rose-300">down</span>
-      </div>
-    </div>
-    <button onclick={loadHealth} disabled={loading} class="px-3 py-1.5 text-xs font-medium text-primary-600 bg-primary-50 hover:bg-primary-100 rounded-lg transition-colors disabled:opacity-50 dark:bg-primary-900/30 dark:text-primary-300">
-      {loading ? "Loading..." : "Refresh"}
-    </button>
-  </div>
- 
-  {#if requestError}
-    <div class="bg-rose-50 dark:bg-rose-900/20 border border-rose-200 dark:border-rose-700 rounded-xl p-4 shadow-sm">
-      <p class="text-sm font-medium text-rose-700 dark:text-rose-300">Failed to fetch LiteLLM health</p>
-      <p class="text-xs text-rose-600 dark:text-rose-400 mt-1">{requestError}</p>
-    </div>
-  {/if}
- 
-  <div class="bg-white dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 p-5 shadow-sm">
-    {#if loading && !health}
-      <div class="space-y-3">
-        <div class="h-6 w-36 rounded bg-surface-100 dark:bg-surface-700 animate-pulse"></div>
-        <div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
-          {#each Array(6) as _}
-            <div class="h-16 rounded-lg bg-surface-100 dark:bg-surface-700 animate-pulse"></div>
-          {/each}
-        </div>
-      </div>
-    {:else if health}
-      <div class="flex items-center justify-between mb-4">
-        <h3 class="text-sm font-semibold text-surface-700 dark:text-surface-200">Health Status</h3>
-        <span class="px-2.5 py-1 text-xs font-semibold rounded-full {statusBadgeClass(health.status)}">{valueOrDash(health.status)}</span>
-      </div>
- 
-      <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
-        <div class="rounded-lg border border-surface-200 dark:border-surface-700 p-3">
-          <p class="text-xs font-medium text-surface-400 uppercase tracking-wider">OK</p>
-          <p class="text-sm font-semibold text-surface-800 dark:text-surface-100 mt-1">{String(Boolean(health.ok))}</p>
-        </div>
- 
-        <div class="rounded-lg border border-surface-200 dark:border-surface-700 p-3">
-          <p class="text-xs font-medium text-surface-400 uppercase tracking-wider">Latency</p>
-          <p class="text-sm font-semibold text-surface-800 dark:text-surface-100 mt-1">{health.latency_ms === null || health.latency_ms === undefined ? "—" : `${health.latency_ms} ms`}</p>
-        </div>
- 
-        <div class="rounded-lg border border-surface-200 dark:border-surface-700 p-3">
-          <p class="text-xs font-medium text-surface-400 uppercase tracking-wider">Endpoint</p>
-          <p class="text-sm font-semibold text-surface-800 dark:text-surface-100 mt-1">{valueOrDash(health.endpoint)}</p>
-        </div>
- 
-        <div class="rounded-lg border border-surface-200 dark:border-surface-700 p-3">
-          <p class="text-xs font-medium text-surface-400 uppercase tracking-wider">HTTP Status</p>
-          <p class="text-sm font-semibold text-surface-800 dark:text-surface-100 mt-1">{valueOrDash(health.http_status)}</p>
-        </div>
- 
-        <div class="rounded-lg border border-surface-200 dark:border-surface-700 p-3 sm:col-span-2 lg:col-span-2">
-          <p class="text-xs font-medium text-surface-400 uppercase tracking-wider">Error</p>
-          <p class="text-sm font-semibold text-surface-800 dark:text-surface-100 mt-1 break-all">{valueOrDash(health.error)}</p>
-        </div>
-      </div>
-    {:else}
-      <p class="text-sm text-surface-400">No health data available.</p>
-    {/if}
-  </div>
-</div>
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/dashboard/frontend/coverage/lcov-report/src/routes/Login.svelte.html b/dashboard/frontend/coverage/lcov-report/src/routes/Login.svelte.html deleted file mode 100644 index 1bf06cf..0000000 --- a/dashboard/frontend/coverage/lcov-report/src/routes/Login.svelte.html +++ /dev/null @@ -1,775 +0,0 @@ - - - - - - Code coverage report for src/routes/Login.svelte - - - - - - - - - -
-
-

All files / src/routes Login.svelte

-
- -
- 41.02% - Statements - 48/117 -
- - -
- 30% - Branches - 3/10 -
- - -
- 33.33% - Functions - 2/6 -
- - -
- 41.02% - Lines - 48/117 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -178 -179 -180 -181 -182 -183 -184 -185 -186 -187 -188 -189 -190 -191 -192 -193 -194 -195 -196 -197 -198 -199 -200 -201 -202 -203 -204 -205 -206 -207 -208 -209 -210 -211 -212 -213 -214 -215 -216 -217 -218 -219 -220 -221 -222 -223 -224 -225 -226 -227 -228 -229 -230 -2313x -  -  -  -3x -3x -3x -3x -3x -3x -3x -  -3x -  -  -  -  -  -3x -  -  -  -  -  -  -3x -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -3x -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -3x -  -1x -1x -  -  -  -  -  -  -  -  -  -  -  -  -1x -1x -1x -  -1x -1x -  -  -1x -1x -  -  -  -  -  -  -  -  -  -  -  -  -1x -  -  -  -  -1x -1x -1x -1x -1x -  -1x -  -  -  -  -1x -  -  -  -  -1x -  -1x -  -  -  -  -1x -  -  -  -  -  -1x -  -1x -  -  -  -  -  -  -  -  -1x -  -  -  -  -1x -1x -  -  -  -  -  -  -  -1x -  -1x -  -  -1x -1x -  -  -  -  -1x -  -  -  -  -1x -1x -1x -  -  -  -  -  -1x -1x -  -  -  -  -  -  -  - 
<script>
-  import { login, setToken } from "../lib/api.js";
-  import { fade, fly } from "svelte/transition";
- 
-  let username = $state("");
-  let password = $state("");
-  let totp_code = $state("");
-  let error = $state("");
-  let loading = $state(false);
-  let require2FA = $state(false);
-  let showPasswordForm = $state(!window.isSecureContext);
- 
-  function base64urlToBuffer(b64) {
-    const s = b64.replace(/-/g, '+').replace(/_/g, '/');
-    const raw = atob(s);
-    return Uint8Array.from(raw, c => c.charCodeAt(0)).buffer;
-  }
- 
-  function bufferToBase64url(buf) {
-    const bytes = new Uint8Array(buf);
-    let s = '';
-    bytes.forEach(b => s += String.fromCharCode(b));
-    return btoa(s).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
-  }
- 
-  async function loginWithPasskey() {
-    loading = true;
-    error = "";
-    try {
-      const r = await fetch("/api/auth/passkey/login/options", { method: "POST" });
-      if (!r.ok) throw new Error("No passkeys registered");
-      const opts = await r.json();
-      opts.challenge = base64urlToBuffer(opts.challenge);
-      if (opts.allowCredentials) {
-        opts.allowCredentials = opts.allowCredentials.map(c => ({
-          ...c, id: base64urlToBuffer(c.id)
-        }));
-      }
-      const cred = await navigator.credentials.get({ publicKey: opts });
-      const body = {
-        id: cred.id,
-        rawId: bufferToBase64url(cred.rawId),
-        type: cred.type,
-        response: {
-          authenticatorData: bufferToBase64url(cred.response.authenticatorData),
-          clientDataJSON: bufferToBase64url(cred.response.clientDataJSON),
-          signature: bufferToBase64url(cred.response.signature),
-          userHandle: cred.response.userHandle ? bufferToBase64url(cred.response.userHandle) : null,
-        },
-      };
-      const res = await fetch("/api/auth/passkey/login/verify", {
-        method: "POST",
-        headers: { "Content-Type": "application/json" },
-        body: JSON.stringify(body),
-      });
-      if (!res.ok) { const e = await res.json(); throw new Error(e.detail || "Passkey verification failed"); }
-      const data = await res.json();
-      setToken(data.access_token);
-      window.location.reload();
-    } catch (e) {
-      if (e.name === "NotAllowedError") { error = "Passkey authentication cancelled"; }
-      else { error = e.message || "Passkey login failed"; }
-      showPasswordForm = true;
-    } finally {
-      loading = false;
-    }
-  }
- 
-  async function handleSubmit(e) {
-    e.preventDefault();
-    loading = true;
-    error = "";
-    
-    try {
-      const payload = { username, password };
-      if (require2FA) payload.totp_code = totp_code;
-      
-      const res = await login(payload); // api.js helper sends JSON
-      
-      setToken(res.access_token);
-      // Determine if we need to reload or just update state. 
-      // Reload is safest to clear any stale state.
-      window.location.reload(); 
-      
-    } catch (e) {
-      if (e.status === 403 && e.body?.detail === "2FA code required") {
-         require2FA = true;
-         // Clear error if any
-         error = "";
-      } else {
-         error = e.body?.detail || "Login failed. Please check your credentials.";
-         // Reset 2FA state on general failure (maybe password was wrong this time)
-         if (!require2FA) {
-            password = "";
-         }
-      }
-    } finally {
-      loading = false;
-    }
-  }
-</script>
- 
-<div class="flex min-h-screen items-center justify-center bg-surface-50 dark:bg-surface-950 px-4">
-  <div class="w-full max-w-sm space-y-8 bg-white dark:bg-surface-900 p-8 rounded-2xl shadow-xl border border-surface-200 dark:border-surface-800" in:fly={{ y: 20, duration: 400 }}>
-    <div class="text-center">
-      <div class="mx-auto h-12 w-12 bg-gradient-to-br from-primary-500 to-primary-600 rounded-xl flex items-center justify-center shadow-lg mb-4">
-        <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
-          <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
-        </svg>
-      </div>
-      <h2 class="text-2xl font-bold tracking-tight text-surface-900 dark:text-white">Sign in to NAS</h2>
-      <p class="mt-2 text-sm text-surface-600 dark:text-surface-400">
-        Enter your credentials to access the dashboard
-      </p>
-    </div>
- 
-    <div class="mt-8 space-y-6">
-      {#if !showPasswordForm && !require2FA}
-        <button
-          type="button"
-          disabled={loading}
-          onclick={loginWithPasskey}
-          class="flex w-full justify-center rounded-lg bg-surface-800 dark:bg-surface-700 px-3 py-2.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-surface-700 dark:hover:bg-surface-600 disabled:opacity-70 transition-all active:scale-[0.98]"
-        >
-          {#if loading}
-            <svg class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" fill="none" viewBox="0 0 24 24">
-              <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
-              <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
-            </svg>
-          {/if}
-          Sign in with Passkey
-        </button>
- 
-        <div class="relative">
-          <div class="absolute inset-0 flex items-center"><div class="w-full border-t border-surface-200 dark:border-surface-700"></div></div>
-          <div class="relative flex justify-center text-xs"><span class="bg-white dark:bg-surface-900 px-2 text-surface-400">or</span></div>
-        </div>
- 
-        <button type="button" onclick={() => showPasswordForm = true} class="flex w-full justify-center text-sm text-primary-500 hover:text-primary-400">
-          Use password instead
-        </button>
-      {/if}
- 
-      {#if showPasswordForm || require2FA}
-      <form class="space-y-6" onsubmit={handleSubmit}>
-      <div class="space-y-4">
-        {#if !require2FA}
-          <div>
-            <label for="username" class="sr-only">Username</label>
-            <input
-              id="username"
-              name="username"
-              type="text"
-              required
-              bind:value={username}
-              class="relative block w-full rounded-lg border-0 py-2.5 px-3 text-surface-900 dark:text-white ring-1 ring-inset ring-surface-300 dark:ring-surface-700 placeholder:text-surface-400 focus:z-10 focus:ring-2 focus:ring-inset focus:ring-primary-600 dark:bg-surface-800 sm:text-sm sm:leading-6"
-              placeholder="Username"
-            />
-          </div>
-          <div>
-            <label for="password" class="sr-only">Password</label>
-            <input
-              id="password"
-              name="password"
-              type="password"
-              required
-              bind:value={password}
-              class="relative block w-full rounded-lg border-0 py-2.5 px-3 text-surface-900 dark:text-white ring-1 ring-inset ring-surface-300 dark:ring-surface-700 placeholder:text-surface-400 focus:z-10 focus:ring-2 focus:ring-inset focus:ring-primary-600 dark:bg-surface-800 sm:text-sm sm:leading-6"
-              placeholder="Password"
-            />
-          </div>
-        {:else}
-          <div in:fade>
-            <label for="totp" class="block text-sm font-medium leading-6 text-surface-900 dark:text-white text-center mb-2">Authenticator Code</label>
-            <input
-              id="totp"
-              name="totp"
-              type="text"
-              inputmode="numeric"
-              pattern="[0-9]*"
-              autocomplete="one-time-code"
-              required
-              autofocus
-              bind:value={totp_code}
-              class="block w-full text-center tracking-[0.5em] text-2xl font-mono rounded-lg border-0 py-3 text-surface-900 dark:text-white ring-1 ring-inset ring-surface-300 dark:ring-surface-700 placeholder:text-surface-400 focus:ring-2 focus:ring-inset focus:ring-primary-600 dark:bg-surface-800"
-              placeholder="000000"
-              maxlength="6"
-            />
-            <p class="mt-4 text-center text-sm text-surface-500">
-              <button type="button" class="text-primary-500 hover:text-primary-400" onclick={() => require2FA = false}>
-                Back to credentials
-              </button>
-            </p>
-          </div>
-        {/if}
-      </div>
- 
-        <button
-          type="submit"
-          disabled={loading}
-          class="flex w-full justify-center rounded-lg bg-primary-600 px-3 py-2.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-primary-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600 disabled:opacity-70 transition-all active:scale-[0.98]"
-        >
-          {#if loading}
-            <svg class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" fill="none" viewBox="0 0 24 24">
-              <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
-              <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
-            </svg>
-          {/if}
-          {require2FA ? "Verify Code" : "Sign in"}
-        </button>
-      </form>
-      {/if}
- 
-      {#if error}
-        <div class="rounded-md bg-red-50 dark:bg-red-900/30 p-3" in:fade>
-          <div class="flex">
-            <div class="flex-shrink-0">
-              <svg class="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
-                <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
-              </svg>
-            </div>
-            <div class="ml-3">
-              <h3 class="text-sm font-medium text-red-800 dark:text-red-200">{error}</h3>
-            </div>
-          </div>
-        </div>
-      {/if}
-    </div>
-  </div>
-</div>
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/dashboard/frontend/coverage/lcov-report/src/routes/OPC.svelte.html b/dashboard/frontend/coverage/lcov-report/src/routes/OPC.svelte.html deleted file mode 100644 index 519fb34..0000000 --- a/dashboard/frontend/coverage/lcov-report/src/routes/OPC.svelte.html +++ /dev/null @@ -1,709 +0,0 @@ - - - - - - Code coverage report for src/routes/OPC.svelte - - - - - - - - - -
-
-

All files / src/routes OPC.svelte

-
- -
- 0% - Statements - 0/129 -
- - -
- 0% - Branches - 0/1 -
- - -
- 0% - Functions - 0/1 -
- - -
- 0% - Lines - 0/129 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -178 -179 -180 -181 -182 -183 -184 -185 -186 -187 -188 -189 -190 -191 -192 -193 -194 -195 -196 -197 -198 -199 -200 -201 -202 -203 -204 -205 -206 -207 -208 -209  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  - 
<script>
-  import { onMount, onDestroy } from "svelte";
-  import KanbanBoard from "../components/opc/KanbanBoard.svelte";
-  import TaskModal from "../components/opc/TaskModal.svelte";
-  import AgentPanel from "../components/opc/AgentPanel.svelte";
-  import { getTasks, getAgents } from "../lib/opc-api.js";
-  import * as opcWs from "../lib/opc-ws.js";
- 
-  let tasks = $state([]);
-  let agents = $state([]);
-  let loading = $state(true);
-  let showTaskModal = $state(false);
-  let editingTask = $state(null);
-  let showAgentPanel = $state(
-    typeof localStorage !== 'undefined'
-      ? localStorage.getItem('opc_showAgentPanel') !== 'false'
-      : true
-  );
-  let unsubscribe = null;
- 
-  // Persist agent panel visibility
-  $effect(() => {
-    if (typeof localStorage !== 'undefined') {
-      localStorage.setItem('opc_showAgentPanel', showAgentPanel);
-    }
-  });
- 
-  async function loadData() {
-    loading = true;
-    try {
-      const [tasksRes, agentsRes] = await Promise.all([
-        getTasks(),
-        getAgents()
-      ]);
-      tasks = tasksRes.items || [];
-      agents = agentsRes.items || [];
-    } catch (e) {
-      console.error("Failed to load data:", e);
-    } finally {
-      loading = false;
-    }
-  }
- 
-  function handleWebSocketMessage(message) {
-    if (message.type === "task_update") {
-      const { task_id, action, data } = message;
- 
-      if (action === "created") {
-        tasks = [...tasks, data];
-      } else if (action === "updated" || action === "moved") {
-        tasks = tasks.map(t => t.id === task_id ? data : t);
-      } else if (action === "deleted") {
-        tasks = tasks.filter(t => t.id !== task_id);
-      }
-    } else if (message.type === "agent_execution") {
-      // Could show agent status in UI
-      console.log("Agent execution update:", message);
-    }
-  }
- 
-  function handleCreateTask() {
-    editingTask = null;
-    showTaskModal = true;
-  }
- 
-  function handleEditTask(task) {
-    editingTask = task;
-    showTaskModal = true;
-  }
- 
-  function handleDeleteTask(taskId) {
-    tasks = tasks.filter(t => t.id !== taskId);
-  }
- 
-  function handleAssignTask(taskId, assignedTo, assignedType) {
-    const task = tasks.find(t => t.id === taskId);
-    if (task) {
-      task.assigned_to = assignedTo;
-      task.assigned_type = assignedType;
-      tasks = [...tasks];
-    }
-  }
- 
-  function handleTaskMoved(taskId, newStatus) {
-    const task = tasks.find(t => t.id === taskId);
-    if (task) {
-      task.status = newStatus;
-      tasks = [...tasks];
-    }
-  }
- 
-  function handleModalClose() {
-    showTaskModal = false;
-    editingTask = null;
-  }
- 
-  function handleModalSave() {
-    loadData();
-  }
- 
-  onMount(() => {
-    loadData();
- 
-    // Connect WebSocket
-    opcWs.connect();
-    unsubscribe = opcWs.subscribe(handleWebSocketMessage);
-  });
- 
-  onDestroy(() => {
-    if (unsubscribe) {
-      unsubscribe();
-    }
-    opcWs.disconnect();
-  });
-</script>
- 
-<div class="space-y-6 h-full flex flex-col">
-  <!-- Header -->
-  <div class="flex items-center justify-between">
-    <div>
-      <h1 class="text-2xl font-bold text-surface-900 dark:text-surface-100">
-        OPC Management
-      </h1>
-      <p class="text-surface-600 dark:text-surface-400 mt-1">
-        Manage your tasks with AI agents
-      </p>
-    </div>
-    <div class="flex gap-2">
-      <button
-        onclick={() => showAgentPanel = !showAgentPanel}
-        class="px-4 py-2 bg-surface-200 dark:bg-surface-700 text-surface-900 dark:text-surface-100 rounded-lg hover:bg-surface-300 dark:hover:bg-surface-600 flex items-center gap-2"
-      >
-        <span>🤖</span>
-        <span>{showAgentPanel ? "Hide" : "Show"} Agents</span>
-      </button>
-      <button
-        onclick={handleCreateTask}
-        class="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 flex items-center gap-2"
-      >
-        <span>➕</span>
-        <span>New Task</span>
-      </button>
-    </div>
-  </div>
- 
-  <!-- Stats -->
-  <div class="grid grid-cols-1 md:grid-cols-4 gap-4">
-    <div class="bg-white dark:bg-surface-800 rounded-lg p-4 border border-surface-200 dark:border-surface-700">
-      <div class="text-sm text-surface-600 dark:text-surface-400">Total Tasks</div>
-      <div class="text-2xl font-bold text-surface-900 dark:text-surface-100 mt-1">
-        {tasks.length}
-      </div>
-    </div>
-    <div class="bg-white dark:bg-surface-800 rounded-lg p-4 border border-surface-200 dark:border-surface-700">
-      <div class="text-sm text-surface-600 dark:text-surface-400">Parking Lot</div>
-      <div class="text-2xl font-bold text-slate-600 dark:text-slate-400 mt-1">
-        {tasks.filter(t => t.status === "parking_lot").length}
-      </div>
-    </div>
-    <div class="bg-white dark:bg-surface-800 rounded-lg p-4 border border-surface-200 dark:border-surface-700">
-      <div class="text-sm text-surface-600 dark:text-surface-400">In Progress</div>
-      <div class="text-2xl font-bold text-blue-600 dark:text-blue-400 mt-1">
-        {tasks.filter(t => t.status === "in_progress").length}
-      </div>
-    </div>
-    <div class="bg-white dark:bg-surface-800 rounded-lg p-4 border border-surface-200 dark:border-surface-700">
-      <div class="text-sm text-surface-600 dark:text-surface-400">Done</div>
-      <div class="text-2xl font-bold text-emerald-600 dark:text-emerald-400 mt-1">
-        {tasks.filter(t => t.status === "done").length}
-      </div>
-    </div>
-  </div>
- 
-  <!-- Kanban Board -->
-  {#if loading}
-    <div class="flex-1 flex items-center justify-center">
-      <div class="text-surface-600 dark:text-surface-400">Loading...</div>
-    </div>
-  {:else}
-    <div class="flex-1 overflow-hidden">
-      <KanbanBoard
-        {tasks}
-        {agents}
-        onEdit={handleEditTask}
-        onDelete={handleDeleteTask}
-        onAssign={handleAssignTask}
-        onTaskMoved={handleTaskMoved}
-      />
-    </div>
-  {/if}
- 
-  <!-- Agent Panel -->
-  {#if showAgentPanel}
-    <div class="mt-6">
-      <AgentPanel />
-    </div>
-  {/if}
-</div>
- 
-<!-- Task Modal -->
-{#if showTaskModal}
-  <TaskModal
-    task={editingTask}
-    {agents}
-    onClose={handleModalClose}
-    onSave={handleModalSave}
-  />
-{/if}
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/dashboard/frontend/coverage/lcov-report/src/routes/OpenClaw.svelte.html b/dashboard/frontend/coverage/lcov-report/src/routes/OpenClaw.svelte.html deleted file mode 100644 index e27d18a..0000000 --- a/dashboard/frontend/coverage/lcov-report/src/routes/OpenClaw.svelte.html +++ /dev/null @@ -1,283 +0,0 @@ - - - - - - Code coverage report for src/routes/OpenClaw.svelte - - - - - - - - - -
-
-

All files / src/routes OpenClaw.svelte

-
- -
- 0% - Statements - 0/22 -
- - -
- 0% - Branches - 0/1 -
- - -
- 0% - Functions - 0/1 -
- - -
- 0% - Lines - 0/22 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  - 
<script>
-  import { onMount } from "svelte";
-  import { get } from "../lib/api.js";
- 
-  let isTailscale = location.hostname.startsWith("100.");
-  let isLocal = location.hostname === "localhost" || location.hostname === "127.0.0.1" || location.hostname.startsWith("192.168.");
-  let canAccess = isTailscale || isLocal;
-  let token = $state("");
- 
-  onMount(async () => {
-    if (canAccess) {
-      const res = await get("/system/openclaw-token");
-      token = res?.token || "";
-    }
-  });
- 
-  function clawUrl() {
-    const base = `${location.protocol}//${location.hostname}:3100`;
-    return token ? `${base}/#token=${token}` : base;
-  }
-</script>
- 
-<div class="space-y-4">
-  <div>
-    <h1 class="text-2xl font-bold text-surface-900 tracking-tight">OpenClaw</h1>
-    <p class="text-sm text-surface-400 mt-1">AI Assistant</p>
-  </div>
- 
-  {#if canAccess}
-    <div class="flex flex-col items-center justify-center p-12 text-center bg-surface-50 dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 h-[calc(100vh-10rem)]">
-      <div class="h-16 w-16 bg-primary-100 dark:bg-primary-900/30 text-primary-600 dark:text-primary-400 rounded-2xl flex items-center justify-center mb-6">
-        <svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
-          <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" />
-        </svg>
-      </div>
-      <h2 class="text-xl font-semibold text-surface-900 dark:text-white mb-2">OpenClaw Ready</h2>
-      <p class="text-surface-600 dark:text-surface-400 max-w-md mb-8">
-        OpenClaw's security settings prevent it from being embedded inside the dashboard. Click below to launch the assistant securely in a new tab.
-      </p>
-      <a
-        href={clawUrl()}
-        target="_blank"
-        rel="noopener noreferrer"
-        class="inline-flex items-center justify-center px-6 py-3 border border-transparent text-base font-medium rounded-lg text-white bg-primary-600 hover:bg-primary-700 shadow-sm transition-colors"
-      >
-        Launch OpenClaw
-        <svg xmlns="http://www.w3.org/2000/svg" class="ml-2 -mr-1 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
-          <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
-        </svg>
-      </a>
-    </div>
-  {:else}
-    <div class="flex flex-col items-center justify-center p-12 text-center bg-surface-50 dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 h-[calc(100vh-10rem)]">
-      <svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16 text-surface-400 mb-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
-        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
-      </svg>
-      <h2 class="text-xl font-semibold text-surface-900 dark:text-white mb-2">Tailscale Network Required</h2>
-      <p class="text-surface-600 dark:text-surface-400 max-w-md">
-        For security reasons, OpenClaw is only accessible when connected directly to the NAS via the internal Tailscale network.
-      </p>
-      <p class="mt-4 text-sm text-surface-500">
-        Connect to Tailscale and visit <a href="http://100.78.131.124:4000" class="text-primary-500 hover:underline font-medium">http://100.78.131.124:4000</a> to use the AI Assistant.
-      </p>
-    </div>
-  {/if}
-</div>
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/dashboard/frontend/coverage/lcov-report/src/routes/Security.svelte.html b/dashboard/frontend/coverage/lcov-report/src/routes/Security.svelte.html deleted file mode 100644 index f999709..0000000 --- a/dashboard/frontend/coverage/lcov-report/src/routes/Security.svelte.html +++ /dev/null @@ -1,934 +0,0 @@ - - - - - - Code coverage report for src/routes/Security.svelte - - - - - - - - - -
-
-

All files / src/routes Security.svelte

-
- -
- 0% - Statements - 0/171 -
- - -
- 0% - Branches - 0/1 -
- - -
- 0% - Functions - 0/1 -
- - -
- 0% - Lines - 0/171 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -178 -179 -180 -181 -182 -183 -184 -185 -186 -187 -188 -189 -190 -191 -192 -193 -194 -195 -196 -197 -198 -199 -200 -201 -202 -203 -204 -205 -206 -207 -208 -209 -210 -211 -212 -213 -214 -215 -216 -217 -218 -219 -220 -221 -222 -223 -224 -225 -226 -227 -228 -229 -230 -231 -232 -233 -234 -235 -236 -237 -238 -239 -240 -241 -242 -243 -244 -245 -246 -247 -248 -249 -250 -251 -252 -253 -254 -255 -256 -257 -258 -259 -260 -261 -262 -263 -264 -265 -266 -267 -268 -269 -270 -271 -272 -273 -274 -275 -276 -277 -278 -279 -280 -281 -282 -283 -284  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  - 
<script>
-  import { get } from "../lib/api.js";
-  import { onMount } from "svelte";
- 
-  let logs = $state([]);
-  let stats = $state(null);
-  let loading = $state(false);
-  let filter = $state("all");
-  let ipFilter = $state("");
-  let startDate = $state("");
-  let endDate = $state("");
-  let filterError = $state("");
- 
-  onMount(loadAll);
- 
-  async function loadAll() {
-    loading = true;
-    await Promise.all([loadStats(), loadLogs()]);
-    loading = false;
-  }
- 
-  async function loadStats() {
-    try {
-      stats = await get("/security/stats");
-    } catch (e) {
-      console.error("Failed to load security stats:", e);
-    }
-  }
- 
-  function buildLogsQuery() {
-    const params = new URLSearchParams({ limit: "300" });
-    if (filter !== "all") params.set("type", filter);
-    if (ipFilter.trim()) params.set("ip", ipFilter.trim());
-    if (startDate) params.set("start_date", startDate);
-    if (endDate) params.set("end_date", endDate);
-    return params.toString();
-  }
- 
-  function validateFilters() {
-    if (startDate && endDate && startDate > endDate) {
-      filterError = "From date must be on or before To date";
-      return false;
-    }
-    filterError = "";
-    return true;
-  }
- 
-  async function loadLogs() {
-    if (!validateFilters()) {
-      logs = [];
-      return;
-    }
- 
-    try {
-      const res = await get(`/security/logs?${buildLogsQuery()}`);
-      logs = res.logs;
-    } catch (e) {
-      console.error("Failed to load security logs:", e);
-    }
-  }
- 
-  async function applyTypeFilter(nextFilter) {
-    filter = nextFilter;
-    await loadLogs();
-  }
- 
-  async function applyIpFilter(ip) {
-    ipFilter = ip;
-    await loadLogs();
-  }
- 
-  async function clearFilters() {
-    filter = "all";
-    ipFilter = "";
-    startDate = "";
-    endDate = "";
-    filterError = "";
-    await loadLogs();
-  }
- 
-  let maxTimelineCount = $derived(
-    stats ? Math.max(1, ...stats.timeline.map(t => t.count)) : 1
-  );
- 
-  let hasActiveFilters = $derived(
-    filter !== "all" || !!ipFilter.trim() || !!startDate || !!endDate
-  );
- 
-  function isBlockedScannerEvent(entry) {
-    if (entry.type !== "unauthorized") return false;
- 
-    const message = (entry.message || "").toLowerCase();
-    return [
-      "/.git/",
-      "phpinfo",
-      "debug.log",
-      "laravel.log",
-      "stripe",
-      "swagger.json",
-      "wp-config",
-      "credentials",
-      "secrets",
-      "config",
-      "@vite",
-      "manifest.json",
-    ].some((needle) => message.includes(needle));
-  }
- 
-  const logTypeFilters = ["all", "auth_failure", "suspicious_request", "unauthorized"];
-  const logTypeButtonLabels = {
-    all: "All",
-    auth_failure: "Auth",
-    suspicious_request: "Requests",
-    unauthorized: "Blocked",
-  };
- 
-  const typeLabels = { auth_failure: "Auth", suspicious_request: "Request", unauthorized: "Blocked", error: "Error" };
-  const typeColors = {
-    auth_failure: "text-red-400",
-    suspicious_request: "text-amber-400",
-    unauthorized: "text-orange-400",
-    error: "text-surface-500"
-  };
-</script>
- 
-<div class="space-y-6">
-  <div class="flex items-center justify-between">
-    <h2 class="text-lg font-bold text-surface-800 dark:text-surface-100">Security</h2>
-    <button onclick={loadAll} disabled={loading} class="px-3 py-1.5 text-xs font-medium text-primary-600 bg-primary-50 hover:bg-primary-100 rounded-lg transition-colors disabled:opacity-50 dark:bg-primary-900/30 dark:text-primary-300">
-      {loading ? "Loading..." : "Refresh"}
-    </button>
-  </div>
- 
-  <!-- Stats cards -->
-  {#if stats}
-    <div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
-      <div class="bg-white dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 p-5 shadow-sm">
-        <p class="text-xs font-medium text-surface-400 uppercase tracking-wider">Failed Logins (24h)</p>
-        <p class="text-2xl font-bold text-red-600 dark:text-red-400 mt-1">{stats.failed_logins_24h}</p>
-      </div>
-      <div class="bg-white dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 p-5 shadow-sm">
-        <p class="text-xs font-medium text-surface-400 uppercase tracking-wider">Suspicious Requests</p>
-        <p class="text-2xl font-bold text-amber-600 dark:text-amber-400 mt-1">{stats.suspicious_requests}</p>
-      </div>
-      <div class="bg-white dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 p-5 shadow-sm">
-        <p class="text-xs font-medium text-surface-400 uppercase tracking-wider">Unique IPs</p>
-        <p class="text-2xl font-bold text-surface-700 dark:text-surface-200 mt-1">{stats.unique_ips}</p>
-      </div>
-    </div>
-  {/if}
- 
-  <!-- Timeline chart -->
-  {#if stats?.timeline}
-    <div class="bg-white dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 p-5 shadow-sm">
-      <h3 class="text-sm font-semibold text-surface-700 dark:text-surface-200 mb-3">Activity (Last 24h)</h3>
-      <div class="flex items-end gap-1 h-24">
-        {#each [...stats.timeline].reverse() as bar}
-          <div class="flex-1 flex flex-col items-center justify-end h-full">
-            <div
-              class="w-full rounded-t transition-all {bar.count > 0 ? 'bg-primary-500 dark:bg-primary-400' : 'bg-surface-100 dark:bg-surface-700'}"
-              style="height: {bar.count > 0 ? Math.max(8, (bar.count / maxTimelineCount) * 100) : 4}%"
-              title="{bar.hours_ago}h ago: {bar.count} events"
-            ></div>
-          </div>
-        {/each}
-      </div>
-      <div class="flex justify-between mt-1">
-        <span class="text-[10px] text-surface-400">24h ago</span>
-        <span class="text-[10px] text-surface-400">now</span>
-      </div>
-    </div>
-  {/if}
- 
-  <!-- Top IPs -->
-  {#if stats?.top_ips?.length}
-    <div class="bg-white dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 p-5 shadow-sm">
-      <h3 class="text-sm font-semibold text-surface-700 dark:text-surface-200 mb-3">Top IPs</h3>
-      <div class="space-y-2">
-        {#each stats.top_ips as entry}
-          <div class="flex items-center gap-3 text-sm">
-            <button onclick={() => applyIpFilter(entry.ip)} class="text-xs font-mono text-surface-600 hover:text-primary-600 dark:text-surface-300 dark:hover:text-primary-300 w-36 shrink-0 text-left transition-colors">{entry.ip}</button>
-            <div class="flex-1 bg-surface-100 dark:bg-surface-700 rounded-full h-2 overflow-hidden">
-              <div class="bg-red-500 dark:bg-red-400 h-full rounded-full" style="width: {(entry.count / stats.top_ips[0].count) * 100}%"></div>
-            </div>
-            <span class="text-xs text-surface-500 w-8 text-right">{entry.count}</span>
-          </div>
-        {/each}
-      </div>
-    </div>
-  {/if}
- 
-  <!-- Log table -->
-  <div class="bg-white dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 p-5 shadow-sm">
-    <div class="flex items-start justify-between gap-3 mb-3">
-      <div>
-        <h3 class="text-sm font-semibold text-surface-700 dark:text-surface-200">Event Log</h3>
-        <p class="text-xs text-surface-400 mt-1">Filter recent events by type, IP, or date range.</p>
-      </div>
-      {#if hasActiveFilters}
-        <button onclick={clearFilters} class="px-2.5 py-1 text-xs rounded-md transition-colors bg-surface-100 text-surface-500 hover:bg-surface-200 dark:bg-surface-700 dark:text-surface-300 dark:hover:bg-surface-600">
-          Clear filters
-        </button>
-      {/if}
-    </div>
- 
-    <div class="flex flex-wrap gap-1.5 mb-3">
-      {#each logTypeFilters as f}
-        <button onclick={() => applyTypeFilter(f)} class="px-2.5 py-1 text-xs rounded-md transition-colors {filter === f ? 'bg-surface-800 text-white dark:bg-surface-200 dark:text-surface-900' : 'bg-surface-100 text-surface-500 hover:bg-surface-200 dark:bg-surface-700 dark:text-surface-300 dark:hover:bg-surface-600'}">
-          {logTypeButtonLabels[f]}
-        </button>
-      {/each}
-    </div>
- 
-    <div class="grid grid-cols-1 sm:grid-cols-3 gap-3 mb-3">
-      <label class="block">
-        <span class="block text-xs font-medium text-surface-500 mb-1">Filter by IP</span>
-        <input
-          type="text"
-          bind:value={ipFilter}
-          placeholder="e.g. 1.2.3.4"
-          onkeydown={async (e) => { if (e.key === 'Enter') await loadLogs(); }}
-          class="w-full px-3 py-2 text-sm border border-surface-200 dark:border-surface-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 dark:bg-surface-700 dark:text-white"
-        />
-      </label>
-      <label class="block">
-        <span class="block text-xs font-medium text-surface-500 mb-1">From</span>
-        <input
-          type="date"
-          bind:value={startDate}
-          onchange={loadLogs}
-          class="w-full px-3 py-2 text-sm border border-surface-200 dark:border-surface-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 dark:bg-surface-700 dark:text-white"
-        />
-      </label>
-      <label class="block">
-        <span class="block text-xs font-medium text-surface-500 mb-1">To</span>
-        <input
-          type="date"
-          bind:value={endDate}
-          onchange={loadLogs}
-          class="w-full px-3 py-2 text-sm border border-surface-200 dark:border-surface-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 dark:bg-surface-700 dark:text-white"
-        />
-      </label>
-    </div>
- 
-    <div class="flex items-center gap-2 mb-3">
-      <button onclick={loadLogs} class="px-3 py-1.5 text-xs font-medium text-primary-600 bg-primary-50 hover:bg-primary-100 rounded-lg transition-colors dark:bg-primary-900/30 dark:text-primary-300">
-        Apply filters
-      </button>
-      {#if ipFilter.trim()}
-        <button onclick={() => applyIpFilter(ipFilter.trim())} class="px-2.5 py-1 text-xs rounded-md transition-colors bg-surface-100 text-surface-500 hover:bg-surface-200 dark:bg-surface-700 dark:text-surface-300 dark:hover:bg-surface-600">
-          Use typed IP
-        </button>
-      {/if}
-    </div>
- 
-    {#if filterError}
-      <p class="text-sm text-rose-500 mb-3">{filterError}</p>
-    {/if}
- 
-    {#if logs.length > 0}
-      <div class="max-h-96 overflow-auto rounded-lg bg-surface-950 p-3 space-y-0.5">
-        {#each logs as e}
-          <div class="text-xs font-mono flex gap-2 leading-5">
-            <span class="{typeColors[e.type] || 'text-surface-500'} w-12 shrink-0">{typeLabels[e.type] || e.type}</span>
-            <span class="text-surface-500 shrink-0">{e.timestamp}</span>
-            {#if e.ip}
-              <button onclick={() => applyIpFilter(e.ip)} class="text-surface-400 hover:text-primary-300 shrink-0 w-28 text-left transition-colors">{e.ip}</button>
-            {/if}
-            {#if e.username}
-              <span class="text-red-400 shrink-0">{e.username}</span>
-            {/if}
-            {#if isBlockedScannerEvent(e)}
-              <span class="shrink-0 rounded-md bg-orange-500/15 px-1.5 py-0.5 text-[10px] font-medium text-orange-300 border border-orange-500/30">scanner</span>
-            {/if}
-            <span class="text-surface-300 truncate">{e.message}</span>
-          </div>
-        {/each}
-      </div>
-    {:else if !loading}
-      <p class="text-sm text-surface-400">{hasActiveFilters ? "No security events match the current filters" : "No security events found"}</p>
-    {/if}
-  </div>
-</div>
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/dashboard/frontend/coverage/lcov-report/src/routes/Settings.svelte.html b/dashboard/frontend/coverage/lcov-report/src/routes/Settings.svelte.html deleted file mode 100644 index 79728ca..0000000 --- a/dashboard/frontend/coverage/lcov-report/src/routes/Settings.svelte.html +++ /dev/null @@ -1,1597 +0,0 @@ - - - - - - Code coverage report for src/routes/Settings.svelte - - - - - - - - - -
-
-

All files / src/routes Settings.svelte

-
- -
- 0% - Statements - 0/362 -
- - -
- 0% - Branches - 0/1 -
- - -
- 0% - Functions - 0/1 -
- - -
- 0% - Lines - 0/362 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -178 -179 -180 -181 -182 -183 -184 -185 -186 -187 -188 -189 -190 -191 -192 -193 -194 -195 -196 -197 -198 -199 -200 -201 -202 -203 -204 -205 -206 -207 -208 -209 -210 -211 -212 -213 -214 -215 -216 -217 -218 -219 -220 -221 -222 -223 -224 -225 -226 -227 -228 -229 -230 -231 -232 -233 -234 -235 -236 -237 -238 -239 -240 -241 -242 -243 -244 -245 -246 -247 -248 -249 -250 -251 -252 -253 -254 -255 -256 -257 -258 -259 -260 -261 -262 -263 -264 -265 -266 -267 -268 -269 -270 -271 -272 -273 -274 -275 -276 -277 -278 -279 -280 -281 -282 -283 -284 -285 -286 -287 -288 -289 -290 -291 -292 -293 -294 -295 -296 -297 -298 -299 -300 -301 -302 -303 -304 -305 -306 -307 -308 -309 -310 -311 -312 -313 -314 -315 -316 -317 -318 -319 -320 -321 -322 -323 -324 -325 -326 -327 -328 -329 -330 -331 -332 -333 -334 -335 -336 -337 -338 -339 -340 -341 -342 -343 -344 -345 -346 -347 -348 -349 -350 -351 -352 -353 -354 -355 -356 -357 -358 -359 -360 -361 -362 -363 -364 -365 -366 -367 -368 -369 -370 -371 -372 -373 -374 -375 -376 -377 -378 -379 -380 -381 -382 -383 -384 -385 -386 -387 -388 -389 -390 -391 -392 -393 -394 -395 -396 -397 -398 -399 -400 -401 -402 -403 -404 -405 -406 -407 -408 -409 -410 -411 -412 -413 -414 -415 -416 -417 -418 -419 -420 -421 -422 -423 -424 -425 -426 -427 -428 -429 -430 -431 -432 -433 -434 -435 -436 -437 -438 -439 -440 -441 -442 -443 -444 -445 -446 -447 -448 -449 -450 -451 -452 -453 -454 -455 -456 -457 -458 -459 -460 -461 -462 -463 -464 -465 -466 -467 -468 -469 -470 -471 -472 -473 -474 -475 -476 -477 -478 -479 -480 -481 -482 -483 -484 -485 -486 -487 -488 -489 -490 -491 -492 -493 -494 -495 -496 -497 -498 -499 -500 -501 -502 -503 -504 -505  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  - 
<script>
-  import { post, get, del, put } from "../lib/api.js";
-  import { onMount } from "svelte";
-  import { currentUser } from "../lib/api.js";
- 
-  let currentPassword = $state("");
-  let newPassword = $state("");
-  let confirmPassword = $state("");
-  let message = $state("");
-  let error = $state("");
-  let saving = $state(false);
- 
-  let passkeys = $state([]);
-  let passkeyMsg = $state("");
-  let passkeyErr = $state("");
-  let passkeyLoading = $state(false);
- 
-  async function loadPasskeys() {
-    try {
-      const res = await get("/auth/passkey/list");
-      passkeys = res.passkeys;
-    } catch {}
-  }
- 
-  function base64urlToBuffer(b64) {
-    const s = b64.replace(/-/g, '+').replace(/_/g, '/');
-    const raw = atob(s);
-    return Uint8Array.from(raw, c => c.charCodeAt(0)).buffer;
-  }
- 
-  function bufferToBase64url(buf) {
-    const bytes = new Uint8Array(buf);
-    let s = '';
-    bytes.forEach(b => s += String.fromCharCode(b));
-    return btoa(s).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
-  }
- 
-  async function registerPasskey() {
-    alert("registerPasskey called!");
-    passkeyLoading = true;
-    passkeyMsg = ""; passkeyErr = "";
-    try {
-      alert("Checking PublicKeyCredential support...");
-      if (!window.PublicKeyCredential) {
-        alert("PublicKeyCredential NOT supported!");
-        throw new Error("Passkeys are not supported in this browser or context");
-      }
-      alert("Fetching registration options...");
-      const opts = await post("/auth/passkey/register/options");
-      alert("Got options: " + JSON.stringify(opts).substring(0, 100));
-      opts.challenge = base64urlToBuffer(opts.challenge);
-      opts.user.id = base64urlToBuffer(opts.user.id);
-      if (opts.excludeCredentials) {
-        opts.excludeCredentials = opts.excludeCredentials.map(c => ({ ...c, id: base64urlToBuffer(c.id) }));
-      }
-      const cred = await navigator.credentials.create({ publicKey: opts });
-      if (!cred) {
-        throw new Error("Passkey creation was cancelled");
-      }
-      const name = prompt("Name this passkey (e.g. MacBook, iPhone):", "Passkey") || "Passkey";
-      const body = {
-        id: cred.id,
-        rawId: bufferToBase64url(cred.rawId),
-        type: cred.type,
-        name,
-        response: {
-          attestationObject: bufferToBase64url(cred.response.attestationObject),
-          clientDataJSON: bufferToBase64url(cred.response.clientDataJSON),
-        },
-      };
-      await post("/auth/passkey/register/verify", body);
-      passkeyMsg = "Passkey registered successfully";
-      await loadPasskeys();
-    } catch (e) {
-      console.error("Passkey registration error:", e);
-      const errorMsg = e.body?.detail || e.message || "Failed to register passkey";
-      passkeyErr = errorMsg;
-      alert("Passkey Error: " + errorMsg + "\n\nFull error: " + JSON.stringify(e, null, 2));
-    } finally {
-      passkeyLoading = false;
-    }
-  }
- 
-  async function clearPasskeys() {
-    if (!confirm("Remove all passkeys?")) return;
-    passkeyLoading = true;
-    passkeyMsg = ""; passkeyErr = "";
-    try {
-      await post("/auth/passkey/clear");
-      passkeyMsg = "All passkeys removed";
-      passkeys = [];
-    } catch (e) {
-      passkeyErr = e.body?.detail || "Failed to remove passkeys";
-    }
-    passkeyLoading = false;
-  }
- 
-  async function deletePasskey(id, name) {
-    if (!confirm(`Remove passkey "${name}"?`)) return;
-    passkeyLoading = true;
-    passkeyMsg = ""; passkeyErr = "";
-    try {
-      await post("/auth/passkey/delete", { id });
-      passkeyMsg = `Passkey "${name}" removed`;
-      await loadPasskeys();
-    } catch (e) {
-      passkeyErr = e.body?.detail || "Failed to remove passkey";
-    }
-    passkeyLoading = false;
-  }
- 
-  async function changePassword() {
-    error = "";
-    message = "";
-    if (newPassword !== confirmPassword) { error = "Passwords do not match"; return; }
-    if (newPassword.length < 8) { error = "Password must be at least 8 characters"; return; }
-    saving = true;
-    try {
-      await post("/auth/change-password", { current_password: currentPassword, new_password: newPassword });
-      message = "Password changed successfully";
-      currentPassword = ""; newPassword = ""; confirmPassword = "";
-    } catch (e) {
-      error = e.body?.detail || "Failed to change password";
-    }
-    saving = false;
-  }
- 
-  let auditEntries = $state([]);
-  let auditLoading = $state(false);
-  let auditFilter = $state("all");
- 
-  const levelColors = { high: "text-rose-400", medium: "text-amber-400", low: "text-emerald-400", info: "text-surface-400" };
-  const levelLabels = { high: "HIGH", medium: "MED", low: "LOW", info: "INFO" };
- 
-  let filteredEntries = $derived(auditFilter === "all" ? auditEntries : auditEntries.filter(e => e.level === auditFilter));
- 
-  async function loadAuditLog() {
-    auditLoading = true;
-    try {
-      const res = await get("/system/audit-log?lines=200");
-      auditEntries = res.entries.reverse();
-    } catch {}
-    auditLoading = false;
-  }
- 
-  // Access Control
-  const allPages = ["dashboard", "docker", "files", "terminal", "security", "openclaw", "chat-digest", "gitea", "settings"];
-  const allSidebarLinks = ["Overview", "Docker", "Files", "Terminal", "Security", "Navidrome", "Jellyfin", "Audiobookshelf", "Immich", "OpenClaw", "Chat Digest", "Repos", "Gitea Web", "Stirling PDF", "Vaultwarden", "n8n", "Speedtest"];
-  let rbacConfig = $state(null);
-  let rbacLoading = $state(false);
-  let rbacMsg = $state("");
-  let rbacErr = $state("");
-  let editingUser = $state("");
-  let editPages = $state([]);
-  let newUsername = $state("");
-  let editingRole = $state("");
-  let editRolePages = $state([]);
-  let editRoleSidebarLinks = $state([]);
- 
-  async function loadRbac() {
-    rbacLoading = true;
-    try {
-      rbacConfig = await get("/auth/rbac/config");
-    } catch {}
-    rbacLoading = false;
-  }
- 
-  function startEdit(username, pages) {
-    editingUser = username;
-    editPages = [...pages];
-    newUsername = "";
-    editingRole = "";
-  }
- 
-  function startAdd() {
-    editingUser = "__new__";
-    editPages = ["dashboard"];
-    newUsername = "";
-    editingRole = "";
-  }
- 
-  function cancelEdit() {
-    editingUser = "";
-    editPages = [];
-    newUsername = "";
-    editingRole = "";
-    editRolePages = [];
-    editRoleSidebarLinks = [];
-  }
- 
-  function startEditRole(role, pages, sidebarLinks) {
-    editingRole = role;
-    editRolePages = pages === "*" ? ["*"] : [...pages];
-    editRoleSidebarLinks = sidebarLinks === "*" ? ["*"] : (sidebarLinks ? [...sidebarLinks] : ["*"]);
-    editingUser = "";
-  }
- 
-  function toggleRolePage(p) {
-    if (p === "*") {
-      editRolePages = ["*"];
-    } else {
-      if (editRolePages.includes("*")) editRolePages = [];
-      if (editRolePages.includes(p)) editRolePages = editRolePages.filter(x => x !== p);
-      else editRolePages = [...editRolePages, p];
-    }
-  }
- 
-  function toggleRoleSidebarLink(link) {
-    if (link === "*") {
-      editRoleSidebarLinks = ["*"];
-    } else {
-      if (editRoleSidebarLinks.includes("*")) editRoleSidebarLinks = [];
-      if (editRoleSidebarLinks.includes(link)) editRoleSidebarLinks = editRoleSidebarLinks.filter(x => x !== link);
-      else editRoleSidebarLinks = [...editRoleSidebarLinks, link];
-    }
-  }
- 
-  function togglePage(p) {
-    if (editPages.includes(p)) editPages = editPages.filter(x => x !== p);
-    else editPages = [...editPages, p];
-  }
- 
-  async function saveOverride() {
-    rbacMsg = ""; rbacErr = "";
-    const uname = editingUser === "__new__" ? newUsername.trim() : editingUser;
-    if (!uname) { rbacErr = "Username required"; return; }
-    if (!editPages.length) { rbacErr = "Select at least one page"; return; }
-    try {
-      await put(`/auth/rbac/overrides/${encodeURIComponent(uname)}`, { pages: editPages });
-      rbacMsg = `Saved override for ${uname}`;
-      cancelEdit();
-      await loadRbac();
-    } catch (e) {
-      rbacErr = e.body?.detail || "Failed to save";
-    }
-  }
- 
-  async function deleteOverride(username) {
-    if (!confirm(`Remove override for "${username}"?`)) return;
-    rbacMsg = ""; rbacErr = "";
-    try {
-      await del(`/auth/rbac/overrides/${encodeURIComponent(username)}`);
-      rbacMsg = `Removed override for ${username}`;
-      await loadRbac();
-    } catch (e) {
-      rbacErr = e.body?.detail || "Failed to delete";
-    }
-  }
- 
-  async function saveRoleDefault() {
-    rbacMsg = ""; rbacErr = "";
-    if (!editingRole) return;
-    if (!editRolePages.length) { rbacErr = "Select at least one page or '*' for all"; return; }
-    if (!editRoleSidebarLinks.length) { rbacErr = "Select at least one sidebar link or '*' for all"; return; }
-    try {
-      const pages = editRolePages.includes("*") ? "*" : editRolePages;
-      const sidebar_links = editRoleSidebarLinks.includes("*") ? "*" : editRoleSidebarLinks;
-      await put(`/auth/rbac/roles/${encodeURIComponent(editingRole)}`, { pages, sidebar_links });
-      rbacMsg = `Updated role default for ${editingRole}`;
-      cancelEdit();
-      await loadRbac();
-    } catch (e) {
-      rbacErr = e.body?.detail || "Failed to save role";
-    }
-  }
- 
-  onMount(() => {
-    loadPasskeys();
-    if (currentUser.role === "admin") loadRbac();
-  });
-</script>
- 
-<div class="space-y-6">
-  <div>
-    <h1 class="text-2xl font-bold text-surface-900 dark:text-white tracking-tight">Settings</h1>
-    <p class="text-sm text-surface-400 mt-1">Account and security settings</p>
-  </div>
- 
-  <div class="bg-white dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 p-6 shadow-sm max-w-md">
-    <h3 class="text-sm font-semibold text-surface-700 dark:text-surface-200 mb-4">Change Password</h3>
- 
-    {#if message}
-      <div class="text-sm text-emerald-600 bg-emerald-50 px-3 py-2 rounded-lg mb-4">{message}</div>
-    {/if}
-    {#if error}
-      <div class="text-sm text-rose-600 bg-rose-50 px-3 py-2 rounded-lg mb-4">{error}</div>
-    {/if}
- 
-    <form onsubmit={(e) => { e.preventDefault(); changePassword(); }} class="space-y-3">
-      <div>
-        <label class="block text-xs font-medium text-surface-500 mb-1">Current Password</label>
-        <input type="password" bind:value={currentPassword} required class="w-full px-3 py-2 text-sm border border-surface-200 dark:border-surface-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 dark:bg-surface-700 dark:text-white" />
-      </div>
-      <div>
-        <label class="block text-xs font-medium text-surface-500 mb-1">New Password</label>
-        <input type="password" bind:value={newPassword} required class="w-full px-3 py-2 text-sm border border-surface-200 dark:border-surface-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 dark:bg-surface-700 dark:text-white" />
-      </div>
-      <div>
-        <label class="block text-xs font-medium text-surface-500 mb-1">Confirm New Password</label>
-        <input type="password" bind:value={confirmPassword} required class="w-full px-3 py-2 text-sm border border-surface-200 dark:border-surface-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 dark:bg-surface-700 dark:text-white" />
-      </div>
-      <button type="submit" disabled={saving} class="w-full px-4 py-2 text-sm font-medium text-white bg-primary-600 hover:bg-primary-700 rounded-lg transition-colors disabled:opacity-50">
-        {saving ? "Saving..." : "Change Password"}
-      </button>
-    </form>
-  </div>
- 
-  <div class="bg-white dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 p-6 shadow-sm max-w-md">
-    <h3 class="text-sm font-semibold text-surface-700 dark:text-surface-200 mb-4">Passkeys</h3>
- 
-    {#if passkeyMsg}
-      <div class="text-sm text-emerald-600 bg-emerald-50 px-3 py-2 rounded-lg mb-4">{passkeyMsg}</div>
-    {/if}
-    {#if passkeyErr}
-      <div class="text-sm text-rose-600 bg-rose-50 px-3 py-2 rounded-lg mb-4">{passkeyErr}</div>
-    {/if}
- 
-    {#if passkeys.length > 0}
-      <div class="space-y-2 mb-4">
-        {#each passkeys as pk}
-          <div class="flex items-center justify-between px-3 py-2 bg-surface-50 dark:bg-surface-700 rounded-lg">
-            <div>
-              <span class="text-sm font-medium text-surface-700 dark:text-surface-200">{pk.name}</span>
-              {#if pk.created_at}
-                <span class="text-xs text-surface-400 ml-2">{new Date(pk.created_at).toLocaleDateString()}</span>
-              {/if}
-            </div>
-            <button onclick={() => deletePasskey(pk.id, pk.name)} disabled={passkeyLoading} class="text-xs text-rose-500 hover:text-rose-700 disabled:opacity-50">Remove</button>
-          </div>
-        {/each}
-      </div>
-    {:else}
-      <p class="text-sm text-surface-400 mb-4">No passkeys registered</p>
-    {/if}
- 
-    <div class="flex gap-2">
-      <button onclick={registerPasskey} disabled={passkeyLoading} class="px-4 py-2 text-sm font-medium text-white bg-primary-600 hover:bg-primary-700 rounded-lg transition-colors disabled:opacity-50">
-        Register new passkey
-      </button>
-      {#if passkeys.length > 1}
-        <button onclick={clearPasskeys} disabled={passkeyLoading} class="px-4 py-2 text-sm font-medium text-rose-600 bg-rose-50 hover:bg-rose-100 rounded-lg transition-colors disabled:opacity-50">
-          Remove all
-        </button>
-      {/if}
-    </div>
-  </div>
- 
-  {#if currentUser.role === "admin" && rbacConfig}
-  <div class="bg-white dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 p-6 shadow-sm">
-    <div class="flex items-center justify-between mb-4">
-      <h3 class="text-sm font-semibold text-surface-700 dark:text-surface-200">Access Control</h3>
-      <button onclick={loadRbac} disabled={rbacLoading} class="px-3 py-1.5 text-xs font-medium text-primary-600 bg-primary-50 hover:bg-primary-100 rounded-lg transition-colors disabled:opacity-50">
-        {rbacLoading ? "Loading..." : "Refresh"}
-      </button>
-    </div>
- 
-    {#if rbacMsg}
-      <div class="text-sm text-emerald-600 bg-emerald-50 px-3 py-2 rounded-lg mb-4">{rbacMsg}</div>
-    {/if}
-    {#if rbacErr}
-      <div class="text-sm text-rose-600 bg-rose-50 px-3 py-2 rounded-lg mb-4">{rbacErr}</div>
-    {/if}
- 
-    <!-- Role Defaults -->
-    <div class="mb-4">
-      <p class="text-xs font-medium text-surface-500 mb-2">Role Defaults</p>
-      <div class="space-y-2">
-        {#each Object.entries(rbacConfig.role_defaults || {}) as [role, cfg]}
-          {#if editingRole === role}
-            <div class="p-3 bg-surface-50 dark:bg-surface-700 rounded-lg space-y-2">
-              <p class="text-xs font-medium text-surface-700 dark:text-surface-200">{role}</p>
-              <p class="text-[10px] text-surface-500 mt-1">Pages (internal dashboard pages)</p>
-              <div class="flex flex-wrap gap-1.5">
-                <button onclick={() => toggleRolePage("*")} class="px-2 py-0.5 text-xs rounded-md transition-colors {editRolePages.includes('*') ? 'bg-primary-600 text-white' : 'bg-surface-200 dark:bg-surface-600 text-surface-500'}">* (all)</button>
-                {#each allPages as p}
-                  <button onclick={() => toggleRolePage(p)} class="px-2 py-0.5 text-xs rounded-md transition-colors {editRolePages.includes(p) ? 'bg-primary-600 text-white' : 'bg-surface-200 dark:bg-surface-600 text-surface-500'}">{p}</button>
-                {/each}
-              </div>
-              <p class="text-[10px] text-surface-500 mt-2">Sidebar Links (all services)</p>
-              <div class="flex flex-wrap gap-1.5">
-                <button onclick={() => toggleRoleSidebarLink("*")} class="px-2 py-0.5 text-xs rounded-md transition-colors {editRoleSidebarLinks.includes('*') ? 'bg-primary-600 text-white' : 'bg-surface-200 dark:bg-surface-600 text-surface-500'}">* (all)</button>
-                {#each allSidebarLinks as link}
-                  <button onclick={() => toggleRoleSidebarLink(link)} class="px-2 py-0.5 text-xs rounded-md transition-colors {editRoleSidebarLinks.includes(link) ? 'bg-primary-600 text-white' : 'bg-surface-200 dark:bg-surface-600 text-surface-500'}">{link}</button>
-                {/each}
-              </div>
-              <div class="flex gap-2">
-                <button onclick={saveRoleDefault} class="px-3 py-1 text-xs font-medium text-white bg-primary-600 hover:bg-primary-700 rounded-md transition-colors">Save</button>
-                <button onclick={cancelEdit} class="px-3 py-1 text-xs font-medium text-surface-600 hover:text-surface-800 dark:text-surface-400 dark:hover:text-surface-200">Cancel</button>
-              </div>
-            </div>
-          {:else}
-            <div class="flex items-center justify-between p-2 bg-surface-50 dark:bg-surface-700 rounded-lg">
-              <div class="flex flex-col gap-1 text-xs">
-                <div class="flex items-center gap-2">
-                  <span class="font-medium text-surface-700 dark:text-surface-300 w-16">{role}</span>
-                  <span class="text-surface-400">Pages: {cfg.pages === "*" ? "All" : (cfg.pages || []).join(", ")}</span>
-                </div>
-                <div class="flex items-center gap-2">
-                  <span class="w-16"></span>
-                  <span class="text-surface-400">Links: {cfg.sidebar_links === "*" ? "All" : (cfg.sidebar_links || ["*"]).join(", ")}</span>
-                </div>
-              </div>
-              <button onclick={() => startEditRole(role, cfg.pages, cfg.sidebar_links)} class="text-xs text-primary-500 hover:text-primary-700">Edit</button>
-            </div>
-          {/if}
-        {/each}
-      </div>
-    </div>
- 
-    <!-- User Overrides -->
-    <div class="mb-4">
-      <div class="flex items-center justify-between mb-2">
-        <p class="text-xs font-medium text-surface-500">User Overrides</p>
-        {#if editingUser !== "__new__"}
-          <button onclick={startAdd} class="px-2 py-1 text-xs font-medium text-primary-600 bg-primary-50 hover:bg-primary-100 rounded-md transition-colors">Add Override</button>
-        {/if}
-      </div>
- 
-      {#if Object.keys(rbacConfig.user_overrides || {}).length > 0}
-        <div class="space-y-2">
-          {#each Object.entries(rbacConfig.user_overrides) as [username, cfg]}
-            {#if editingUser === username}
-              <div class="p-3 bg-surface-50 dark:bg-surface-700 rounded-lg space-y-2">
-                <p class="text-xs font-medium text-surface-700 dark:text-surface-200">{username}</p>
-                <div class="flex flex-wrap gap-1.5">
-                  {#each allPages as p}
-                    <button onclick={() => togglePage(p)} class="px-2 py-0.5 text-xs rounded-md transition-colors {editPages.includes(p) ? 'bg-primary-600 text-white' : 'bg-surface-200 dark:bg-surface-600 text-surface-500'}">{p}</button>
-                  {/each}
-                </div>
-                <div class="flex gap-2">
-                  <button onclick={saveOverride} class="px-3 py-1 text-xs font-medium text-white bg-primary-600 hover:bg-primary-700 rounded-md">Save</button>
-                  <button onclick={cancelEdit} class="px-3 py-1 text-xs font-medium text-surface-500 bg-surface-200 hover:bg-surface-300 rounded-md">Cancel</button>
-                </div>
-              </div>
-            {:else}
-              <div class="flex items-center justify-between px-3 py-2 bg-surface-50 dark:bg-surface-700 rounded-lg">
-                <div>
-                  <span class="text-sm font-medium text-surface-700 dark:text-surface-200">{username}</span>
-                  <span class="text-xs text-surface-400 ml-2">{(cfg.pages || []).join(", ")}</span>
-                </div>
-                <div class="flex gap-1.5">
-                  <button onclick={() => startEdit(username, cfg.pages || [])} class="text-xs text-primary-500 hover:text-primary-700">Edit</button>
-                  <button onclick={() => deleteOverride(username)} class="text-xs text-rose-500 hover:text-rose-700">Delete</button>
-                </div>
-              </div>
-            {/if}
-          {/each}
-        </div>
-      {:else if editingUser !== "__new__"}
-        <p class="text-sm text-surface-400">No user overrides configured</p>
-      {/if}
- 
-      {#if editingUser === "__new__"}
-        <div class="p-3 bg-surface-50 dark:bg-surface-700 rounded-lg space-y-2 mt-2">
-          <input type="text" bind:value={newUsername} placeholder="Username" class="w-full px-3 py-1.5 text-sm border border-surface-200 dark:border-surface-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 dark:bg-surface-800 dark:text-white" />
-          <div class="flex flex-wrap gap-1.5">
-            {#each allPages as p}
-              <button onclick={() => togglePage(p)} class="px-2 py-0.5 text-xs rounded-md transition-colors {editPages.includes(p) ? 'bg-primary-600 text-white' : 'bg-surface-200 dark:bg-surface-600 text-surface-500'}">{p}</button>
-            {/each}
-          </div>
-          <div class="flex gap-2">
-            <button onclick={saveOverride} class="px-3 py-1 text-xs font-medium text-white bg-primary-600 hover:bg-primary-700 rounded-md">Save</button>
-            <button onclick={cancelEdit} class="px-3 py-1 text-xs font-medium text-surface-500 bg-surface-200 hover:bg-surface-300 rounded-md">Cancel</button>
-          </div>
-        </div>
-      {/if}
-    </div>
-  </div>
-  {/if}
- 
-  <div class="bg-white dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 p-6 shadow-sm">
-    <div class="flex items-center justify-between mb-4">
-      <h3 class="text-sm font-semibold text-surface-700 dark:text-surface-200">Audit Log</h3>
-      <button onclick={loadAuditLog} disabled={auditLoading} class="px-3 py-1.5 text-xs font-medium text-primary-600 bg-primary-50 hover:bg-primary-100 rounded-lg transition-colors disabled:opacity-50">
-        {auditLoading ? "Loading..." : auditEntries.length ? "Refresh" : "Load"}
-      </button>
-    </div>
-    {#if auditEntries.length > 0}
-      <div class="flex gap-1.5 mb-3">
-        {#each ["all", "high", "medium", "low", "info"] as lvl}
-          <button onclick={() => auditFilter = lvl} class="px-2.5 py-1 text-xs rounded-md transition-colors {auditFilter === lvl ? 'bg-surface-800 text-white' : 'bg-surface-100 text-surface-500 hover:bg-surface-200'}">
-            {lvl === "all" ? "All" : levelLabels[lvl] || lvl}
-            {#if lvl !== "all"}
-              <span class="ml-1 opacity-60">{auditEntries.filter(e => e.level === lvl).length}</span>
-            {/if}
-          </button>
-        {/each}
-      </div>
-      <div class="max-h-80 overflow-auto rounded-lg bg-surface-950 p-3 space-y-0.5">
-        {#each filteredEntries as e}
-          <div class="text-xs font-mono flex gap-2 leading-5">
-            <span class="{levelColors[e.level]} w-8 shrink-0">{levelLabels[e.level]}</span>
-            <span class="text-surface-500 shrink-0">{e.ts.replace('T', ' ')}</span>
-            <span class="text-surface-400 shrink-0 w-10">{e.status}</span>
-            <span class="text-surface-300 shrink-0">{e.user}</span>
-            <span class="text-surface-500">{e.method} {e.path}</span>
-          </div>
-        {/each}
-      </div>
-    {:else if !auditLoading}
-      <p class="text-sm text-surface-400">Click Load to view recent audit log entries</p>
-    {/if}
-  </div>
-</div>
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/dashboard/frontend/coverage/lcov-report/src/routes/Terminal.svelte.html b/dashboard/frontend/coverage/lcov-report/src/routes/Terminal.svelte.html deleted file mode 100644 index 526cd1b..0000000 --- a/dashboard/frontend/coverage/lcov-report/src/routes/Terminal.svelte.html +++ /dev/null @@ -1,2494 +0,0 @@ - - - - - - Code coverage report for src/routes/Terminal.svelte - - - - - - - - - -
-
-

All files / src/routes Terminal.svelte

-
- -
- 0% - Statements - 0/545 -
- - -
- 0% - Branches - 0/1 -
- - -
- 0% - Functions - 0/1 -
- - -
- 0% - Lines - 0/545 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -178 -179 -180 -181 -182 -183 -184 -185 -186 -187 -188 -189 -190 -191 -192 -193 -194 -195 -196 -197 -198 -199 -200 -201 -202 -203 -204 -205 -206 -207 -208 -209 -210 -211 -212 -213 -214 -215 -216 -217 -218 -219 -220 -221 -222 -223 -224 -225 -226 -227 -228 -229 -230 -231 -232 -233 -234 -235 -236 -237 -238 -239 -240 -241 -242 -243 -244 -245 -246 -247 -248 -249 -250 -251 -252 -253 -254 -255 -256 -257 -258 -259 -260 -261 -262 -263 -264 -265 -266 -267 -268 -269 -270 -271 -272 -273 -274 -275 -276 -277 -278 -279 -280 -281 -282 -283 -284 -285 -286 -287 -288 -289 -290 -291 -292 -293 -294 -295 -296 -297 -298 -299 -300 -301 -302 -303 -304 -305 -306 -307 -308 -309 -310 -311 -312 -313 -314 -315 -316 -317 -318 -319 -320 -321 -322 -323 -324 -325 -326 -327 -328 -329 -330 -331 -332 -333 -334 -335 -336 -337 -338 -339 -340 -341 -342 -343 -344 -345 -346 -347 -348 -349 -350 -351 -352 -353 -354 -355 -356 -357 -358 -359 -360 -361 -362 -363 -364 -365 -366 -367 -368 -369 -370 -371 -372 -373 -374 -375 -376 -377 -378 -379 -380 -381 -382 -383 -384 -385 -386 -387 -388 -389 -390 -391 -392 -393 -394 -395 -396 -397 -398 -399 -400 -401 -402 -403 -404 -405 -406 -407 -408 -409 -410 -411 -412 -413 -414 -415 -416 -417 -418 -419 -420 -421 -422 -423 -424 -425 -426 -427 -428 -429 -430 -431 -432 -433 -434 -435 -436 -437 -438 -439 -440 -441 -442 -443 -444 -445 -446 -447 -448 -449 -450 -451 -452 -453 -454 -455 -456 -457 -458 -459 -460 -461 -462 -463 -464 -465 -466 -467 -468 -469 -470 -471 -472 -473 -474 -475 -476 -477 -478 -479 -480 -481 -482 -483 -484 -485 -486 -487 -488 -489 -490 -491 -492 -493 -494 -495 -496 -497 -498 -499 -500 -501 -502 -503 -504 -505 -506 -507 -508 -509 -510 -511 -512 -513 -514 -515 -516 -517 -518 -519 -520 -521 -522 -523 -524 -525 -526 -527 -528 -529 -530 -531 -532 -533 -534 -535 -536 -537 -538 -539 -540 -541 -542 -543 -544 -545 -546 -547 -548 -549 -550 -551 -552 -553 -554 -555 -556 -557 -558 -559 -560 -561 -562 -563 -564 -565 -566 -567 -568 -569 -570 -571 -572 -573 -574 -575 -576 -577 -578 -579 -580 -581 -582 -583 -584 -585 -586 -587 -588 -589 -590 -591 -592 -593 -594 -595 -596 -597 -598 -599 -600 -601 -602 -603 -604 -605 -606 -607 -608 -609 -610 -611 -612 -613 -614 -615 -616 -617 -618 -619 -620 -621 -622 -623 -624 -625 -626 -627 -628 -629 -630 -631 -632 -633 -634 -635 -636 -637 -638 -639 -640 -641 -642 -643 -644 -645 -646 -647 -648 -649 -650 -651 -652 -653 -654 -655 -656 -657 -658 -659 -660 -661 -662 -663 -664 -665 -666 -667 -668 -669 -670 -671 -672 -673 -674 -675 -676 -677 -678 -679 -680 -681 -682 -683 -684 -685 -686 -687 -688 -689 -690 -691 -692 -693 -694 -695 -696 -697 -698 -699 -700 -701 -702 -703 -704 -705 -706 -707 -708 -709 -710 -711 -712 -713 -714 -715 -716 -717 -718 -719 -720 -721 -722 -723 -724 -725 -726 -727 -728 -729 -730 -731 -732 -733 -734 -735 -736 -737 -738 -739 -740 -741 -742 -743 -744 -745 -746 -747 -748 -749 -750 -751 -752 -753 -754 -755 -756 -757 -758 -759 -760 -761 -762 -763 -764 -765 -766 -767 -768 -769 -770 -771 -772 -773 -774 -775 -776 -777 -778 -779 -780 -781 -782 -783 -784 -785 -786 -787 -788 -789 -790 -791 -792 -793 -794 -795 -796 -797 -798 -799 -800 -801 -802 -803 -804  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  - 
<script>
-  import { onMount, onDestroy } from "svelte";
-  import { VoiceSession } from "../lib/voice.js";
-  import { tryRefreshSession } from "../lib/api.js";
- 
-  const PERSISTENCE_STATUS_PREFIX = "__DASHBOARD_PERSISTENCE__:";
-  const PERSISTENCE_STATUS_PREFIX_BYTES = new TextEncoder().encode(PERSISTENCE_STATUS_PREFIX);
- 
-  const hosts = [
-    { id: "nas", label: "NAS" },
-    { id: "vps", label: "VPS" },
-    { id: "caddy-vps", label: "Caddy VPS" },
-    { id: "mac", label: "Mac" },
-    { id: "claude-dev", label: "Claude Dev", persistent: true },
-  ];
- 
-  const DARK_THEME = {
-    background: "#0f172a", foreground: "#e2e8f0", cursor: "#6366f1",
-    cursorAccent: "#0f172a", selectionBackground: "#334155",
-    black: "#0f172a", red: "#f43f5e", green: "#10b981",
-    yellow: "#f59e0b", blue: "#3b82f6", magenta: "#a855f7",
-    cyan: "#06b6d4", white: "#e2e8f0",
-  };
- 
-  const LIGHT_THEME = {
-    background: "#f8fafc", foreground: "#0f172a", cursor: "#4f46e5",
-    cursorAccent: "#f8fafc", selectionBackground: "#cbd5e1",
-    black: "#0f172a", red: "#e11d48", green: "#059669",
-    yellow: "#d97706", blue: "#2563eb", magenta: "#9333ea",
-    cyan: "#0891b2", white: "#334155",
-  };
- 
-  let tabs = $state([]);
-  let activeTab = $state(null);
-  let showMenu = $state(false);
-  let voiceTick = $state(0);
-  let isDark = $state(false);
-  let isFullscreen = $state(false);
-  let showActionButtons = $state(false);
-  let tabCounter = 0;
-  const tabData = new Map();
- 
-  function currentTheme() {
-    return isDark ? DARK_THEME : LIGHT_THEME;
-  }
- 
-  function hostById(hostId) {
-    return hosts.find((h) => h.id === hostId);
-  }
- 
-  function openFreshSession(tab) {
-    const url = new URL(window.location.href);
-    url.searchParams.set("page", "terminal");
-    url.searchParams.set("host", tab.hostId);
-    window.open(`${url.pathname}${url.search}${url.hash}`, "_blank", "noopener");
-  }
- 
-  function persistenceLabel(status, reconnecting) {
-    if (status === "created") return "created new persistent session";
-    if (reconnecting) return "reattached to persistent session";
-    return "attached to persistent session";
-  }
- 
-  function applyThemeToTerminals() {
-    const theme = currentTheme();
-    tabData.forEach((d) => {
-      d.term?.options && (d.term.options.theme = theme);
-    });
-  }
- 
-  function syncTheme() {
-    isDark = document.documentElement.classList.contains("dark");
-    applyThemeToTerminals();
-  }
- 
-  const themeObserver = new MutationObserver((mutations) => {
-    if (mutations.some((m) => m.attributeName === "class")) syncTheme();
-  });
-  onMount(() => {
-    syncTheme();
-    themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ["class"] });
-    const requestedHost = new URLSearchParams(window.location.search).get("host");
-    if (requestedHost && !tabs.length && hostById(requestedHost)) addTab(requestedHost);
-  });
- 
-  function cleanName(name) {
-    return (name || "image")
-      .replace(/[^a-zA-Z0-9._-]/g, "-")
-      .replace(/-+/g, "-")
-      .replace(/^-|-$/g, "") || "image";
-  }
- 
-  function extensionFor(type) {
-    if (type === "image/png") return "png";
-    if (type === "image/jpeg") return "jpg";
-    if (type === "image/webp") return "webp";
-    if (type === "image/gif") return "gif";
-    return "bin";
-  }
- 
-  async function fileToBase64(file) {
-    const buf = await file.arrayBuffer();
-    let binary = "";
-    const bytes = new Uint8Array(buf);
-    const chunkSize = 0x8000;
-    for (let i = 0; i < bytes.length; i += chunkSize) {
-      binary += String.fromCharCode(...bytes.subarray(i, i + chunkSize));
-    }
-    return btoa(binary);
-  }
- 
-  async function sendDroppedImage(term, ws, file) {
-    if (!file?.type?.startsWith("image/")) return;
-    if (ws.readyState !== 1) return;
- 
-    const ext = extensionFor(file.type);
-    const baseName = cleanName(file.name).replace(/\.[a-zA-Z0-9]+$/, "");
-    const stamp = Date.now();
-    const imagePath = `/tmp/claude-images/${baseName || "image"}-${stamp}.${ext}`;
-    const b64 = await fileToBase64(file);
- 
-    const cmd = [
-      "python3 - <<'PY'",
-      "import base64, pathlib",
-      `p = pathlib.Path(${JSON.stringify(imagePath)})`,
-      "p.parent.mkdir(parents=True, exist_ok=True)",
-      `p.write_bytes(base64.b64decode(${JSON.stringify(b64)}))`,
-      `print(f'saved image -> {p}')`,
-      "PY",
-      "",
-    ].join("\n");
- 
-    ws.send(new TextEncoder().encode(cmd));
-    term.write(`\r\n\x1b[90m[Uploaded image to ${imagePath}]\x1b[0m\r\n`);
-  }
- 
-  function addTab(hostId) {
-    const id = ++tabCounter;
-    const host = hostById(hostId);
-    tabs = [...tabs, {
-      id,
-      hostId,
-      label: host.label,
-      connected: false,
-      error: "",
-      statusBanner: host.persistent ? "Connecting to persistent session..." : "",
-      persistent: !!host.persistent,
-    }];
-    activeTab = id;
-    showMenu = false;
-  }
- 
-  function closeTab(id) {
-    const d = tabData.get(id);
-    if (d) {
-      d.closedManually = true;
-      if (d.reconnectTimer) clearTimeout(d.reconnectTimer);
-      d.voice?.destroy();
-      d.ro?.disconnect();
-      if (d.heartbeat) clearInterval(d.heartbeat);
-      d.term?.element?.removeEventListener("dragover", d.handleDragOver);
-      d.term?.element?.removeEventListener("drop", d.handleDrop);
-      d.term?.textarea?.removeEventListener("paste", d.handlePaste);
-      d.ws?.close();
-      d.term?.dispose();
-      tabData.delete(id);
-    }
-    tabs = tabs.filter((t) => t.id !== id);
-    if (activeTab === id) activeTab = tabs.length ? tabs[tabs.length - 1].id : null;
-  }
- 
-  function updateTab(tabId, props) {
-    tabs = tabs.map((t) => (t.id === tabId ? { ...t, ...props } : t));
-  }
- 
-  function initTerminal(el, tab) {
-    const tabId = tab.id;
-    (async () => {
-      const { Terminal } = await import("@xterm/xterm");
-      const { FitAddon } = await import("@xterm/addon-fit");
-      await import("@xterm/xterm/css/xterm.css");
-      const term = new Terminal({
-        cursorBlink: true, fontSize: 13,
-        fontFamily: "'JetBrains Mono', 'Fira Code', 'SF Mono', 'Menlo', monospace",
-        lineHeight: 1.4,
-        theme: currentTheme(),
-      });
-      const fit = new FitAddon();
-      term.loadAddon(fit);
-      term.open(el);
-      fit.fit();
- 
-      const proto = location.protocol === "https:" ? "wss:" : "ws:";
-      let heartbeat = null;
-      let pongTimeout = null;
-      let reconnectTimer = null;
-      let closedManually = false;
-      let reconnecting = false;
-      let authRecoveryInFlight = false;
-      let reconnectAttempts = 0;
-      let consecutiveAuthFailures = 0;
-      const MAX_RECONNECT_DELAY = 30000;
-      const MAX_AUTH_FAILURES = 3;
-      const PING_INTERVAL = 30000; // 30s between pings
-      const PONG_TIMEOUT = 10000; // 10s to receive pong
- 
-      function clearHeartbeat() {
-        if (heartbeat) {
-          clearInterval(heartbeat);
-          heartbeat = null;
-        }
-        if (pongTimeout) {
-          clearTimeout(pongTimeout);
-          pongTimeout = null;
-        }
-      }
- 
-      function handleAuthExpired(reason = "Session expired — please log in again") {
-        clearHeartbeat();
-        if (reconnectTimer) {
-          clearTimeout(reconnectTimer);
-          reconnectTimer = null;
-        }
-        reconnecting = false;
-        updateTab(tabId, {
-          connected: false,
-          error: reason,
-          statusBanner: "",
-        });
-        term.write(`\r\n\x1b[90m[${reason}]\x1b[0m`);
-        const d = tabData.get(tabId);
-        if (d) {
-          d.ws = null;
-          d.heartbeat = heartbeat;
-          d.reconnectTimer = reconnectTimer;
-          d.closedManually = closedManually;
-          d.reconnecting = reconnecting;
-        }
-      }
- 
-      function scheduleReconnect(reason) {
-        clearHeartbeat();
-        if (closedManually || reconnectTimer) return;
-        reconnecting = true;
-        reconnectAttempts++;
-        const delay = Math.min(1000 * Math.pow(2, reconnectAttempts - 1), MAX_RECONNECT_DELAY);
-        updateTab(tabId, {
-          connected: false,
-          error: `${reason} — reconnecting in ${Math.round(delay / 1000)}s (attempt ${reconnectAttempts})...`,
-          statusBanner: tab.persistent ? "Reconnecting to persistent session..." : "",
-        });
-        term.write(`\r\n\x1b[90m[${reason} — reconnecting in ${Math.round(delay / 1000)}s...]\x1b[0m`);
-        reconnectTimer = setTimeout(() => {
-          reconnectTimer = null;
-          if (!tabData.has(tabId) || closedManually) return;
-          connect(true);
-        }, delay);
-      }
- 
-      function sendSize(ws) {
-        if (ws.readyState === 1) {
-          const buf = new Uint8Array(5);
-          buf[0] = 0x01;
-          new DataView(buf.buffer).setUint16(1, term.cols);
-          new DataView(buf.buffer).setUint16(3, term.rows);
-          ws.send(buf);
-        }
-      }
- 
-      async function connect(forceRefresh = false) {
-        let d = tabData.get(tabId);
- 
-        if (forceRefresh) {
-          const refreshed = await tryRefreshSession();
-          if (!refreshed) {
-            handleAuthExpired();
-            return null;
-          }
-        }
- 
-        const ws = new WebSocket(
-          `${proto}//${location.host}/ws/terminal?host=${encodeURIComponent(tab.hostId)}`
-        );
-        ws.binaryType = "arraybuffer";
- 
-        ws.onopen = () => {
-          reconnectAttempts = 0;
-          consecutiveAuthFailures = 0; // Reset auth failure counter on successful connection
-          updateTab(tabId, {
-            connected: true,
-            error: "",
-            statusBanner: tab.persistent ? "Connected. Waiting for persistent session status..." : "",
-          });
-          sendSize(ws);
-          clearHeartbeat();
-          heartbeat = setInterval(() => {
-            if (ws.readyState === 1) {
-              ws.send("__ping__");
-              // Set timeout to detect missing pong
-              pongTimeout = setTimeout(() => {
-                // Check if still the same WebSocket instance and still open
-                const currentTab = tabData.get(tabId);
-                if (currentTab?.ws === ws && ws.readyState === 1) {
-                  ws.close(1000, "keepalive ping timeout");
-                }
-              }, PONG_TIMEOUT);
-            }
-          }, PING_INTERVAL);
-          const d = tabData.get(tabId);
-          if (d) {
-            d.ws = ws;
-            d.heartbeat = heartbeat;
-            d.reconnectTimer = reconnectTimer;
-            d.closedManually = closedManually;
-            d.reconnecting = reconnecting;
-            d.voice?.attach(term, ws);
-          }
-        };
-        ws.onmessage = (e) => {
-          // Check for text pong response
-          if (typeof e.data === 'string' && e.data === '__pong__') {
-            // Clear the pong timeout - connection is alive
-            if (pongTimeout) {
-              clearTimeout(pongTimeout);
-              pongTimeout = null;
-            }
-            return;
-          }
- 
-          const data = new Uint8Array(e.data);
-          const isPersistenceMessage = tab.persistent
-            && data.length >= PERSISTENCE_STATUS_PREFIX_BYTES.length
-            && PERSISTENCE_STATUS_PREFIX_BYTES.every((byte, index) => data[index] === byte);
-          if (isPersistenceMessage) {
-            const status = new TextDecoder().decode(data.slice(PERSISTENCE_STATUS_PREFIX_BYTES.length)).trim();
-            updateTab(tabId, { statusBanner: persistenceLabel(status, reconnecting) });
-            reconnecting = false;
-            const d = tabData.get(tabId);
-            if (d) d.reconnecting = reconnecting;
-            return;
-          }
-          term.write(data);
-        };
-        ws.onclose = async (e) => {
-          const reason = e.reason || `Connection closed (code ${e.code})`;
-          const tabState = tabData.get(tabId);
-          if (tabState) tabState.ws = null;
- 
-          // Handle auth failures (code 1008)
-          if (e.code === 1008) {
-            consecutiveAuthFailures++;
- 
-            // Stop after MAX_AUTH_FAILURES consecutive auth failures
-            if (consecutiveAuthFailures >= MAX_AUTH_FAILURES) {
-              handleAuthExpired(`Session expired after ${MAX_AUTH_FAILURES} attempts — please refresh page`);
-              return;
-            }
- 
-            // Try to refresh session
-            if (!closedManually && !authRecoveryInFlight) {
-              authRecoveryInFlight = true;
-              try {
-                const refreshed = await tryRefreshSession();
-                if (refreshed && !closedManually) {
-                  reconnecting = true;
-                  updateTab(tabId, {
-                    connected: false,
-                    error: `Refreshing session (attempt ${consecutiveAuthFailures}/${MAX_AUTH_FAILURES})...`,
-                    statusBanner: tab.persistent ? "Reconnecting to persistent session..." : "",
-                  });
-                  term.write(`\r\n\x1b[90m[Refreshing session...]\x1b[0m`);
-                  void connect(false);
-                  return;
-                }
-              } catch (err) {
-                console.error("Session refresh failed:", err);
-              } finally {
-                authRecoveryInFlight = false;
-              }
-            }
-            handleAuthExpired("Session expired — please log in again");
-            return;
-          }
- 
-          // Non-auth failures: continue with normal reconnection
-          if (!closedManually) scheduleReconnect(reason);
-          else {
-            clearHeartbeat();
-            updateTab(tabId, { connected: false, error: reason });
-            term.write(`\r\n\x1b[90m[${reason}]\x1b[0m`);
-          }
-        };
-        ws.onerror = () => {
-          const tabState = tabData.get(tabId);
-          if (tabState) tabState.ws = null;
-          if (!closedManually) scheduleReconnect("Connection failed");
-          else updateTab(tabId, { connected: false, error: "Connection failed" });
-        };
- 
-        d = tabData.get(tabId);
-        if (d) {
-          d.ws = ws;
-          d.heartbeat = heartbeat;
-          d.reconnectTimer = reconnectTimer;
-          d.closedManually = closedManually;
-          d.reconnecting = reconnecting;
-        }
-        return ws;
-      }
- 
-      let ws = null;
-      void connect().then((connectedWs) => {
-        ws = connectedWs;
-        if (connectedWs) voice.attach(term, connectedWs);
-      });
-      term.onData((data) => {
-        const currentWs = tabData.get(tabId)?.ws;
-        if (currentWs?.readyState === 1) currentWs.send(new TextEncoder().encode(data));
-      });
- 
-      const handleDragOver = (e) => {
-        e.preventDefault();
-      };
- 
-      const handleDrop = async (e) => {
-        e.preventDefault();
-        if (activeTab !== tabId) return;
-        const currentWs = tabData.get(tabId)?.ws;
-        const files = [...(e.dataTransfer?.files || [])].filter((f) => f.type?.startsWith("image/"));
-        for (const file of files) {
-          await sendDroppedImage(term, currentWs, file);
-        }
-      };
- 
-      const handlePaste = async (e) => {
-        if (activeTab !== tabId) return;
-        const currentWs = tabData.get(tabId)?.ws;
-        const items = [...(e.clipboardData?.items || [])]
-          .filter((it) => it.kind === "file" && it.type?.startsWith("image/"));
-        for (const item of items) {
-          const file = item.getAsFile();
-          if (file) await sendDroppedImage(term, currentWs, file);
-        }
-      };
- 
-      term.element?.addEventListener("dragover", handleDragOver);
-      term.element?.addEventListener("drop", handleDrop);
-      term.textarea?.addEventListener("paste", handlePaste);
- 
-      const ro = new ResizeObserver(() => {
-        fit.fit();
-        const currentWs = tabData.get(tabId)?.ws;
-        if (currentWs) sendSize(currentWs);
-      });
-      ro.observe(el);
-      const voice = new VoiceSession(() => { voiceTick++; });
-      voice.attach(term, ws);
-      tabData.set(tabId, { ws, term, ro, el, voice, heartbeat, reconnectTimer, closedManually, reconnecting, handleDragOver, handleDrop, handlePaste });
-    })();
-  }
- 
-  function activeVoice() { return voiceTick >= 0 && tabData.get(activeTab)?.voice; }
- 
-  function toggleFullscreen() {
-    const elem = document.documentElement;
-    if (!document.fullscreenElement) {
-      elem.requestFullscreen().then(() => {
-        isFullscreen = true;
-        showActionButtons = true;
-      }).catch(err => {
-        console.error('Failed to enter fullscreen:', err);
-      });
-    } else {
-      document.exitFullscreen().then(() => {
-        isFullscreen = false;
-        showActionButtons = false;
-      }).catch(err => {
-        console.error('Failed to exit fullscreen:', err);
-      });
-    }
-  }
- 
-  function sendKey(key) {
-    const currentWs = tabData.get(activeTab)?.ws;
-    if (currentWs?.readyState === 1) {
-      currentWs.send(new TextEncoder().encode(key));
-    }
-  }
- 
-  onMount(() => {
-    syncTheme();
-    themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ["class"] });
-    const requestedHost = new URLSearchParams(window.location.search).get("host");
-    if (requestedHost && !tabs.length && hostById(requestedHost)) addTab(requestedHost);
- 
-    // Listen for fullscreen changes
-    document.addEventListener('fullscreenchange', () => {
-      isFullscreen = !!document.fullscreenElement;
-      showActionButtons = !!document.fullscreenElement;
-    });
-  });
- 
-  onDestroy(() => {
-    themeObserver.disconnect();
-    tabData.forEach((d) => {
-      d.closedManually = true;
-      if (d.reconnectTimer) clearTimeout(d.reconnectTimer);
-      d.voice?.destroy();
-      d.ro?.disconnect();
-      if (d.heartbeat) clearInterval(d.heartbeat);
-      d.term?.element?.removeEventListener("dragover", d.handleDragOver);
-      d.term?.element?.removeEventListener("drop", d.handleDrop);
-      d.term?.textarea?.removeEventListener("paste", d.handlePaste);
-      d.ws?.close();
-      d.term?.dispose();
-    });
-  });
-</script>
-<div class={activeVoice()?.voiceMode ? 'flex flex-col h-[calc(100vh-4rem)] overflow-hidden gap-1' : 'space-y-2'}>
-  {#if !activeVoice()?.voiceMode}
-    <h1 class="text-2xl font-bold text-surface-900 dark:text-surface-100 tracking-tight">Terminal</h1>
-  {/if}
- 
-  <!-- iPhone Landscape Action Buttons (only in fullscreen) -->
-  {#if showActionButtons && activeTab}
-    <!-- Top Left Pocket -->
-    <div class="actions-top-left">
-      <button class="action-btn" onclick={() => sendKey('\x1b')} title="ESC">ESC</button>
-      <button class="action-btn" onclick={() => sendKey('\t')} title="TAB">TAB</button>
-    </div>
- 
-    <!-- Bottom Left Pocket -->
-    <div class="actions-bottom-left">
-      <button class="action-btn" onclick={() => sendKey('\x01')} title="CTRL+A">^A</button>
-      <button class="action-btn" onclick={() => sendKey('\x03')} title="CTRL+C">^C</button>
-    </div>
- 
-    <!-- Top Right Pocket -->
-    <div class="actions-top-right">
-      <button class="action-btn" onclick={() => sendKey('\x0c')} title="CTRL+L">^L</button>
-      <button class="action-btn" onclick={() => sendKey('\x1a')} title="CTRL+Z">^Z</button>
-    </div>
- 
-    <!-- Bottom Right Pocket -->
-    <div class="actions-bottom-right">
-      <button class="action-btn" onclick={() => sendKey('\x04')} title="CTRL+D">^D</button>
-      <button class="action-btn" onclick={() => sendKey('\x12')} title="CTRL+R">^R</button>
-    </div>
-  {/if}
- 
-  <div class="flex items-center gap-1 border-b border-surface-300 dark:border-surface-700 pb-1 relative">
-    {#each tabs as tab (tab.id)}
-      <button
-        class="flex items-center gap-1.5 px-3 py-1.5 text-sm rounded-t-lg transition-colors {activeTab === tab.id ? 'bg-surface-200 dark:bg-surface-800 text-surface-900 dark:text-surface-100' : 'text-surface-600 dark:text-surface-400 hover:text-surface-800 dark:hover:text-surface-200 hover:bg-surface-100 dark:hover:bg-surface-800/50'}"
-        onclick={() => activeTab = tab.id}
-      >
-        {#if tab.connected}
-          <span class="w-1.5 h-1.5 rounded-full bg-emerald-500"></span>
-        {:else if tab.error}
-          <span class="w-1.5 h-1.5 rounded-full bg-rose-500"></span>
-        {:else}
-          <span class="w-1.5 h-1.5 rounded-full bg-amber-400 animate-pulse"></span>
-        {/if}
-        {tab.label}
-        {#if tab.persistent}
-          <span class="rounded bg-indigo-100 px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-wide text-indigo-700 dark:bg-indigo-500/20 dark:text-indigo-300">tmux</span>
-        {/if}
-        <span
-          class="ml-1 text-surface-500 hover:text-sky-500 cursor-pointer"
-          role="button"
-          tabindex="0"
-          title="Open fresh session in new browser tab"
-          aria-label={`Open fresh ${tab.label} session in new browser tab`}
-          onclick={(e) => { e.stopPropagation(); openFreshSession(tab); }}
-          onkeydown={(e) => e.key === 'Enter' && openFreshSession(tab)}
-        >↗</span>
-        <span
-          class="ml-1 text-surface-500 hover:text-rose-400 cursor-pointer"
-          role="button"
-          tabindex="0"
-          onclick={(e) => { e.stopPropagation(); closeTab(tab.id); }}
-          onkeydown={(e) => e.key === 'Enter' && closeTab(tab.id)}
-        >&times;</span>
-      </button>
-    {/each}
-    <div class="relative">
-      <button
-        class="px-2 py-1 text-surface-600 dark:text-surface-400 hover:text-surface-900 dark:hover:text-surface-100 hover:bg-surface-200 dark:hover:bg-surface-800 rounded text-lg leading-none"
-        onclick={() => showMenu = !showMenu}
-      >+</button>
-      {#if showMenu}
-        <div class="absolute top-full left-0 mt-1 bg-surface-100 dark:bg-surface-800 border border-surface-300 dark:border-surface-600 rounded-lg shadow-xl z-10 py-1 min-w-[140px]">
-          {#each hosts as h}
-            <button
-              class="flex w-full items-center justify-between gap-2 px-3 py-1.5 text-left text-sm text-surface-800 dark:text-surface-200 hover:bg-surface-200 dark:hover:bg-surface-700"
-              onclick={() => addTab(h.id)}
-            >
-              <span>{h.label}</span>
-              {#if h.persistent}
-                <span class="rounded bg-indigo-100 px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-wide text-indigo-700 dark:bg-indigo-500/20 dark:text-indigo-300">persistent</span>
-              {/if}
-            </button>
-          {/each}
-        </div>
-      {/if}
-    </div>
-    {#if activeTab && tabs.find(t => t.id === activeTab)?.connected}
-      <div class="ml-auto flex items-center gap-1">
-        <button
-          class="px-2 py-1 rounded text-sm transition-colors text-surface-600 dark:text-surface-500 hover:text-surface-800 dark:hover:text-surface-300"
-          title={isFullscreen ? 'Exit fullscreen' : 'Enter fullscreen'}
-          onclick={toggleFullscreen}
-        >
-          {#if isFullscreen}
-            <svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M8 3v3a2 2 0 01-2 2H3m18 0h-3a2 2 0 01-2-2V3m0 18v-3a2 2 0 012-2h3M3 16h3a2 2 0 012 2v3"/></svg>
-          {:else}
-            <svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M8 3H5a2 2 0 00-2 2v3m18 0V5a2 2 0 00-2-2h-3m0 18h3a2 2 0 002-2v-3M3 16v3a2 2 0 002 2h3"/></svg>
-          {/if}
-        </button>
-        {#if activeVoice()}
-          {@const v = activeVoice()}
-          <button
-            class="px-2 py-1 rounded text-sm transition-colors {v.voiceMode ? 'text-rose-400' : v.sttAvailable ? 'text-surface-600 dark:text-surface-500 hover:text-surface-800 dark:hover:text-surface-300' : 'text-surface-300 dark:text-surface-700 cursor-not-allowed'}"
-            title={!v.sttAvailable ? 'STT not supported' : v.voiceMode ? 'Voice mode on' : 'Enter voice mode'}
-            disabled={!v.sttAvailable}
-            onclick={() => { if (!v.voiceMode) v.enterVoiceMode(); else v.exitVoiceMode(); voiceTick++; }}
-          >
-            <svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 1a3 3 0 00-3 3v8a3 3 0 006 0V4a3 3 0 00-3-3z"/><path d="M19 10v2a7 7 0 01-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg>
-          </button>
-        {/if}
-      </div>
-    {/if}
-  </div>
- 
-  <p class="text-xs text-surface-600 dark:text-surface-500">
-    Tip: drag & drop or paste images into the active terminal tab to upload them to <code class="text-surface-700 dark:text-surface-400">/tmp/claude-images</code>.
-  </p>
- 
-  {#if activeTab && tabs.find(t => t.id === activeTab)?.statusBanner}
-    <div class="rounded-lg border border-indigo-200 bg-indigo-50 px-3 py-2 text-sm text-indigo-800 dark:border-indigo-500/30 dark:bg-indigo-500/10 dark:text-indigo-200">
-      {tabs.find(t => t.id === activeTab)?.statusBanner}
-    </div>
-  {/if}
- 
-  {#if !tabs.length}
-    <div class="flex items-center justify-center h-[calc(100vh-12rem)] text-surface-600 dark:text-surface-500 text-sm">
-      Click + to open a terminal
-    </div>
-  {/if}
- 
-  {#each tabs as tab (tab.id)}
-    <div class="{activeTab === tab.id ? '' : 'hidden'} {activeVoice()?.voiceMode ? 'flex-1 min-h-0' : ''}">
-      <div
-        use:initTerminal={tab}
-        class="h-full rounded-xl overflow-hidden shadow-lg border border-surface-300 dark:border-surface-700 bg-surface-50 dark:bg-[#0f172a] transition-all duration-300"
-        style="{activeVoice()?.voiceMode ? '' : 'height: calc(100vh - 10rem);'}"
-      ></div>
-    </div>
-  {/each}
- 
-  {#if activeVoice()?.voiceMode}
-    {@const v = activeVoice()}
-    <div class="mt-1 shrink-0 rounded-xl bg-surface-100/95 dark:bg-surface-800/95 border border-surface-300 dark:border-surface-700 shadow-xl overflow-hidden animate-slide-up">
-      <!-- Header -->
-      <div class="flex items-center justify-between px-3 py-1.5 border-b border-surface-300/50 dark:border-surface-700/50">
-        <span class="text-xs font-medium text-surface-700 dark:text-surface-300">Voice Mode</span>
-        <div class="flex items-center gap-2">
-          <select
-            class="bg-surface-200 dark:bg-surface-700 text-surface-800 dark:text-surface-300 text-xs rounded px-1 py-0.5 outline-none max-w-[120px]"
-            onchange={(e) => { v.setVoice(+e.target.value); voiceTick++; }}
-          >
-            {#each v.voices as voice, i}
-              <option value={i} selected={i === v.voiceIdx}>{voice.name}</option>
-            {/each}
-          </select>
-          <button
-            class="px-1.5 py-0.5 rounded text-xs font-medium text-surface-600 dark:text-surface-400 hover:text-surface-800 dark:hover:text-surface-200 hover:bg-surface-200 dark:hover:bg-surface-700"
-            onclick={() => { v.cycleLang(); voiceTick++; }}
-          >{v.langLabel}</button>
-          <button
-            class="px-1.5 py-0.5 rounded text-xs font-medium text-surface-600 dark:text-surface-400 hover:text-surface-800 dark:hover:text-surface-200 hover:bg-surface-200 dark:hover:bg-surface-700"
-            onclick={() => { v.cycleTalkMode(); voiceTick++; }}
-          >{v.modeLabel}</button>
-          <button
-            class="px-1.5 py-0.5 rounded text-xs text-surface-500 hover:text-rose-400"
-            onclick={() => { v.exitVoiceMode(); voiceTick++; }}
-          >&times;</button>
-        </div>
-      </div>
-      <!-- Transcript + Mic -->
-      <div class="flex items-center gap-3 px-3 py-2">
-        {#if v.talkMode === "hold"}
-          <button
-            aria-label="Hold to talk"
-            class="w-10 h-10 shrink-0 rounded-full flex items-center justify-center transition-colors {v.listening ? 'bg-rose-500/30 text-rose-400' : 'bg-surface-200 dark:bg-surface-700 text-surface-700 dark:text-surface-300 hover:bg-surface-300 dark:hover:bg-surface-600'}"
-            onpointerdown={() => { v.startHold(); voiceTick++; }}
-            onpointerup={() => { v.releaseHold(); voiceTick++; }}
-            onpointerleave={() => { if (v.listening) { v.releaseHold(); voiceTick++; } }}
-          >
-            <svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 1a3 3 0 00-3 3v8a3 3 0 006 0V4a3 3 0 00-3-3z"/><path d="M19 10v2a7 7 0 01-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg>
-          </button>
-        {:else if v.talkMode === "tap"}
-          <button
-            aria-label="Tap to talk"
-            class="w-10 h-10 shrink-0 rounded-full flex items-center justify-center transition-colors {v.listening ? 'bg-rose-500/30 text-rose-400' : 'bg-surface-200 dark:bg-surface-700 text-surface-700 dark:text-surface-300 hover:bg-surface-300 dark:hover:bg-surface-600'}"
-            onclick={() => { v.tapMic(); voiceTick++; }}
-          >
-            <svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 1a3 3 0 00-3 3v8a3 3 0 006 0V4a3 3 0 00-3-3z"/><path d="M19 10v2a7 7 0 01-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg>
-          </button>
-        {:else}
-          <div class="w-10 h-10 shrink-0 rounded-full flex items-center justify-center {v.listening ? 'bg-emerald-500/20 text-emerald-400 animate-pulse' : 'bg-surface-200 dark:bg-surface-700 text-surface-500'}">
-            <svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 1a3 3 0 00-3 3v8a3 3 0 006 0V4a3 3 0 00-3-3z"/><path d="M19 10v2a7 7 0 01-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg>
-          </div>
-        {/if}
-        <div class="flex-1 min-w-0">
-          {#if v.transcript}
-            <p class="text-sm text-surface-600 dark:text-surface-400 italic truncate">"{v.transcript}"</p>
-          {:else}
-            <p class="text-xs text-surface-500 dark:text-surface-600">{v.speaking ? 'Speaking...' : v.listening ? 'Listening...' : 'Ready'}</p>
-          {/if}
-        </div>
-      </div>
-    </div>
-  {/if}
-</div>
- 
-<style>
-  @keyframes slide-up {
-    from { transform: translateY(100%); opacity: 0; }
-    to { transform: translateY(0); opacity: 1; }
-  }
-  .animate-slide-up { animation: slide-up 0.25s ease-out; }
- 
-  /* iPhone 17 Pro Landscape Action Button Zones */
-  .actions-top-left,
-  .actions-bottom-left,
-  .actions-top-right,
-  .actions-bottom-right {
-    position: fixed;
-    display: flex;
-    flex-direction: column;
-    gap: 5px;
-    z-index: 9999;
-  }
- 
-  .actions-top-left {
-    top: 5px;
-    left: 5px;
-    width: 52px;
-    height: 125px;
-  }
- 
-  .actions-bottom-left {
-    bottom: 26px;
-    left: 5px;
-    width: 52px;
-    height: 110px;
-  }
- 
-  .actions-top-right {
-    top: 5px;
-    right: 5px;
-    width: 52px;
-    height: 125px;
-  }
- 
-  .actions-bottom-right {
-    bottom: 26px;
-    right: 5px;
-    width: 52px;
-    height: 110px;
-  }
- 
-  .action-btn {
-    flex: 1;
-    background: rgba(99, 102, 241, 0.9);
-    color: white;
-    border: none;
-    border-radius: 8px;
-    font-size: 11px;
-    font-weight: 600;
-    cursor: pointer;
-    transition: all 0.2s;
-    backdrop-filter: blur(10px);
-    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
-    display: flex;
-    align-items: center;
-    justify-content: center;
-  }
- 
-  .action-btn:active {
-    background: rgba(79, 70, 229, 1);
-    transform: scale(0.95);
-  }
- 
-  /* Terminal container respects safe areas */
-  .terminal-safe-container {
-    padding-left: env(safe-area-inset-left);
-    padding-right: env(safe-area-inset-right);
-    padding-bottom: env(safe-area-inset-bottom);
-    box-sizing: border-box;
-  }
-</style>
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/dashboard/frontend/coverage/lcov-report/src/routes/Transmission.svelte.html b/dashboard/frontend/coverage/lcov-report/src/routes/Transmission.svelte.html deleted file mode 100644 index 10efbbf..0000000 --- a/dashboard/frontend/coverage/lcov-report/src/routes/Transmission.svelte.html +++ /dev/null @@ -1,931 +0,0 @@ - - - - - - Code coverage report for src/routes/Transmission.svelte - - - - - - - - - -
-
-

All files / src/routes Transmission.svelte

-
- -
- 0% - Statements - 0/145 -
- - -
- 0% - Branches - 0/1 -
- - -
- 0% - Functions - 0/1 -
- - -
- 0% - Lines - 0/145 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -178 -179 -180 -181 -182 -183 -184 -185 -186 -187 -188 -189 -190 -191 -192 -193 -194 -195 -196 -197 -198 -199 -200 -201 -202 -203 -204 -205 -206 -207 -208 -209 -210 -211 -212 -213 -214 -215 -216 -217 -218 -219 -220 -221 -222 -223 -224 -225 -226 -227 -228 -229 -230 -231 -232 -233 -234 -235 -236 -237 -238 -239 -240 -241 -242 -243 -244 -245 -246 -247 -248 -249 -250 -251 -252 -253 -254 -255 -256 -257 -258 -259 -260 -261 -262 -263 -264 -265 -266 -267 -268 -269 -270 -271 -272 -273 -274 -275 -276 -277 -278 -279 -280 -281 -282 -283  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  - 
<script>
-  import { onMount, onDestroy } from "svelte";
-  import { get, post } from "../lib/api.js";
- 
-  let status = $state(null);
-  let torrents = $state([]);
-  let trackers = $state([]);
-  let stats = $state(null);
-  let loading = $state(true);
-  let selectedTab = $state("torrents");
- 
-  async function load() {
-    try {
-      const [statusData, torrentsData, trackersData, statsData] = await Promise.all([
-        get("/transmission/status").catch(() => null),
-        get("/transmission/torrents").catch(() => ({ torrents: [] })),
-        get("/transmission/trackers").catch(() => ({ trackers: [] })),
-        get("/transmission/stats").catch(() => null),
-      ]);
- 
-      status = statusData;
-      torrents = torrentsData?.torrents || [];
-      trackers = trackersData?.trackers || [];
-      stats = statsData;
-      loading = false;
-    } catch (e) {
-      console.error("Failed to load transmission data:", e);
-      loading = false;
-    }
-  }
- 
-  async function reannounce(torrentId) {
-    try {
-      await post(`/transmission/reannounce/${torrentId}`);
-      await load();
-    } catch (e) {
-      alert("Failed to reannounce: " + e.message);
-    }
-  }
- 
-  async function startTorrent(torrentId) {
-    try {
-      await post(`/transmission/start/${torrentId}`);
-      await load();
-    } catch (e) {
-      alert("Failed to start: " + e.message);
-    }
-  }
- 
-  async function stopTorrent(torrentId) {
-    try {
-      await post(`/transmission/stop/${torrentId}`);
-      await load();
-    } catch (e) {
-      alert("Failed to stop: " + e.message);
-    }
-  }
- 
-  function fmtBytes(b) {
-    if (!b) return "0 B";
-    const u = ["B", "KB", "MB", "GB", "TB"];
-    const i = Math.floor(Math.log(b) / Math.log(1024));
-    return (b / Math.pow(1024, i)).toFixed(1) + " " + u[i];
-  }
- 
-  function fmtSpeed(bytesPerSec) {
-    return fmtBytes(bytesPerSec) + "/s";
-  }
- 
-  function statusText(status) {
-    const statusMap = {
-      0: "Stopped",
-      1: "Check waiting",
-      2: "Checking",
-      3: "Download waiting",
-      4: "Downloading",
-      5: "Seed waiting",
-      6: "Seeding"
-    };
-    return statusMap[status] || "Unknown";
-  }
- 
-  let interval;
-  onMount(() => { load(); interval = setInterval(load, 10000); });
-  onDestroy(() => clearInterval(interval));
-</script>
- 
-<div class="space-y-6">
-  <div>
-    <h1 class="text-2xl font-bold text-surface-900 dark:text-white tracking-tight">Transmission</h1>
-    <p class="text-sm text-surface-400 mt-1">BitTorrent client status and management</p>
-  </div>
- 
-  {#if loading}
-    <div class="h-[400px] rounded-xl bg-surface-100 dark:bg-surface-800 animate-pulse"></div>
-  {:else}
-    <!-- Status Cards -->
-    <div class="grid grid-cols-1 md:grid-cols-4 gap-4">
-      <!-- Daemon Status -->
-      <div class="rounded-xl bg-white dark:bg-surface-800 p-6 border border-surface-200 dark:border-surface-700">
-        <div class="flex items-center justify-between">
-          <div>
-            <p class="text-sm text-surface-500 dark:text-surface-400">Daemon</p>
-            <p class="text-2xl font-bold mt-1 {status?.running ? 'text-emerald-500' : 'text-rose-500'}">
-              {status?.running ? 'Running' : 'Stopped'}
-            </p>
-          </div>
-          <div class="w-12 h-12 rounded-full {status?.running ? 'bg-emerald-100 dark:bg-emerald-900/30' : 'bg-rose-100 dark:bg-rose-900/30'} flex items-center justify-center">
-            <svg class="w-6 h-6 {status?.running ? 'text-emerald-500' : 'text-rose-500'}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
-              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path>
-            </svg>
-          </div>
-        </div>
-      </div>
- 
-      <!-- Active Torrents -->
-      <div class="rounded-xl bg-white dark:bg-surface-800 p-6 border border-surface-200 dark:border-surface-700">
-        <div class="flex items-center justify-between">
-          <div>
-            <p class="text-sm text-surface-500 dark:text-surface-400">Active Torrents</p>
-            <p class="text-2xl font-bold mt-1 text-surface-900 dark:text-white">
-              {torrents.filter(t => t.status === 4 || t.status === 6).length}
-            </p>
-          </div>
-          <div class="w-12 h-12 rounded-full bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center">
-            <svg class="w-6 h-6 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
-              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M9 19l3 3m0 0l3-3m-3 3V10"></path>
-            </svg>
-          </div>
-        </div>
-      </div>
- 
-      <!-- Download Speed -->
-      <div class="rounded-xl bg-white dark:bg-surface-800 p-6 border border-surface-200 dark:border-surface-700">
-        <div class="flex items-center justify-between">
-          <div>
-            <p class="text-sm text-surface-500 dark:text-surface-400">Download</p>
-            <p class="text-2xl font-bold mt-1 text-surface-900 dark:text-white">
-              {fmtSpeed(torrents.reduce((sum, t) => sum + (t.rateDownload || 0), 0))}
-            </p>
-          </div>
-          <div class="w-12 h-12 rounded-full bg-purple-100 dark:bg-purple-900/30 flex items-center justify-center">
-            <svg class="w-6 h-6 text-purple-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
-              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 14l-7 7m0 0l-7-7m7 7V3"></path>
-            </svg>
-          </div>
-        </div>
-      </div>
- 
-      <!-- Upload Speed -->
-      <div class="rounded-xl bg-white dark:bg-surface-800 p-6 border border-surface-200 dark:border-surface-700">
-        <div class="flex items-center justify-between">
-          <div>
-            <p class="text-sm text-surface-500 dark:text-surface-400">Upload</p>
-            <p class="text-2xl font-bold mt-1 text-surface-900 dark:text-white">
-              {fmtSpeed(torrents.reduce((sum, t) => sum + (t.rateUpload || 0), 0))}
-            </p>
-          </div>
-          <div class="w-12 h-12 rounded-full bg-amber-100 dark:bg-amber-900/30 flex items-center justify-center">
-            <svg class="w-6 h-6 text-amber-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
-              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 10l7-7m0 0l7 7m-7-7v18"></path>
-            </svg>
-          </div>
-        </div>
-      </div>
-    </div>
- 
-    <!-- Tabs -->
-    <div class="border-b border-surface-200 dark:border-surface-700">
-      <nav class="-mb-px flex space-x-8">
-        <button
-          onclick={() => selectedTab = "torrents"}
-          class="py-4 px-1 border-b-2 font-medium text-sm {selectedTab === 'torrents' ? 'border-blue-500 text-blue-600 dark:text-blue-400' : 'border-transparent text-surface-500 hover:text-surface-700 hover:border-surface-300 dark:text-surface-400 dark:hover:text-surface-300'}"
-        >
-          Torrents ({torrents.length})
-        </button>
-        <button
-          onclick={() => selectedTab = "trackers"}
-          class="py-4 px-1 border-b-2 font-medium text-sm {selectedTab === 'trackers' ? 'border-blue-500 text-blue-600 dark:text-blue-400' : 'border-transparent text-surface-500 hover:text-surface-700 hover:border-surface-300 dark:text-surface-400 dark:hover:text-surface-300'}"
-        >
-          Trackers ({trackers.length})
-        </button>
-      </nav>
-    </div>
- 
-    <!-- Content -->
-    {#if selectedTab === "torrents"}
-      <div class="rounded-xl bg-white dark:bg-surface-800 border border-surface-200 dark:border-surface-700 overflow-hidden">
-        <div class="overflow-x-auto">
-          <table class="w-full">
-            <thead class="bg-surface-50 dark:bg-surface-900/50">
-              <tr>
-                <th class="px-6 py-3 text-left text-xs font-medium text-surface-500 dark:text-surface-400 uppercase tracking-wider">Name</th>
-                <th class="px-6 py-3 text-left text-xs font-medium text-surface-500 dark:text-surface-400 uppercase tracking-wider">Status</th>
-                <th class="px-6 py-3 text-left text-xs font-medium text-surface-500 dark:text-surface-400 uppercase tracking-wider">Progress</th>
-                <th class="px-6 py-3 text-left text-xs font-medium text-surface-500 dark:text-surface-400 uppercase tracking-wider">Down</th>
-                <th class="px-6 py-3 text-left text-xs font-medium text-surface-500 dark:text-surface-400 uppercase tracking-wider">Up</th>
-                <th class="px-6 py-3 text-left text-xs font-medium text-surface-500 dark:text-surface-400 uppercase tracking-wider">Ratio</th>
-                <th class="px-6 py-3 text-left text-xs font-medium text-surface-500 dark:text-surface-400 uppercase tracking-wider">Tracker</th>
-                <th class="px-6 py-3 text-left text-xs font-medium text-surface-500 dark:text-surface-400 uppercase tracking-wider">Actions</th>
-              </tr>
-            </thead>
-            <tbody class="divide-y divide-surface-200 dark:divide-surface-700">
-              {#each torrents as torrent}
-                <tr class="hover:bg-surface-50 dark:hover:bg-surface-900/30">
-                  <td class="px-6 py-4 text-sm text-surface-900 dark:text-white max-w-md truncate">{torrent.name}</td>
-                  <td class="px-6 py-4 text-sm">
-                    <span class="px-2 py-1 rounded-full text-xs font-medium {torrent.status === 4 || torrent.status === 6 ? 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400' : 'bg-surface-100 text-surface-700 dark:bg-surface-700 dark:text-surface-300'}">
-                      {statusText(torrent.status)}
-                    </span>
-                  </td>
-                  <td class="px-6 py-4 text-sm text-surface-500 dark:text-surface-400">{(torrent.percentDone * 100).toFixed(1)}%</td>
-                  <td class="px-6 py-4 text-sm text-surface-500 dark:text-surface-400">{fmtSpeed(torrent.rateDownload)}</td>
-                  <td class="px-6 py-4 text-sm text-surface-500 dark:text-surface-400">{fmtSpeed(torrent.rateUpload)}</td>
-                  <td class="px-6 py-4 text-sm text-surface-500 dark:text-surface-400">{torrent.uploadRatio?.toFixed(2) || '0.00'}</td>
-                  <td class="px-6 py-4 text-sm">
-                    {#if torrent.hasTrackerErrors}
-                      <span class="text-rose-500 flex items-center gap-1">
-                        <svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
-                          <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"></path>
-                        </svg>
-                        Error
-                      </span>
-                    {:else}
-                      <span class="text-emerald-500">OK</span>
-                    {/if}
-                  </td>
-                  <td class="px-6 py-4 text-sm">
-                    <button
-                      onclick={() => reannounce(torrent.id)}
-                      class="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300"
-                      title="Reannounce to tracker"
-                    >
-                      Reannounce
-                    </button>
-                  </td>
-                </tr>
-              {/each}
-            </tbody>
-          </table>
-        </div>
-      </div>
-    {:else if selectedTab === "trackers"}
-      <div class="rounded-xl bg-white dark:bg-surface-800 border border-surface-200 dark:border-surface-700 overflow-hidden">
-        <div class="overflow-x-auto">
-          <table class="w-full">
-            <thead class="bg-surface-50 dark:bg-surface-900/50">
-              <tr>
-                <th class="px-6 py-3 text-left text-xs font-medium text-surface-500 dark:text-surface-400 uppercase tracking-wider">Tracker</th>
-                <th class="px-6 py-3 text-left text-xs font-medium text-surface-500 dark:text-surface-400 uppercase tracking-wider">Total Torrents</th>
-                <th class="px-6 py-3 text-left text-xs font-medium text-surface-500 dark:text-surface-400 uppercase tracking-wider">Errors</th>
-                <th class="px-6 py-3 text-left text-xs font-medium text-surface-500 dark:text-surface-400 uppercase tracking-wider">Status</th>
-                <th class="px-6 py-3 text-left text-xs font-medium text-surface-500 dark:text-surface-400 uppercase tracking-wider">Last Error</th>
-              </tr>
-            </thead>
-            <tbody class="divide-y divide-surface-200 dark:divide-surface-700">
-              {#each trackers as tracker}
-                <tr class="hover:bg-surface-50 dark:hover:bg-surface-900/30">
-                  <td class="px-6 py-4 text-sm font-medium text-surface-900 dark:text-white">{tracker.host}</td>
-                  <td class="px-6 py-4 text-sm text-surface-500 dark:text-surface-400">{tracker.total}</td>
-                  <td class="px-6 py-4 text-sm text-surface-500 dark:text-surface-400">{tracker.errors}</td>
-                  <td class="px-6 py-4 text-sm">
-                    {#if tracker.errors > 0}
-                      <span class="px-2 py-1 rounded-full text-xs font-medium bg-rose-100 text-rose-700 dark:bg-rose-900/30 dark:text-rose-400">
-                        Error
-                      </span>
-                    {:else}
-                      <span class="px-2 py-1 rounded-full text-xs font-medium bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400">
-                        OK
-                      </span>
-                    {/if}
-                  </td>
-                  <td class="px-6 py-4 text-sm text-surface-500 dark:text-surface-400">{tracker.lastError || '-'}</td>
-                </tr>
-              {/each}
-            </tbody>
-          </table>
-        </div>
-      </div>
-    {/if}
-  {/if}
-</div>
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/dashboard/frontend/coverage/lcov-report/src/routes/index.html b/dashboard/frontend/coverage/lcov-report/src/routes/index.html deleted file mode 100644 index 42a0bd3..0000000 --- a/dashboard/frontend/coverage/lcov-report/src/routes/index.html +++ /dev/null @@ -1,341 +0,0 @@ - - - - - - Code coverage report for src/routes - - - - - - - - - -
-
-

All files src/routes

-
- -
- 2.01% - Statements - 48/2380 -
- - -
- 12% - Branches - 3/25 -
- - -
- 9.52% - Functions - 2/21 -
- - -
- 2.01% - Lines - 48/2380 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FileStatementsBranchesFunctionsLines
CcConnect.svelte -
-
0%0/870%0/10%0/10%0/87
ChatSummary.svelte -
-
0%0/910%0/10%0/10%0/91
Conversations.svelte -
-
0%0/2010%0/10%0/10%0/201
Dashboard.svelte -
-
0%0/870%0/10%0/10%0/87
Docker.svelte -
-
0%0/940%0/10%0/10%0/94
Files.svelte -
-
0%0/1050%0/10%0/10%0/105
Gitea.svelte -
-
0%0/740%0/10%0/10%0/74
InfoEngine.svelte -
-
0%0/920%0/10%0/10%0/92
LiteLLM.svelte -
-
0%0/580%0/10%0/10%0/58
Login.svelte -
-
41.02%48/11730%3/1033.33%2/641.02%48/117
OPC.svelte -
-
0%0/1290%0/10%0/10%0/129
OpenClaw.svelte -
-
0%0/220%0/10%0/10%0/22
Security.svelte -
-
0%0/1710%0/10%0/10%0/171
Settings.svelte -
-
0%0/3620%0/10%0/10%0/362
Terminal.svelte -
-
0%0/5450%0/10%0/10%0/545
Transmission.svelte -
-
0%0/1450%0/10%0/10%0/145
-
-
-
- - - - - - - - \ No newline at end of file diff --git a/dashboard/frontend/coverage/lcov.info b/dashboard/frontend/coverage/lcov.info deleted file mode 100644 index 33e8064..0000000 --- a/dashboard/frontend/coverage/lcov.info +++ /dev/null @@ -1,4265 +0,0 @@ -TN: -SF:src/App.svelte -FN:1,(empty-report) -FNF:1 -FNH:0 -FNDA:0,(empty-report) -DA:1,0 -DA:22,0 -DA:23,0 -DA:24,0 -DA:25,0 -DA:26,0 -DA:27,0 -DA:28,0 -DA:29,0 -DA:30,0 -DA:31,0 -DA:32,0 -DA:33,0 -DA:34,0 -DA:35,0 -DA:36,0 -DA:37,0 -DA:40,0 -DA:41,0 -DA:42,0 -DA:43,0 -DA:44,0 -DA:45,0 -DA:46,0 -DA:47,0 -DA:48,0 -DA:49,0 -DA:51,0 -DA:52,0 -DA:53,0 -DA:54,0 -DA:56,0 -DA:57,0 -DA:58,0 -DA:59,0 -DA:60,0 -DA:61,0 -DA:63,0 -DA:64,0 -DA:65,0 -DA:66,0 -DA:67,0 -DA:68,0 -DA:69,0 -DA:71,0 -DA:72,0 -DA:73,0 -DA:74,0 -DA:75,0 -DA:76,0 -DA:77,0 -DA:79,0 -DA:80,0 -DA:81,0 -DA:82,0 -DA:83,0 -DA:84,0 -DA:85,0 -DA:86,0 -DA:88,0 -DA:89,0 -DA:90,0 -DA:91,0 -DA:92,0 -DA:93,0 -DA:94,0 -DA:95,0 -DA:96,0 -DA:98,0 -DA:99,0 -DA:100,0 -DA:101,0 -DA:103,0 -DA:105,0 -DA:106,0 -DA:107,0 -DA:108,0 -DA:109,0 -DA:110,0 -DA:111,0 -DA:114,0 -DA:115,0 -DA:116,0 -DA:117,0 -DA:118,0 -DA:119,0 -DA:120,0 -DA:121,0 -DA:122,0 -DA:123,0 -DA:125,0 -DA:126,0 -DA:128,0 -DA:131,0 -DA:132,0 -DA:133,0 -DA:134,0 -DA:136,0 -DA:138,0 -DA:139,0 -DA:140,0 -DA:141,0 -DA:142,0 -DA:143,0 -DA:144,0 -DA:145,0 -DA:146,0 -DA:147,0 -DA:148,0 -DA:149,0 -DA:150,0 -DA:152,0 -DA:153,0 -DA:154,0 -DA:155,0 -DA:157,0 -DA:158,0 -DA:159,0 -DA:160,0 -DA:161,0 -DA:162,0 -DA:163,0 -DA:164,0 -DA:165,0 -DA:166,0 -DA:168,0 -DA:169,0 -DA:170,0 -DA:171,0 -DA:172,0 -DA:173,0 -DA:174,0 -DA:175,0 -DA:176,0 -DA:177,0 -DA:179,0 -DA:180,0 -DA:183,0 -DA:184,0 -DA:186,0 -DA:187,0 -DA:188,0 -DA:189,0 -DA:191,0 -DA:194,0 -DA:195,0 -DA:196,0 -DA:197,0 -DA:198,0 -DA:199,0 -DA:200,0 -DA:201,0 -DA:202,0 -DA:203,0 -DA:204,0 -DA:205,0 -DA:206,0 -DA:207,0 -DA:208,0 -DA:209,0 -DA:210,0 -DA:211,0 -DA:212,0 -DA:213,0 -DA:214,0 -DA:215,0 -DA:216,0 -DA:217,0 -DA:218,0 -DA:219,0 -DA:220,0 -DA:221,0 -DA:222,0 -DA:223,0 -DA:226,0 -DA:227,0 -DA:228,0 -LF:177 -LH:0 -BRDA:1,0,0,0 -BRF:1 -BRH:0 -end_of_record -TN: -SF:src/main.js -FN:1,(empty-report) -FNF:1 -FNH:0 -FNDA:0,(empty-report) -DA:1,0 -DA:2,0 -DA:3,0 -DA:5,0 -DA:6,0 -LF:5 -LH:0 -BRDA:1,0,0,0 -BRF:1 -BRH:0 -end_of_record -TN: -SF:src/components/Sidebar.svelte -FN:1,(empty-report) -FNF:1 -FNH:0 -FNDA:0,(empty-report) -DA:1,0 -DA:2,0 -DA:4,0 -DA:5,0 -DA:6,0 -DA:7,0 -DA:8,0 -DA:9,0 -DA:10,0 -DA:12,0 -DA:13,0 -DA:15,0 -DA:16,0 -DA:17,0 -DA:19,0 -DA:20,0 -DA:21,0 -DA:22,0 -DA:23,0 -DA:24,0 -DA:25,0 -DA:26,0 -DA:27,0 -DA:30,0 -DA:31,0 -DA:32,0 -DA:33,0 -DA:35,0 -DA:36,0 -DA:37,0 -DA:38,0 -DA:41,0 -DA:42,0 -DA:43,0 -DA:44,0 -DA:45,0 -DA:46,0 -DA:48,0 -DA:49,0 -DA:50,0 -DA:51,0 -DA:52,0 -DA:53,0 -DA:56,0 -DA:57,0 -DA:58,0 -DA:59,0 -DA:60,0 -DA:61,0 -DA:62,0 -DA:63,0 -DA:64,0 -DA:65,0 -DA:66,0 -DA:69,0 -DA:71,0 -DA:74,0 -DA:77,0 -DA:78,0 -DA:80,0 -DA:81,0 -DA:83,0 -DA:84,0 -DA:86,0 -DA:87,0 -DA:88,0 -DA:89,0 -DA:90,0 -DA:91,0 -DA:92,0 -DA:93,0 -DA:94,0 -DA:97,0 -DA:98,0 -DA:99,0 -DA:100,0 -DA:101,0 -DA:102,0 -DA:103,0 -DA:104,0 -DA:106,0 -DA:107,0 -DA:109,0 -DA:110,0 -DA:111,0 -DA:112,0 -DA:113,0 -DA:116,0 -DA:119,0 -DA:120,0 -DA:121,0 -DA:122,0 -DA:124,0 -DA:125,0 -DA:126,0 -DA:127,0 -DA:128,0 -DA:129,0 -DA:131,0 -DA:132,0 -DA:133,0 -DA:134,0 -DA:135,0 -DA:136,0 -DA:138,0 -DA:139,0 -DA:140,0 -DA:141,0 -DA:142,0 -DA:143,0 -DA:145,0 -DA:146,0 -DA:147,0 -DA:148,0 -DA:149,0 -DA:150,0 -DA:151,0 -DA:152,0 -DA:153,0 -DA:154,0 -DA:155,0 -DA:156,0 -DA:157,0 -DA:159,0 -DA:160,0 -DA:161,0 -DA:164,0 -DA:165,0 -DA:166,0 -DA:167,0 -DA:168,0 -DA:169,0 -DA:171,0 -DA:172,0 -DA:173,0 -DA:174,0 -DA:175,0 -DA:176,0 -DA:177,0 -DA:178,0 -DA:179,0 -DA:180,0 -DA:181,0 -DA:182,0 -DA:184,0 -DA:185,0 -DA:186,0 -DA:188,0 -DA:189,0 -DA:190,0 -DA:192,0 -DA:193,0 -DA:195,0 -DA:196,0 -DA:197,0 -DA:198,0 -DA:199,0 -DA:200,0 -DA:201,0 -DA:202,0 -DA:203,0 -DA:204,0 -DA:205,0 -DA:207,0 -DA:209,0 -DA:210,0 -DA:211,0 -DA:213,0 -DA:214,0 -DA:215,0 -DA:216,0 -DA:217,0 -DA:218,0 -DA:219,0 -DA:220,0 -DA:222,0 -DA:223,0 -DA:224,0 -DA:225,0 -DA:227,0 -DA:228,0 -DA:229,0 -DA:230,0 -DA:231,0 -DA:233,0 -DA:234,0 -DA:235,0 -DA:236,0 -DA:237,0 -DA:238,0 -DA:239,0 -DA:240,0 -DA:242,0 -DA:243,0 -DA:244,0 -DA:246,0 -DA:247,0 -DA:248,0 -DA:249,0 -DA:250,0 -DA:251,0 -DA:252,0 -DA:253,0 -DA:254,0 -DA:255,0 -DA:256,0 -DA:257,0 -DA:258,0 -DA:259,0 -DA:260,0 -DA:261,0 -DA:262,0 -DA:263,0 -DA:264,0 -DA:265,0 -DA:266,0 -DA:267,0 -DA:268,0 -DA:269,0 -DA:270,0 -DA:271,0 -DA:272,0 -DA:273,0 -DA:274,0 -DA:275,0 -DA:276,0 -DA:277,0 -DA:278,0 -DA:279,0 -DA:280,0 -DA:281,0 -DA:282,0 -DA:283,0 -DA:284,0 -DA:285,0 -DA:286,0 -DA:287,0 -DA:288,0 -DA:289,0 -DA:290,0 -DA:291,0 -DA:297,0 -DA:298,0 -DA:301,0 -DA:314,0 -DA:315,0 -DA:316,0 -DA:317,0 -DA:319,0 -DA:320,0 -DA:321,0 -DA:322,0 -DA:323,0 -DA:324,0 -DA:326,0 -DA:327,0 -DA:328,0 -DA:331,0 -DA:332,0 -DA:333,0 -DA:335,0 -DA:336,0 -DA:337,0 -DA:342,0 -DA:343,0 -DA:344,0 -DA:345,0 -DA:346,0 -DA:350,0 -DA:351,0 -DA:352,0 -DA:353,0 -DA:354,0 -DA:357,0 -DA:358,0 -DA:362,0 -DA:364,0 -DA:365,0 -DA:366,0 -DA:367,0 -DA:369,0 -DA:370,0 -DA:371,0 -DA:372,0 -DA:373,0 -DA:376,0 -DA:377,0 -DA:387,0 -DA:388,0 -DA:389,0 -DA:390,0 -DA:391,0 -DA:397,0 -DA:398,0 -DA:399,0 -DA:405,0 -DA:406,0 -DA:408,0 -DA:410,0 -DA:411,0 -DA:413,0 -LF:301 -LH:0 -BRDA:1,0,0,0 -BRF:1 -BRH:0 -end_of_record -TN: -SF:src/components/opc/AgentPanel.svelte -FN:1,(empty-report) -FNF:1 -FNH:0 -FNDA:0,(empty-report) -DA:1,0 -DA:5,0 -DA:6,0 -DA:7,0 -DA:8,0 -DA:10,0 -DA:11,0 -DA:12,0 -DA:13,0 -DA:14,0 -DA:15,0 -DA:17,0 -DA:18,0 -DA:19,0 -DA:20,0 -DA:21,0 -DA:22,0 -DA:23,0 -DA:24,0 -DA:26,0 -DA:27,0 -DA:28,0 -DA:29,0 -DA:30,0 -DA:31,0 -DA:32,0 -DA:33,0 -DA:35,0 -DA:36,0 -DA:38,0 -DA:39,0 -DA:40,0 -DA:41,0 -DA:42,0 -DA:43,0 -DA:44,0 -DA:46,0 -DA:47,0 -DA:49,0 -DA:50,0 -DA:51,0 -DA:53,0 -DA:54,0 -DA:55,0 -DA:56,0 -DA:58,0 -DA:59,0 -DA:61,0 -DA:62,0 -DA:63,0 -DA:64,0 -DA:66,0 -DA:71,0 -DA:72,0 -DA:75,0 -DA:76,0 -DA:77,0 -DA:78,0 -DA:79,0 -DA:81,0 -DA:82,0 -DA:83,0 -DA:86,0 -DA:87,0 -DA:88,0 -DA:89,0 -DA:90,0 -DA:91,0 -DA:93,0 -DA:94,0 -DA:98,0 -DA:99,0 -DA:104,0 -DA:109,0 -DA:110,0 -DA:112,0 -DA:113,0 -DA:116,0 -DA:118,0 -DA:119,0 -DA:125,0 -DA:127,0 -DA:128,0 -DA:129,0 -DA:130,0 -DA:133,0 -DA:134,0 -DA:143,0 -DA:144,0 -DA:145,0 -DA:146,0 -DA:150,0 -DA:151,0 -DA:152,0 -DA:153,0 -DA:154,0 -DA:155,0 -DA:156,0 -DA:157,0 -DA:158,0 -DA:160,0 -DA:161,0 -DA:165,0 -DA:166,0 -DA:170,0 -DA:171,0 -DA:172,0 -DA:176,0 -DA:177,0 -DA:178,0 -DA:179,0 -DA:180,0 -DA:181,0 -DA:187,0 -DA:188,0 -DA:189,0 -DA:194,0 -LF:117 -LH:0 -BRDA:1,0,0,0 -BRF:1 -BRH:0 -end_of_record -TN: -SF:src/components/opc/KanbanBoard.svelte -FN:1,(empty-report) -FNF:1 -FNH:0 -FNDA:0,(empty-report) -DA:1,0 -DA:5,0 -DA:7,0 -DA:8,0 -DA:9,0 -DA:10,0 -DA:13,0 -DA:14,0 -DA:15,0 -DA:17,0 -DA:18,0 -DA:19,0 -DA:20,0 -DA:21,0 -DA:22,0 -DA:23,0 -DA:24,0 -DA:25,0 -DA:27,0 -DA:28,0 -DA:29,0 -DA:30,0 -DA:31,0 -DA:32,0 -DA:33,0 -DA:37,0 -LF:26 -LH:0 -BRDA:1,0,0,0 -BRF:1 -BRH:0 -end_of_record -TN: -SF:src/components/opc/KanbanColumn.svelte -FN:1,(empty-report) -FNF:1 -FNH:0 -FNDA:0,(empty-report) -DA:1,0 -DA:7,0 -DA:8,0 -DA:15,0 -DA:16,0 -DA:17,0 -DA:18,0 -DA:21,0 -DA:22,0 -DA:23,0 -DA:24,0 -DA:26,0 -DA:27,0 -DA:28,0 -DA:30,0 -DA:31,0 -DA:32,0 -DA:34,0 -DA:35,0 -DA:37,0 -DA:38,0 -DA:39,0 -DA:40,0 -DA:42,0 -DA:43,0 -DA:44,0 -DA:45,0 -DA:46,0 -DA:47,0 -DA:49,0 -DA:51,0 -DA:52,0 -DA:55,0 -DA:56,0 -DA:61,0 -DA:62,0 -DA:63,0 -DA:64,0 -DA:65,0 -DA:67,0 -DA:68,0 -DA:72,0 -DA:73,0 -DA:74,0 -DA:75,0 -DA:76,0 -LF:46 -LH:0 -BRDA:1,0,0,0 -BRF:1 -BRH:0 -end_of_record -TN: -SF:src/components/opc/TaskCard.svelte -FN:1,(empty-report) -FNF:1 -FNH:0 -FNDA:0,(empty-report) -DA:1,0 -DA:4,0 -DA:6,0 -DA:7,0 -DA:9,0 -DA:10,0 -DA:11,0 -DA:12,0 -DA:13,0 -DA:16,0 -DA:17,0 -DA:18,0 -DA:19,0 -DA:20,0 -DA:21,0 -DA:22,0 -DA:23,0 -DA:24,0 -DA:26,0 -DA:27,0 -DA:28,0 -DA:30,0 -DA:31,0 -DA:32,0 -DA:33,0 -DA:34,0 -DA:35,0 -DA:36,0 -DA:37,0 -DA:38,0 -DA:39,0 -DA:41,0 -DA:42,0 -DA:43,0 -DA:44,0 -DA:45,0 -DA:46,0 -DA:47,0 -DA:48,0 -DA:50,0 -DA:51,0 -DA:52,0 -DA:53,0 -DA:54,0 -DA:55,0 -DA:56,0 -DA:57,0 -DA:59,0 -DA:60,0 -DA:61,0 -DA:63,0 -DA:65,0 -DA:66,0 -DA:69,0 -DA:70,0 -DA:71,0 -DA:73,0 -DA:74,0 -DA:75,0 -DA:82,0 -DA:83,0 -DA:94,0 -DA:95,0 -DA:99,0 -DA:100,0 -DA:101,0 -DA:106,0 -DA:107,0 -DA:108,0 -DA:109,0 -DA:110,0 -DA:117,0 -DA:118,0 -DA:119,0 -DA:120,0 -DA:121,0 -DA:122,0 -DA:125,0 -DA:128,0 -DA:129,0 -DA:138,0 -DA:139,0 -DA:140,0 -DA:146,0 -DA:147,0 -DA:148,0 -DA:153,0 -DA:154,0 -DA:156,0 -DA:157,0 -DA:158,0 -DA:159,0 -DA:161,0 -DA:163,0 -LF:94 -LH:0 -BRDA:1,0,0,0 -BRF:1 -BRH:0 -end_of_record -TN: -SF:src/components/opc/TaskModal.svelte -FN:1,(empty-report) -FNF:1 -FNH:0 -FNDA:0,(empty-report) -DA:1,0 -DA:4,0 -DA:6,0 -DA:7,0 -DA:8,0 -DA:9,0 -DA:10,0 -DA:11,0 -DA:12,0 -DA:13,0 -DA:14,0 -DA:15,0 -DA:18,0 -DA:19,0 -DA:21,0 -DA:22,0 -DA:23,0 -DA:24,0 -DA:25,0 -DA:26,0 -DA:28,0 -DA:29,0 -DA:30,0 -DA:32,0 -DA:33,0 -DA:34,0 -DA:36,0 -DA:37,0 -DA:38,0 -DA:39,0 -DA:40,0 -DA:43,0 -DA:44,0 -DA:45,0 -DA:46,0 -DA:47,0 -DA:51,0 -DA:52,0 -DA:53,0 -DA:54,0 -DA:55,0 -DA:56,0 -DA:57,0 -DA:60,0 -DA:62,0 -DA:65,0 -DA:67,0 -DA:68,0 -DA:69,0 -DA:71,0 -DA:80,0 -DA:82,0 -DA:86,0 -DA:88,0 -DA:96,0 -DA:100,0 -DA:101,0 -DA:109,0 -DA:110,0 -DA:114,0 -DA:115,0 -DA:118,0 -DA:119,0 -DA:120,0 -DA:124,0 -DA:128,0 -DA:129,0 -DA:132,0 -DA:133,0 -DA:134,0 -DA:135,0 -DA:141,0 -DA:142,0 -DA:146,0 -DA:147,0 -DA:150,0 -DA:151,0 -DA:155,0 -DA:156,0 -DA:157,0 -DA:159,0 -DA:160,0 -DA:161,0 -DA:164,0 -DA:165,0 -DA:166,0 -DA:170,0 -DA:172,0 -DA:181,0 -DA:185,0 -DA:187,0 -DA:193,0 -DA:197,0 -DA:198,0 -DA:200,0 -DA:201,0 -DA:205,0 -DA:207,0 -DA:213,0 -DA:214,0 -DA:215,0 -DA:216,0 -DA:217,0 -DA:218,0 -DA:220,0 -DA:232,0 -DA:233,0 -DA:240,0 -DA:242,0 -DA:245,0 -LF:110 -LH:0 -BRDA:1,0,0,0 -BRF:1 -BRH:0 -end_of_record -TN: -SF:src/lib/api.js -FN:7,setCurrentUser -FN:11,setToken -FN:17,getToken -FN:22,setRefreshToken -FN:26,getLegacyRefreshToken -FN:30,clearLegacyRefreshToken -FN:34,requestRefresh -FN:47,tryRefreshSession -FN:75,request -FN:117,get -FN:121,post -FN:125,del -FN:129,upload -FN:146,login -FN:150,checkAuth -FN:154,logout -FN:158,getPreferences -FN:162,savePreferences -FN:166,getLiteLLMHealth -FN:170,getCcConnectHealth -FN:174,startCcConnect -FN:178,stopCcConnect -FN:182,getInfoEngineItems -FN:193,getInfoEngineItem -FN:197,put -FN:201,download -FNF:26 -FNH:15 -FNDA:1,setCurrentUser -FNDA:30,setToken -FNDA:5,getToken -FNDA:0,setRefreshToken -FNDA:3,getLegacyRefreshToken -FNDA:3,clearLegacyRefreshToken -FNDA:6,requestRefresh -FNDA:5,tryRefreshSession -FNDA:20,request -FNDA:13,get -FNDA:2,post -FNDA:1,del -FNDA:0,upload -FNDA:1,login -FNDA:1,checkAuth -FNDA:1,logout -FNDA:0,getPreferences -FNDA:0,savePreferences -FNDA:0,getLiteLLMHealth -FNDA:0,getCcConnectHealth -FNDA:0,startCcConnect -FNDA:0,stopCcConnect -FNDA:0,getInfoEngineItems -FNDA:0,getInfoEngineItem -FNDA:1,put -FNDA:0,download -DA:1,1 -DA:2,1 -DA:4,1 -DA:5,1 -DA:7,1 -DA:8,1 -DA:9,1 -DA:11,1 -DA:12,30 -DA:13,30 -DA:14,12 -DA:15,30 -DA:17,1 -DA:18,5 -DA:19,5 -DA:21,1 -DA:22,1 -DA:24,1 -DA:26,3 -DA:27,3 -DA:28,3 -DA:30,3 -DA:31,3 -DA:32,3 -DA:34,6 -DA:35,6 -DA:36,6 -DA:37,6 -DA:38,6 -DA:39,6 -DA:40,6 -DA:41,6 -DA:42,3 -DA:43,6 -DA:44,6 -DA:45,6 -DA:47,1 -DA:48,5 -DA:49,5 -DA:50,5 -DA:51,5 -DA:52,5 -DA:53,2 -DA:54,2 -DA:55,2 -DA:57,3 -DA:58,5 -DA:59,1 -DA:60,1 -DA:61,1 -DA:62,1 -DA:63,1 -DA:64,0 -DA:65,0 -DA:66,0 -DA:67,0 -DA:68,5 -DA:69,5 -DA:70,5 -DA:71,5 -DA:72,5 -DA:73,5 -DA:75,20 -DA:76,20 -DA:77,20 -DA:79,20 -DA:80,3 -DA:81,3 -DA:82,3 -DA:83,3 -DA:85,20 -DA:86,20 -DA:88,20 -DA:89,20 -DA:91,20 -DA:92,2 -DA:93,2 -DA:94,1 -DA:95,1 -DA:96,1 -DA:97,2 -DA:99,20 -DA:100,1 -DA:101,1 -DA:102,1 -DA:104,20 -DA:105,2 -DA:106,2 -DA:107,2 -DA:108,2 -DA:109,2 -DA:110,16 -DA:111,20 -DA:112,4 -DA:113,4 -DA:114,4 -DA:115,20 -DA:117,1 -DA:118,13 -DA:119,13 -DA:121,1 -DA:122,2 -DA:123,2 -DA:125,1 -DA:126,1 -DA:127,1 -DA:129,1 -DA:130,0 -DA:131,0 -DA:133,0 -DA:134,0 -DA:136,0 -DA:137,0 -DA:138,0 -DA:139,0 -DA:140,0 -DA:142,0 -DA:143,0 -DA:144,0 -DA:146,1 -DA:147,1 -DA:148,1 -DA:150,1 -DA:151,1 -DA:152,1 -DA:154,1 -DA:155,1 -DA:156,1 -DA:158,1 -DA:159,0 -DA:160,0 -DA:162,1 -DA:163,0 -DA:164,0 -DA:166,1 -DA:167,0 -DA:168,0 -DA:170,1 -DA:171,0 -DA:172,0 -DA:174,1 -DA:175,0 -DA:176,0 -DA:178,1 -DA:179,0 -DA:180,0 -DA:182,1 -DA:183,0 -DA:184,0 -DA:185,0 -DA:186,0 -DA:187,0 -DA:188,0 -DA:189,0 -DA:190,0 -DA:191,0 -DA:193,1 -DA:194,0 -DA:195,0 -DA:197,1 -DA:198,1 -DA:199,1 -DA:201,1 -DA:202,0 -DA:203,0 -DA:205,0 -DA:206,0 -DA:207,0 -DA:208,0 -DA:209,0 -DA:210,0 -DA:212,0 -DA:213,0 -DA:214,0 -DA:215,0 -DA:216,0 -DA:218,0 -DA:220,0 -DA:221,0 -DA:222,0 -DA:223,0 -DA:224,0 -DA:226,0 -DA:227,0 -DA:228,0 -DA:229,0 -DA:230,0 -DA:231,0 -DA:232,0 -DA:233,0 -DA:234,0 -LF:191 -LH:124 -BRDA:7,0,0,1 -BRDA:11,1,0,30 -BRDA:13,2,0,18 -BRDA:13,3,0,12 -BRDA:17,4,0,5 -BRDA:26,5,0,3 -BRDA:27,6,0,2 -BRDA:30,7,0,3 -BRDA:34,8,0,6 -BRDA:39,9,0,1 -BRDA:39,10,0,5 -BRDA:41,11,0,3 -BRDA:43,12,0,0 -BRDA:47,13,0,5 -BRDA:48,14,0,0 -BRDA:49,15,0,5 -BRDA:52,16,0,2 -BRDA:55,17,0,3 -BRDA:58,18,0,2 -BRDA:58,19,0,1 -BRDA:63,20,0,0 -BRDA:75,21,0,20 -BRDA:77,22,0,12 -BRDA:79,23,0,3 -BRDA:86,24,0,3 -BRDA:89,25,0,19 -BRDA:91,26,0,2 -BRDA:91,27,0,2 -BRDA:93,28,0,1 -BRDA:97,29,0,19 -BRDA:99,30,0,1 -BRDA:102,31,0,18 -BRDA:104,32,0,2 -BRDA:107,33,0,0 -BRDA:109,34,0,16 -BRDA:111,35,0,4 -BRDA:117,36,0,13 -BRDA:121,37,0,2 -BRDA:125,38,0,1 -BRDA:146,39,0,1 -BRDA:150,40,0,1 -BRDA:154,41,0,1 -BRDA:197,42,0,1 -BRF:43 -BRH:39 -end_of_record -TN: -SF:src/lib/opc-api.js -FN:1,(empty-report) -FNF:1 -FNH:0 -FNDA:0,(empty-report) -DA:1,0 -DA:2,0 -DA:3,0 -DA:4,0 -DA:6,0 -DA:7,0 -DA:8,0 -DA:9,0 -DA:10,0 -DA:11,0 -DA:12,0 -DA:13,0 -DA:14,0 -DA:15,0 -DA:17,0 -DA:18,0 -DA:19,0 -DA:21,0 -DA:22,0 -DA:23,0 -DA:25,0 -DA:26,0 -DA:27,0 -DA:29,0 -DA:30,0 -DA:31,0 -DA:33,0 -DA:34,0 -DA:35,0 -DA:37,0 -DA:38,0 -DA:39,0 -DA:41,0 -DA:42,0 -DA:43,0 -DA:45,0 -DA:46,0 -DA:47,0 -DA:49,0 -DA:50,0 -DA:51,0 -DA:52,0 -DA:54,0 -DA:55,0 -DA:56,0 -DA:58,0 -DA:59,0 -DA:60,0 -DA:62,0 -DA:63,0 -DA:64,0 -DA:65,0 -DA:66,0 -DA:67,0 -DA:68,0 -DA:70,0 -DA:71,0 -DA:72,0 -DA:74,0 -DA:75,0 -DA:76,0 -DA:78,0 -DA:79,0 -DA:80,0 -LF:64 -LH:0 -BRDA:1,0,0,0 -BRF:1 -BRH:0 -end_of_record -TN: -SF:src/lib/opc-ws.js -FN:1,(empty-report) -FNF:1 -FNH:0 -FNDA:0,(empty-report) -DA:1,0 -DA:2,0 -DA:3,0 -DA:5,0 -DA:7,0 -DA:8,0 -DA:9,0 -DA:11,0 -DA:12,0 -DA:13,0 -DA:14,0 -DA:16,0 -DA:17,0 -DA:18,0 -DA:19,0 -DA:21,0 -DA:23,0 -DA:24,0 -DA:25,0 -DA:26,0 -DA:27,0 -DA:28,0 -DA:30,0 -DA:31,0 -DA:32,0 -DA:33,0 -DA:34,0 -DA:35,0 -DA:36,0 -DA:37,0 -DA:38,0 -DA:40,0 -DA:41,0 -DA:42,0 -DA:43,0 -DA:44,0 -DA:45,0 -DA:46,0 -DA:47,0 -DA:49,0 -DA:50,0 -DA:51,0 -DA:53,0 -DA:54,0 -DA:55,0 -DA:57,0 -DA:58,0 -DA:59,0 -DA:60,0 -DA:61,0 -DA:62,0 -DA:63,0 -DA:65,0 -DA:66,0 -DA:67,0 -DA:68,0 -DA:69,0 -DA:70,0 -DA:71,0 -DA:72,0 -DA:73,0 -DA:74,0 -DA:76,0 -DA:77,0 -DA:78,0 -DA:79,0 -DA:81,0 -DA:82,0 -DA:83,0 -DA:84,0 -DA:85,0 -DA:86,0 -DA:87,0 -DA:88,0 -DA:89,0 -LF:75 -LH:0 -BRDA:1,0,0,0 -BRF:1 -BRH:0 -end_of_record -TN: -SF:src/lib/voice.js -FN:1,(empty-report) -FNF:1 -FNH:0 -FNDA:0,(empty-report) -DA:1,0 -DA:2,0 -DA:3,0 -DA:4,0 -DA:5,0 -DA:6,0 -DA:7,0 -DA:8,0 -DA:9,0 -DA:10,0 -DA:12,0 -DA:13,0 -DA:14,0 -DA:15,0 -DA:16,0 -DA:17,0 -DA:18,0 -DA:19,0 -DA:20,0 -DA:21,0 -DA:22,0 -DA:23,0 -DA:24,0 -DA:25,0 -DA:26,0 -DA:27,0 -DA:28,0 -DA:29,0 -DA:30,0 -DA:31,0 -DA:32,0 -DA:33,0 -DA:35,0 -DA:36,0 -DA:37,0 -DA:38,0 -DA:40,0 -DA:41,0 -DA:42,0 -DA:43,0 -DA:44,0 -DA:45,0 -DA:46,0 -DA:47,0 -DA:48,0 -DA:50,0 -DA:51,0 -DA:52,0 -DA:53,0 -DA:55,0 -DA:56,0 -DA:57,0 -DA:58,0 -DA:59,0 -DA:60,0 -DA:62,0 -DA:64,0 -DA:65,0 -DA:66,0 -DA:67,0 -DA:68,0 -DA:69,0 -DA:70,0 -DA:71,0 -DA:72,0 -DA:73,0 -DA:74,0 -DA:75,0 -DA:76,0 -DA:77,0 -DA:78,0 -DA:79,0 -DA:81,0 -DA:82,0 -DA:83,0 -DA:84,0 -DA:85,0 -DA:86,0 -DA:87,0 -DA:88,0 -DA:89,0 -DA:91,0 -DA:92,0 -DA:93,0 -DA:94,0 -DA:95,0 -DA:96,0 -DA:97,0 -DA:99,0 -DA:100,0 -DA:101,0 -DA:102,0 -DA:104,0 -DA:105,0 -DA:106,0 -DA:107,0 -DA:108,0 -DA:110,0 -DA:111,0 -DA:112,0 -DA:113,0 -DA:115,0 -DA:116,0 -DA:117,0 -DA:118,0 -DA:119,0 -DA:120,0 -DA:122,0 -DA:123,0 -DA:124,0 -DA:125,0 -DA:126,0 -DA:127,0 -DA:128,0 -DA:130,0 -DA:131,0 -DA:132,0 -DA:133,0 -DA:134,0 -DA:135,0 -DA:136,0 -DA:137,0 -DA:138,0 -DA:139,0 -DA:140,0 -DA:141,0 -DA:142,0 -DA:143,0 -DA:144,0 -DA:145,0 -DA:146,0 -DA:147,0 -DA:148,0 -DA:149,0 -DA:150,0 -DA:151,0 -DA:152,0 -DA:153,0 -DA:154,0 -DA:155,0 -DA:156,0 -DA:157,0 -DA:158,0 -DA:159,0 -DA:160,0 -DA:161,0 -DA:162,0 -DA:163,0 -DA:164,0 -DA:165,0 -DA:166,0 -DA:167,0 -DA:168,0 -DA:169,0 -DA:170,0 -DA:171,0 -DA:173,0 -DA:174,0 -DA:175,0 -DA:176,0 -DA:177,0 -DA:178,0 -DA:179,0 -DA:181,0 -DA:182,0 -DA:183,0 -DA:184,0 -DA:185,0 -DA:186,0 -DA:187,0 -DA:188,0 -DA:190,0 -DA:191,0 -DA:192,0 -DA:193,0 -DA:194,0 -DA:195,0 -DA:197,0 -DA:198,0 -DA:199,0 -DA:200,0 -DA:201,0 -DA:202,0 -DA:203,0 -DA:204,0 -DA:205,0 -DA:206,0 -DA:207,0 -DA:208,0 -DA:209,0 -DA:210,0 -DA:212,0 -DA:213,0 -DA:214,0 -DA:215,0 -DA:216,0 -DA:217,0 -DA:218,0 -DA:219,0 -DA:220,0 -DA:221,0 -DA:222,0 -DA:224,0 -DA:225,0 -DA:226,0 -DA:227,0 -DA:228,0 -DA:229,0 -DA:230,0 -DA:231,0 -DA:232,0 -DA:233,0 -DA:235,0 -DA:236,0 -DA:237,0 -DA:238,0 -DA:239,0 -DA:240,0 -DA:241,0 -DA:242,0 -DA:243,0 -DA:244,0 -DA:246,0 -DA:247,0 -DA:248,0 -DA:249,0 -DA:250,0 -DA:251,0 -DA:252,0 -DA:253,0 -DA:254,0 -DA:255,0 -LF:232 -LH:0 -BRDA:1,0,0,0 -BRF:1 -BRH:0 -end_of_record -TN: -SF:src/routes/CcConnect.svelte -FN:1,(empty-report) -FNF:1 -FNH:0 -FNDA:0,(empty-report) -DA:1,0 -DA:5,0 -DA:6,0 -DA:7,0 -DA:8,0 -DA:10,0 -DA:11,0 -DA:12,0 -DA:13,0 -DA:14,0 -DA:15,0 -DA:16,0 -DA:17,0 -DA:18,0 -DA:19,0 -DA:20,0 -DA:21,0 -DA:23,0 -DA:24,0 -DA:25,0 -DA:26,0 -DA:27,0 -DA:28,0 -DA:29,0 -DA:30,0 -DA:31,0 -DA:32,0 -DA:33,0 -DA:34,0 -DA:35,0 -DA:36,0 -DA:37,0 -DA:39,0 -DA:40,0 -DA:41,0 -DA:42,0 -DA:43,0 -DA:44,0 -DA:46,0 -DA:47,0 -DA:48,0 -DA:50,0 -DA:51,0 -DA:53,0 -DA:54,0 -DA:63,0 -DA:64,0 -DA:65,0 -DA:66,0 -DA:67,0 -DA:70,0 -DA:73,0 -DA:74,0 -DA:75,0 -DA:78,0 -DA:81,0 -DA:82,0 -DA:87,0 -DA:88,0 -DA:90,0 -DA:94,0 -DA:95,0 -DA:96,0 -DA:98,0 -DA:99,0 -DA:100,0 -DA:104,0 -DA:105,0 -DA:107,0 -DA:110,0 -DA:111,0 -DA:113,0 -DA:116,0 -DA:118,0 -DA:121,0 -DA:123,0 -DA:126,0 -DA:128,0 -DA:131,0 -DA:133,0 -DA:136,0 -DA:138,0 -DA:141,0 -DA:143,0 -DA:146,0 -DA:148,0 -DA:152,0 -LF:87 -LH:0 -BRDA:1,0,0,0 -BRF:1 -BRH:0 -end_of_record -TN: -SF:src/routes/ChatSummary.svelte -FN:1,(empty-report) -FNF:1 -FNH:0 -FNDA:0,(empty-report) -DA:1,0 -DA:5,0 -DA:6,0 -DA:7,0 -DA:8,0 -DA:9,0 -DA:10,0 -DA:11,0 -DA:12,0 -DA:14,0 -DA:15,0 -DA:16,0 -DA:17,0 -DA:18,0 -DA:19,0 -DA:20,0 -DA:21,0 -DA:23,0 -DA:24,0 -DA:25,0 -DA:26,0 -DA:27,0 -DA:28,0 -DA:29,0 -DA:30,0 -DA:31,0 -DA:32,0 -DA:33,0 -DA:34,0 -DA:35,0 -DA:36,0 -DA:38,0 -DA:39,0 -DA:40,0 -DA:41,0 -DA:42,0 -DA:43,0 -DA:44,0 -DA:45,0 -DA:46,0 -DA:48,0 -DA:49,0 -DA:50,0 -DA:51,0 -DA:52,0 -DA:53,0 -DA:54,0 -DA:55,0 -DA:56,0 -DA:57,0 -DA:59,0 -DA:60,0 -DA:61,0 -DA:62,0 -DA:63,0 -DA:64,0 -DA:65,0 -DA:66,0 -DA:67,0 -DA:68,0 -DA:69,0 -DA:70,0 -DA:71,0 -DA:72,0 -DA:74,0 -DA:75,0 -DA:77,0 -DA:79,0 -DA:83,0 -DA:84,0 -DA:87,0 -DA:88,0 -DA:89,0 -DA:90,0 -DA:91,0 -DA:94,0 -DA:95,0 -DA:99,0 -DA:100,0 -DA:101,0 -DA:102,0 -DA:103,0 -DA:106,0 -DA:108,0 -DA:111,0 -DA:112,0 -DA:113,0 -DA:114,0 -DA:115,0 -DA:116,0 -DA:117,0 -LF:91 -LH:0 -BRDA:1,0,0,0 -BRF:1 -BRH:0 -end_of_record -TN: -SF:src/routes/Conversations.svelte -FN:1,(empty-report) -FNF:1 -FNH:0 -FNDA:0,(empty-report) -DA:1,0 -DA:5,0 -DA:6,0 -DA:7,0 -DA:8,0 -DA:9,0 -DA:10,0 -DA:11,0 -DA:12,0 -DA:13,0 -DA:14,0 -DA:15,0 -DA:16,0 -DA:17,0 -DA:19,0 -DA:20,0 -DA:21,0 -DA:22,0 -DA:23,0 -DA:24,0 -DA:25,0 -DA:26,0 -DA:27,0 -DA:29,0 -DA:30,0 -DA:31,0 -DA:32,0 -DA:33,0 -DA:34,0 -DA:35,0 -DA:36,0 -DA:37,0 -DA:38,0 -DA:39,0 -DA:40,0 -DA:41,0 -DA:42,0 -DA:43,0 -DA:45,0 -DA:46,0 -DA:47,0 -DA:48,0 -DA:49,0 -DA:50,0 -DA:51,0 -DA:52,0 -DA:53,0 -DA:54,0 -DA:55,0 -DA:56,0 -DA:58,0 -DA:59,0 -DA:60,0 -DA:61,0 -DA:62,0 -DA:63,0 -DA:64,0 -DA:65,0 -DA:66,0 -DA:67,0 -DA:68,0 -DA:69,0 -DA:71,0 -DA:72,0 -DA:73,0 -DA:74,0 -DA:75,0 -DA:76,0 -DA:77,0 -DA:79,0 -DA:80,0 -DA:81,0 -DA:82,0 -DA:83,0 -DA:84,0 -DA:85,0 -DA:86,0 -DA:87,0 -DA:88,0 -DA:89,0 -DA:91,0 -DA:92,0 -DA:93,0 -DA:94,0 -DA:95,0 -DA:96,0 -DA:97,0 -DA:98,0 -DA:99,0 -DA:100,0 -DA:101,0 -DA:102,0 -DA:103,0 -DA:105,0 -DA:106,0 -DA:107,0 -DA:108,0 -DA:110,0 -DA:111,0 -DA:113,0 -DA:114,0 -DA:117,0 -DA:119,0 -DA:124,0 -DA:125,0 -DA:129,0 -DA:130,0 -DA:132,0 -DA:133,0 -DA:140,0 -DA:141,0 -DA:142,0 -DA:143,0 -DA:144,0 -DA:145,0 -DA:148,0 -DA:149,0 -DA:154,0 -DA:155,0 -DA:156,0 -DA:158,0 -DA:159,0 -DA:161,0 -DA:163,0 -DA:165,0 -DA:167,0 -DA:169,0 -DA:171,0 -DA:173,0 -DA:177,0 -DA:178,0 -DA:182,0 -DA:183,0 -DA:184,0 -DA:186,0 -DA:187,0 -DA:188,0 -DA:190,0 -DA:191,0 -DA:192,0 -DA:193,0 -DA:195,0 -DA:196,0 -DA:197,0 -DA:204,0 -DA:206,0 -DA:208,0 -DA:209,0 -DA:210,0 -DA:211,0 -DA:212,0 -DA:213,0 -DA:214,0 -DA:219,0 -DA:220,0 -DA:221,0 -DA:222,0 -DA:223,0 -DA:224,0 -DA:226,0 -DA:227,0 -DA:228,0 -DA:229,0 -DA:232,0 -DA:233,0 -DA:234,0 -DA:241,0 -DA:243,0 -DA:244,0 -DA:245,0 -DA:246,0 -DA:248,0 -DA:249,0 -DA:250,0 -DA:252,0 -DA:253,0 -DA:254,0 -DA:255,0 -DA:257,0 -DA:261,0 -DA:262,0 -DA:266,0 -DA:268,0 -DA:269,0 -DA:270,0 -DA:272,0 -DA:274,0 -DA:276,0 -DA:278,0 -DA:280,0 -DA:284,0 -DA:285,0 -DA:287,0 -DA:288,0 -DA:289,0 -DA:290,0 -DA:295,0 -DA:297,0 -DA:298,0 -DA:299,0 -DA:300,0 -LF:201 -LH:0 -BRDA:1,0,0,0 -BRF:1 -BRH:0 -end_of_record -TN: -SF:src/routes/Dashboard.svelte -FN:1,(empty-report) -FNF:1 -FNH:0 -FNDA:0,(empty-report) -DA:1,0 -DA:5,0 -DA:6,0 -DA:7,0 -DA:8,0 -DA:10,0 -DA:11,0 -DA:12,0 -DA:13,0 -DA:14,0 -DA:16,0 -DA:17,0 -DA:18,0 -DA:19,0 -DA:20,0 -DA:22,0 -DA:23,0 -DA:24,0 -DA:25,0 -DA:26,0 -DA:27,0 -DA:29,0 -DA:30,0 -DA:31,0 -DA:32,0 -DA:33,0 -DA:35,0 -DA:37,0 -DA:38,0 -DA:39,0 -DA:41,0 -DA:47,0 -DA:48,0 -DA:49,0 -DA:50,0 -DA:55,0 -DA:57,0 -DA:64,0 -DA:65,0 -DA:69,0 -DA:76,0 -DA:77,0 -DA:81,0 -DA:88,0 -DA:89,0 -DA:93,0 -DA:100,0 -DA:101,0 -DA:106,0 -DA:108,0 -DA:109,0 -DA:111,0 -DA:113,0 -DA:114,0 -DA:115,0 -DA:116,0 -DA:117,0 -DA:118,0 -DA:120,0 -DA:123,0 -DA:124,0 -DA:130,0 -DA:131,0 -DA:133,0 -DA:135,0 -DA:136,0 -DA:137,0 -DA:138,0 -DA:140,0 -DA:142,0 -DA:145,0 -DA:146,0 -DA:153,0 -DA:154,0 -DA:156,0 -DA:157,0 -DA:158,0 -DA:159,0 -DA:160,0 -DA:161,0 -DA:163,0 -DA:164,0 -DA:165,0 -DA:166,0 -DA:169,0 -DA:170,0 -DA:171,0 -LF:87 -LH:0 -BRDA:1,0,0,0 -BRF:1 -BRH:0 -end_of_record -TN: -SF:src/routes/Docker.svelte -FN:1,(empty-report) -FNF:1 -FNH:0 -FNDA:0,(empty-report) -DA:1,0 -DA:5,0 -DA:6,0 -DA:7,0 -DA:8,0 -DA:9,0 -DA:10,0 -DA:11,0 -DA:13,0 -DA:14,0 -DA:15,0 -DA:16,0 -DA:17,0 -DA:18,0 -DA:19,0 -DA:20,0 -DA:21,0 -DA:22,0 -DA:23,0 -DA:25,0 -DA:26,0 -DA:27,0 -DA:28,0 -DA:29,0 -DA:30,0 -DA:31,0 -DA:32,0 -DA:33,0 -DA:34,0 -DA:36,0 -DA:37,0 -DA:38,0 -DA:39,0 -DA:40,0 -DA:41,0 -DA:42,0 -DA:43,0 -DA:44,0 -DA:45,0 -DA:46,0 -DA:48,0 -DA:49,0 -DA:51,0 -DA:52,0 -DA:53,0 -DA:55,0 -DA:57,0 -DA:62,0 -DA:63,0 -DA:64,0 -DA:65,0 -DA:68,0 -DA:69,0 -DA:70,0 -DA:71,0 -DA:73,0 -DA:75,0 -DA:81,0 -DA:82,0 -DA:83,0 -DA:84,0 -DA:85,0 -DA:86,0 -DA:90,0 -DA:93,0 -DA:94,0 -DA:95,0 -DA:98,0 -DA:99,0 -DA:100,0 -DA:102,0 -DA:103,0 -DA:105,0 -DA:107,0 -DA:109,0 -DA:110,0 -DA:112,0 -DA:115,0 -DA:117,0 -DA:118,0 -DA:120,0 -DA:123,0 -DA:125,0 -DA:136,0 -DA:137,0 -DA:138,0 -DA:139,0 -DA:140,0 -DA:142,0 -DA:144,0 -DA:148,0 -DA:149,0 -DA:150,0 -DA:154,0 -LF:94 -LH:0 -BRDA:1,0,0,0 -BRF:1 -BRH:0 -end_of_record -TN: -SF:src/routes/Files.svelte -FN:1,(empty-report) -FNF:1 -FNH:0 -FNDA:0,(empty-report) -DA:1,0 -DA:5,0 -DA:6,0 -DA:7,0 -DA:8,0 -DA:9,0 -DA:10,0 -DA:12,0 -DA:13,0 -DA:14,0 -DA:15,0 -DA:16,0 -DA:17,0 -DA:18,0 -DA:20,0 -DA:21,0 -DA:22,0 -DA:23,0 -DA:24,0 -DA:26,0 -DA:27,0 -DA:28,0 -DA:29,0 -DA:30,0 -DA:31,0 -DA:33,0 -DA:34,0 -DA:35,0 -DA:36,0 -DA:37,0 -DA:38,0 -DA:39,0 -DA:40,0 -DA:41,0 -DA:42,0 -DA:44,0 -DA:45,0 -DA:46,0 -DA:47,0 -DA:48,0 -DA:49,0 -DA:50,0 -DA:51,0 -DA:52,0 -DA:54,0 -DA:55,0 -DA:56,0 -DA:57,0 -DA:58,0 -DA:59,0 -DA:61,0 -DA:62,0 -DA:63,0 -DA:64,0 -DA:65,0 -DA:66,0 -DA:67,0 -DA:68,0 -DA:69,0 -DA:71,0 -DA:73,0 -DA:74,0 -DA:76,0 -DA:77,0 -DA:78,0 -DA:80,0 -DA:82,0 -DA:83,0 -DA:84,0 -DA:85,0 -DA:87,0 -DA:89,0 -DA:95,0 -DA:96,0 -DA:100,0 -DA:101,0 -DA:103,0 -DA:112,0 -DA:113,0 -DA:114,0 -DA:116,0 -DA:118,0 -DA:119,0 -DA:121,0 -DA:122,0 -DA:124,0 -DA:133,0 -DA:134,0 -DA:135,0 -DA:136,0 -DA:140,0 -DA:148,0 -DA:149,0 -DA:150,0 -DA:151,0 -DA:153,0 -DA:156,0 -DA:158,0 -DA:161,0 -DA:162,0 -DA:163,0 -DA:164,0 -DA:168,0 -DA:172,0 -DA:173,0 -LF:105 -LH:0 -BRDA:1,0,0,0 -BRF:1 -BRH:0 -end_of_record -TN: -SF:src/routes/Gitea.svelte -FN:1,(empty-report) -FNF:1 -FNH:0 -FNDA:0,(empty-report) -DA:1,0 -DA:5,0 -DA:6,0 -DA:7,0 -DA:8,0 -DA:9,0 -DA:11,0 -DA:12,0 -DA:13,0 -DA:14,0 -DA:15,0 -DA:16,0 -DA:18,0 -DA:19,0 -DA:20,0 -DA:21,0 -DA:22,0 -DA:23,0 -DA:24,0 -DA:26,0 -DA:27,0 -DA:28,0 -DA:29,0 -DA:30,0 -DA:31,0 -DA:32,0 -DA:34,0 -DA:35,0 -DA:36,0 -DA:39,0 -DA:40,0 -DA:42,0 -DA:43,0 -DA:45,0 -DA:48,0 -DA:49,0 -DA:50,0 -DA:51,0 -DA:55,0 -DA:56,0 -DA:57,0 -DA:58,0 -DA:59,0 -DA:61,0 -DA:62,0 -DA:63,0 -DA:64,0 -DA:66,0 -DA:67,0 -DA:68,0 -DA:69,0 -DA:73,0 -DA:80,0 -DA:81,0 -DA:82,0 -DA:83,0 -DA:85,0 -DA:87,0 -DA:90,0 -DA:95,0 -DA:96,0 -DA:97,0 -DA:98,0 -DA:102,0 -DA:103,0 -DA:104,0 -DA:105,0 -DA:107,0 -DA:108,0 -DA:111,0 -DA:112,0 -DA:113,0 -DA:114,0 -DA:117,0 -LF:74 -LH:0 -BRDA:1,0,0,0 -BRF:1 -BRH:0 -end_of_record -TN: -SF:src/routes/InfoEngine.svelte -FN:1,(empty-report) -FNF:1 -FNH:0 -FNDA:0,(empty-report) -DA:1,0 -DA:5,0 -DA:6,0 -DA:7,0 -DA:8,0 -DA:9,0 -DA:10,0 -DA:11,0 -DA:13,0 -DA:14,0 -DA:15,0 -DA:16,0 -DA:17,0 -DA:18,0 -DA:19,0 -DA:20,0 -DA:21,0 -DA:22,0 -DA:23,0 -DA:24,0 -DA:25,0 -DA:26,0 -DA:27,0 -DA:28,0 -DA:29,0 -DA:30,0 -DA:31,0 -DA:32,0 -DA:33,0 -DA:34,0 -DA:35,0 -DA:37,0 -DA:38,0 -DA:39,0 -DA:40,0 -DA:41,0 -DA:42,0 -DA:43,0 -DA:44,0 -DA:45,0 -DA:46,0 -DA:47,0 -DA:48,0 -DA:49,0 -DA:51,0 -DA:52,0 -DA:53,0 -DA:54,0 -DA:55,0 -DA:56,0 -DA:57,0 -DA:58,0 -DA:60,0 -DA:61,0 -DA:63,0 -DA:64,0 -DA:69,0 -DA:70,0 -DA:74,0 -DA:75,0 -DA:76,0 -DA:80,0 -DA:81,0 -DA:82,0 -DA:83,0 -DA:85,0 -DA:86,0 -DA:87,0 -DA:88,0 -DA:91,0 -DA:92,0 -DA:94,0 -DA:95,0 -DA:96,0 -DA:97,0 -DA:98,0 -DA:105,0 -DA:106,0 -DA:107,0 -DA:113,0 -DA:114,0 -DA:115,0 -DA:116,0 -DA:117,0 -DA:118,0 -DA:121,0 -DA:122,0 -DA:123,0 -DA:124,0 -DA:129,0 -DA:131,0 -DA:135,0 -LF:92 -LH:0 -BRDA:1,0,0,0 -BRF:1 -BRH:0 -end_of_record -TN: -SF:src/routes/LiteLLM.svelte -FN:1,(empty-report) -FNF:1 -FNH:0 -FNDA:0,(empty-report) -DA:1,0 -DA:5,0 -DA:6,0 -DA:7,0 -DA:9,0 -DA:10,0 -DA:11,0 -DA:12,0 -DA:13,0 -DA:14,0 -DA:15,0 -DA:16,0 -DA:17,0 -DA:18,0 -DA:19,0 -DA:20,0 -DA:22,0 -DA:23,0 -DA:24,0 -DA:25,0 -DA:26,0 -DA:27,0 -DA:28,0 -DA:29,0 -DA:30,0 -DA:32,0 -DA:33,0 -DA:34,0 -DA:36,0 -DA:37,0 -DA:39,0 -DA:40,0 -DA:50,0 -DA:51,0 -DA:55,0 -DA:56,0 -DA:58,0 -DA:62,0 -DA:63,0 -DA:64,0 -DA:66,0 -DA:67,0 -DA:68,0 -DA:72,0 -DA:73,0 -DA:75,0 -DA:78,0 -DA:79,0 -DA:81,0 -DA:84,0 -DA:86,0 -DA:89,0 -DA:91,0 -DA:94,0 -DA:96,0 -DA:99,0 -DA:101,0 -DA:105,0 -LF:58 -LH:0 -BRDA:1,0,0,0 -BRF:1 -BRH:0 -end_of_record -TN: -SF:src/routes/Login.svelte -FN:1,Login -FN:13,base64urlToBuffer -FN:19,bufferToBase64url -FN:26,loginWithPasskey -FN:69,handleSubmit -FN:104,Login.__vite_ssr_import_1__.transition.y -FNF:6 -FNH:2 -FNDA:3,Login -FNDA:0,base64urlToBuffer -FNDA:0,bufferToBase64url -FNDA:0,loginWithPasskey -FNDA:0,handleSubmit -FNDA:3,Login.__vite_ssr_import_1__.transition.y -DA:1,3 -DA:5,3 -DA:6,3 -DA:7,3 -DA:8,3 -DA:9,3 -DA:10,3 -DA:11,3 -DA:13,3 -DA:14,0 -DA:15,0 -DA:16,0 -DA:17,0 -DA:19,3 -DA:20,0 -DA:21,0 -DA:22,0 -DA:23,0 -DA:24,0 -DA:26,3 -DA:27,0 -DA:28,0 -DA:29,0 -DA:30,0 -DA:31,0 -DA:32,0 -DA:33,0 -DA:34,0 -DA:35,0 -DA:36,0 -DA:38,0 -DA:39,0 -DA:40,0 -DA:41,0 -DA:42,0 -DA:43,0 -DA:44,0 -DA:45,0 -DA:46,0 -DA:47,0 -DA:48,0 -DA:51,0 -DA:52,0 -DA:53,0 -DA:54,0 -DA:56,0 -DA:57,0 -DA:58,0 -DA:59,0 -DA:60,0 -DA:61,0 -DA:62,0 -DA:63,0 -DA:64,0 -DA:65,0 -DA:66,0 -DA:67,0 -DA:69,3 -DA:70,0 -DA:71,0 -DA:72,0 -DA:74,0 -DA:75,0 -DA:76,0 -DA:78,0 -DA:80,0 -DA:83,0 -DA:85,0 -DA:86,0 -DA:87,0 -DA:89,0 -DA:90,0 -DA:91,0 -DA:93,0 -DA:94,0 -DA:95,0 -DA:96,0 -DA:97,0 -DA:98,0 -DA:99,0 -DA:100,0 -DA:101,3 -DA:103,1 -DA:104,1 -DA:117,1 -DA:118,1 -DA:119,1 -DA:121,1 -DA:122,1 -DA:125,1 -DA:126,1 -DA:139,1 -DA:144,1 -DA:145,1 -DA:146,1 -DA:147,1 -DA:148,1 -DA:150,1 -DA:155,1 -DA:160,1 -DA:162,1 -DA:167,1 -DA:173,1 -DA:175,1 -DA:184,1 -DA:189,1 -DA:190,1 -DA:198,1 -DA:200,1 -DA:203,1 -DA:204,1 -DA:209,1 -DA:214,1 -DA:215,1 -DA:216,1 -DA:222,1 -DA:223,1 -LF:117 -LH:48 -BRDA:1,0,0,3 -BRDA:118,1,0,0 -BRDA:118,2,0,0 -BRDA:147,3,0,0 -BRDA:203,4,0,0 -BRDA:209,5,0,0 -BRDA:144,6,0,0 -BRDA:214,7,0,0 -BRDA:104,8,0,3 -BRDA:104,9,0,3 -BRF:10 -BRH:3 -end_of_record -TN: -SF:src/routes/OPC.svelte -FN:1,(empty-report) -FNF:1 -FNH:0 -FNDA:0,(empty-report) -DA:1,0 -DA:9,0 -DA:10,0 -DA:11,0 -DA:12,0 -DA:13,0 -DA:14,0 -DA:15,0 -DA:16,0 -DA:17,0 -DA:19,0 -DA:22,0 -DA:23,0 -DA:24,0 -DA:25,0 -DA:26,0 -DA:28,0 -DA:29,0 -DA:30,0 -DA:31,0 -DA:32,0 -DA:33,0 -DA:35,0 -DA:36,0 -DA:37,0 -DA:38,0 -DA:39,0 -DA:40,0 -DA:41,0 -DA:42,0 -DA:44,0 -DA:45,0 -DA:46,0 -DA:48,0 -DA:49,0 -DA:50,0 -DA:51,0 -DA:52,0 -DA:53,0 -DA:54,0 -DA:55,0 -DA:57,0 -DA:58,0 -DA:59,0 -DA:61,0 -DA:62,0 -DA:63,0 -DA:64,0 -DA:66,0 -DA:67,0 -DA:68,0 -DA:69,0 -DA:71,0 -DA:72,0 -DA:73,0 -DA:75,0 -DA:76,0 -DA:77,0 -DA:78,0 -DA:79,0 -DA:80,0 -DA:81,0 -DA:82,0 -DA:84,0 -DA:85,0 -DA:86,0 -DA:87,0 -DA:88,0 -DA:89,0 -DA:90,0 -DA:92,0 -DA:93,0 -DA:94,0 -DA:95,0 -DA:97,0 -DA:98,0 -DA:99,0 -DA:101,0 -DA:102,0 -DA:105,0 -DA:106,0 -DA:107,0 -DA:109,0 -DA:110,0 -DA:111,0 -DA:112,0 -DA:113,0 -DA:114,0 -DA:115,0 -DA:117,0 -DA:119,0 -DA:128,0 -DA:129,0 -DA:130,0 -DA:134,0 -DA:136,0 -DA:137,0 -DA:147,0 -DA:148,0 -DA:150,0 -DA:151,0 -DA:154,0 -DA:156,0 -DA:157,0 -DA:160,0 -DA:162,0 -DA:163,0 -DA:166,0 -DA:168,0 -DA:169,0 -DA:175,0 -DA:176,0 -DA:180,0 -DA:181,0 -DA:182,0 -DA:183,0 -DA:184,0 -DA:185,0 -DA:186,0 -DA:187,0 -DA:193,0 -DA:194,0 -DA:195,0 -DA:201,0 -DA:202,0 -DA:203,0 -DA:204,0 -DA:205,0 -DA:206,0 -LF:129 -LH:0 -BRDA:1,0,0,0 -BRF:1 -BRH:0 -end_of_record -TN: -SF:src/routes/OpenClaw.svelte -FN:1,(empty-report) -FNF:1 -FNH:0 -FNDA:0,(empty-report) -DA:1,0 -DA:5,0 -DA:6,0 -DA:7,0 -DA:8,0 -DA:10,0 -DA:11,0 -DA:12,0 -DA:13,0 -DA:14,0 -DA:15,0 -DA:17,0 -DA:18,0 -DA:19,0 -DA:20,0 -DA:21,0 -DA:23,0 -DA:29,0 -DA:30,0 -DA:40,0 -DA:41,0 -DA:53,0 -LF:22 -LH:0 -BRDA:1,0,0,0 -BRF:1 -BRH:0 -end_of_record -TN: -SF:src/routes/Security.svelte -FN:1,(empty-report) -FNF:1 -FNH:0 -FNDA:0,(empty-report) -DA:1,0 -DA:5,0 -DA:6,0 -DA:7,0 -DA:8,0 -DA:9,0 -DA:10,0 -DA:11,0 -DA:12,0 -DA:14,0 -DA:16,0 -DA:17,0 -DA:18,0 -DA:19,0 -DA:20,0 -DA:22,0 -DA:23,0 -DA:24,0 -DA:25,0 -DA:26,0 -DA:27,0 -DA:28,0 -DA:30,0 -DA:31,0 -DA:32,0 -DA:33,0 -DA:34,0 -DA:35,0 -DA:36,0 -DA:37,0 -DA:39,0 -DA:40,0 -DA:41,0 -DA:42,0 -DA:43,0 -DA:44,0 -DA:45,0 -DA:46,0 -DA:48,0 -DA:49,0 -DA:50,0 -DA:52,0 -DA:54,0 -DA:55,0 -DA:56,0 -DA:57,0 -DA:58,0 -DA:59,0 -DA:60,0 -DA:62,0 -DA:63,0 -DA:64,0 -DA:65,0 -DA:67,0 -DA:68,0 -DA:69,0 -DA:70,0 -DA:72,0 -DA:73,0 -DA:74,0 -DA:75,0 -DA:76,0 -DA:77,0 -DA:78,0 -DA:79,0 -DA:81,0 -DA:82,0 -DA:85,0 -DA:86,0 -DA:89,0 -DA:90,0 -DA:92,0 -DA:94,0 -DA:95,0 -DA:96,0 -DA:97,0 -DA:98,0 -DA:99,0 -DA:100,0 -DA:101,0 -DA:102,0 -DA:103,0 -DA:104,0 -DA:105,0 -DA:106,0 -DA:107,0 -DA:109,0 -DA:110,0 -DA:111,0 -DA:112,0 -DA:113,0 -DA:114,0 -DA:117,0 -DA:118,0 -DA:119,0 -DA:120,0 -DA:121,0 -DA:122,0 -DA:124,0 -DA:126,0 -DA:127,0 -DA:129,0 -DA:130,0 -DA:135,0 -DA:136,0 -DA:137,0 -DA:139,0 -DA:141,0 -DA:143,0 -DA:145,0 -DA:147,0 -DA:153,0 -DA:154,0 -DA:156,0 -DA:157,0 -DA:158,0 -DA:159,0 -DA:160,0 -DA:161,0 -DA:162,0 -DA:175,0 -DA:176,0 -DA:178,0 -DA:179,0 -DA:180,0 -DA:181,0 -DA:182,0 -DA:183,0 -DA:185,0 -DA:193,0 -DA:194,0 -DA:199,0 -DA:200,0 -DA:206,0 -DA:207,0 -DA:208,0 -DA:209,0 -DA:214,0 -DA:215,0 -DA:217,0 -DA:219,0 -DA:221,0 -DA:225,0 -DA:227,0 -DA:229,0 -DA:230,0 -DA:234,0 -DA:236,0 -DA:238,0 -DA:239,0 -DA:245,0 -DA:246,0 -DA:249,0 -DA:250,0 -DA:256,0 -DA:257,0 -DA:260,0 -DA:261,0 -DA:262,0 -DA:263,0 -DA:264,0 -DA:265,0 -DA:266,0 -DA:267,0 -DA:269,0 -DA:270,0 -DA:272,0 -DA:273,0 -DA:275,0 -DA:279,0 -DA:280,0 -LF:171 -LH:0 -BRDA:1,0,0,0 -BRF:1 -BRH:0 -end_of_record -TN: -SF:src/routes/Settings.svelte -FN:1,(empty-report) -FNF:1 -FNH:0 -FNDA:0,(empty-report) -DA:1,0 -DA:6,0 -DA:7,0 -DA:8,0 -DA:9,0 -DA:10,0 -DA:11,0 -DA:13,0 -DA:14,0 -DA:15,0 -DA:16,0 -DA:18,0 -DA:19,0 -DA:20,0 -DA:21,0 -DA:22,0 -DA:23,0 -DA:25,0 -DA:26,0 -DA:27,0 -DA:28,0 -DA:29,0 -DA:31,0 -DA:32,0 -DA:33,0 -DA:34,0 -DA:35,0 -DA:36,0 -DA:38,0 -DA:39,0 -DA:40,0 -DA:41,0 -DA:42,0 -DA:43,0 -DA:44,0 -DA:45,0 -DA:46,0 -DA:47,0 -DA:48,0 -DA:49,0 -DA:50,0 -DA:51,0 -DA:52,0 -DA:53,0 -DA:54,0 -DA:55,0 -DA:56,0 -DA:57,0 -DA:58,0 -DA:59,0 -DA:60,0 -DA:61,0 -DA:62,0 -DA:63,0 -DA:64,0 -DA:65,0 -DA:66,0 -DA:67,0 -DA:68,0 -DA:71,0 -DA:72,0 -DA:73,0 -DA:74,0 -DA:75,0 -DA:76,0 -DA:77,0 -DA:78,0 -DA:79,0 -DA:80,0 -DA:81,0 -DA:82,0 -DA:84,0 -DA:85,0 -DA:86,0 -DA:87,0 -DA:88,0 -DA:89,0 -DA:90,0 -DA:91,0 -DA:92,0 -DA:93,0 -DA:94,0 -DA:95,0 -DA:96,0 -DA:98,0 -DA:99,0 -DA:100,0 -DA:101,0 -DA:102,0 -DA:103,0 -DA:104,0 -DA:105,0 -DA:106,0 -DA:107,0 -DA:108,0 -DA:109,0 -DA:110,0 -DA:112,0 -DA:113,0 -DA:114,0 -DA:115,0 -DA:116,0 -DA:117,0 -DA:118,0 -DA:119,0 -DA:120,0 -DA:121,0 -DA:122,0 -DA:123,0 -DA:124,0 -DA:125,0 -DA:126,0 -DA:128,0 -DA:129,0 -DA:130,0 -DA:132,0 -DA:133,0 -DA:135,0 -DA:137,0 -DA:138,0 -DA:139,0 -DA:140,0 -DA:141,0 -DA:142,0 -DA:143,0 -DA:144,0 -DA:147,0 -DA:148,0 -DA:149,0 -DA:150,0 -DA:151,0 -DA:152,0 -DA:153,0 -DA:154,0 -DA:155,0 -DA:156,0 -DA:157,0 -DA:158,0 -DA:160,0 -DA:161,0 -DA:162,0 -DA:163,0 -DA:164,0 -DA:165,0 -DA:166,0 -DA:168,0 -DA:169,0 -DA:170,0 -DA:171,0 -DA:172,0 -DA:173,0 -DA:175,0 -DA:176,0 -DA:177,0 -DA:178,0 -DA:179,0 -DA:180,0 -DA:182,0 -DA:183,0 -DA:184,0 -DA:185,0 -DA:186,0 -DA:187,0 -DA:188,0 -DA:189,0 -DA:191,0 -DA:192,0 -DA:193,0 -DA:194,0 -DA:195,0 -DA:196,0 -DA:198,0 -DA:199,0 -DA:200,0 -DA:201,0 -DA:202,0 -DA:203,0 -DA:204,0 -DA:205,0 -DA:206,0 -DA:208,0 -DA:209,0 -DA:210,0 -DA:211,0 -DA:212,0 -DA:213,0 -DA:214,0 -DA:215,0 -DA:216,0 -DA:218,0 -DA:219,0 -DA:220,0 -DA:221,0 -DA:223,0 -DA:224,0 -DA:225,0 -DA:226,0 -DA:227,0 -DA:228,0 -DA:229,0 -DA:230,0 -DA:231,0 -DA:232,0 -DA:233,0 -DA:234,0 -DA:235,0 -DA:236,0 -DA:238,0 -DA:239,0 -DA:240,0 -DA:241,0 -DA:242,0 -DA:243,0 -DA:244,0 -DA:245,0 -DA:246,0 -DA:247,0 -DA:248,0 -DA:250,0 -DA:251,0 -DA:252,0 -DA:253,0 -DA:254,0 -DA:255,0 -DA:256,0 -DA:257,0 -DA:258,0 -DA:259,0 -DA:260,0 -DA:261,0 -DA:262,0 -DA:263,0 -DA:264,0 -DA:265,0 -DA:267,0 -DA:268,0 -DA:269,0 -DA:270,0 -DA:271,0 -DA:273,0 -DA:279,0 -DA:282,0 -DA:283,0 -DA:285,0 -DA:286,0 -DA:289,0 -DA:290,0 -DA:292,0 -DA:294,0 -DA:296,0 -DA:298,0 -DA:300,0 -DA:302,0 -DA:303,0 -DA:308,0 -DA:311,0 -DA:312,0 -DA:314,0 -DA:315,0 -DA:318,0 -DA:319,0 -DA:320,0 -DA:321,0 -DA:322,0 -DA:323,0 -DA:324,0 -DA:325,0 -DA:328,0 -DA:333,0 -DA:336,0 -DA:337,0 -DA:340,0 -DA:341,0 -DA:348,0 -DA:349,0 -DA:350,0 -DA:352,0 -DA:353,0 -DA:357,0 -DA:358,0 -DA:360,0 -DA:361,0 -DA:365,0 -DA:367,0 -DA:368,0 -DA:369,0 -DA:370,0 -DA:371,0 -DA:373,0 -DA:374,0 -DA:375,0 -DA:376,0 -DA:380,0 -DA:381,0 -DA:382,0 -DA:383,0 -DA:386,0 -DA:387,0 -DA:388,0 -DA:392,0 -DA:393,0 -DA:394,0 -DA:395,0 -DA:396,0 -DA:398,0 -DA:400,0 -DA:403,0 -DA:411,0 -DA:412,0 -DA:414,0 -DA:415,0 -DA:419,0 -DA:420,0 -DA:421,0 -DA:422,0 -DA:423,0 -DA:424,0 -DA:425,0 -DA:426,0 -DA:427,0 -DA:430,0 -DA:431,0 -DA:432,0 -DA:436,0 -DA:437,0 -DA:438,0 -DA:439,0 -DA:441,0 -DA:442,0 -DA:443,0 -DA:449,0 -DA:450,0 -DA:453,0 -DA:454,0 -DA:455,0 -DA:456,0 -DA:457,0 -DA:458,0 -DA:461,0 -DA:462,0 -DA:463,0 -DA:471,0 -DA:472,0 -DA:474,0 -DA:475,0 -DA:478,0 -DA:479,0 -DA:480,0 -DA:481,0 -DA:482,0 -DA:483,0 -DA:484,0 -DA:489,0 -DA:490,0 -DA:491,0 -DA:492,0 -DA:493,0 -DA:494,0 -DA:495,0 -DA:496,0 -DA:500,0 -DA:501,0 -LF:362 -LH:0 -BRDA:1,0,0,0 -BRF:1 -BRH:0 -end_of_record -TN: -SF:src/routes/Terminal.svelte -FN:1,(empty-report) -FNF:1 -FNH:0 -FNDA:0,(empty-report) -DA:1,0 -DA:6,0 -DA:7,0 -DA:9,0 -DA:10,0 -DA:11,0 -DA:12,0 -DA:13,0 -DA:14,0 -DA:17,0 -DA:18,0 -DA:19,0 -DA:20,0 -DA:21,0 -DA:22,0 -DA:25,0 -DA:26,0 -DA:27,0 -DA:28,0 -DA:29,0 -DA:30,0 -DA:33,0 -DA:34,0 -DA:35,0 -DA:36,0 -DA:37,0 -DA:38,0 -DA:39,0 -DA:40,0 -DA:41,0 -DA:43,0 -DA:44,0 -DA:45,0 -DA:47,0 -DA:48,0 -DA:49,0 -DA:51,0 -DA:52,0 -DA:53,0 -DA:54,0 -DA:55,0 -DA:56,0 -DA:58,0 -DA:59,0 -DA:60,0 -DA:61,0 -DA:62,0 -DA:64,0 -DA:65,0 -DA:66,0 -DA:67,0 -DA:68,0 -DA:69,0 -DA:71,0 -DA:72,0 -DA:73,0 -DA:74,0 -DA:76,0 -DA:77,0 -DA:78,0 -DA:79,0 -DA:80,0 -DA:81,0 -DA:82,0 -DA:83,0 -DA:84,0 -DA:86,0 -DA:87,0 -DA:88,0 -DA:89,0 -DA:90,0 -DA:91,0 -DA:93,0 -DA:94,0 -DA:95,0 -DA:96,0 -DA:97,0 -DA:98,0 -DA:99,0 -DA:101,0 -DA:102,0 -DA:103,0 -DA:104,0 -DA:105,0 -DA:106,0 -DA:107,0 -DA:108,0 -DA:109,0 -DA:110,0 -DA:112,0 -DA:113,0 -DA:114,0 -DA:116,0 -DA:117,0 -DA:118,0 -DA:119,0 -DA:120,0 -DA:122,0 -DA:123,0 -DA:124,0 -DA:125,0 -DA:126,0 -DA:127,0 -DA:129,0 -DA:130,0 -DA:131,0 -DA:133,0 -DA:134,0 -DA:135,0 -DA:137,0 -DA:138,0 -DA:139,0 -DA:140,0 -DA:141,0 -DA:142,0 -DA:143,0 -DA:144,0 -DA:145,0 -DA:146,0 -DA:147,0 -DA:149,0 -DA:150,0 -DA:151,0 -DA:153,0 -DA:154,0 -DA:155,0 -DA:156,0 -DA:157,0 -DA:158,0 -DA:159,0 -DA:160,0 -DA:161,0 -DA:162,0 -DA:163,0 -DA:164,0 -DA:165,0 -DA:166,0 -DA:167,0 -DA:168,0 -DA:169,0 -DA:170,0 -DA:172,0 -DA:173,0 -DA:174,0 -DA:176,0 -DA:177,0 -DA:178,0 -DA:179,0 -DA:180,0 -DA:181,0 -DA:182,0 -DA:183,0 -DA:184,0 -DA:185,0 -DA:186,0 -DA:188,0 -DA:189,0 -DA:190,0 -DA:191,0 -DA:193,0 -DA:194,0 -DA:195,0 -DA:196,0 -DA:197,0 -DA:198,0 -DA:199,0 -DA:200,0 -DA:201,0 -DA:202,0 -DA:203,0 -DA:204,0 -DA:205,0 -DA:207,0 -DA:208,0 -DA:209,0 -DA:210,0 -DA:211,0 -DA:212,0 -DA:213,0 -DA:214,0 -DA:215,0 -DA:216,0 -DA:218,0 -DA:219,0 -DA:220,0 -DA:221,0 -DA:222,0 -DA:223,0 -DA:224,0 -DA:225,0 -DA:226,0 -DA:227,0 -DA:228,0 -DA:230,0 -DA:231,0 -DA:232,0 -DA:233,0 -DA:234,0 -DA:235,0 -DA:236,0 -DA:237,0 -DA:238,0 -DA:239,0 -DA:241,0 -DA:242,0 -DA:243,0 -DA:244,0 -DA:245,0 -DA:246,0 -DA:247,0 -DA:248,0 -DA:249,0 -DA:250,0 -DA:252,0 -DA:253,0 -DA:254,0 -DA:255,0 -DA:256,0 -DA:257,0 -DA:258,0 -DA:260,0 -DA:261,0 -DA:262,0 -DA:263,0 -DA:264,0 -DA:265,0 -DA:266,0 -DA:267,0 -DA:268,0 -DA:270,0 -DA:271,0 -DA:273,0 -DA:274,0 -DA:275,0 -DA:276,0 -DA:277,0 -DA:278,0 -DA:279,0 -DA:281,0 -DA:282,0 -DA:284,0 -DA:286,0 -DA:287,0 -DA:288,0 -DA:289,0 -DA:290,0 -DA:291,0 -DA:292,0 -DA:294,0 -DA:295,0 -DA:296,0 -DA:297,0 -DA:298,0 -DA:300,0 -DA:302,0 -DA:303,0 -DA:304,0 -DA:305,0 -DA:306,0 -DA:307,0 -DA:308,0 -DA:309,0 -DA:310,0 -DA:311,0 -DA:312,0 -DA:313,0 -DA:314,0 -DA:315,0 -DA:316,0 -DA:317,0 -DA:318,0 -DA:319,0 -DA:321,0 -DA:323,0 -DA:324,0 -DA:325,0 -DA:326,0 -DA:328,0 -DA:330,0 -DA:331,0 -DA:332,0 -DA:333,0 -DA:334,0 -DA:335,0 -DA:336,0 -DA:337,0 -DA:338,0 -DA:339,0 -DA:341,0 -DA:342,0 -DA:343,0 -DA:344,0 -DA:345,0 -DA:346,0 -DA:347,0 -DA:350,0 -DA:351,0 -DA:354,0 -DA:355,0 -DA:357,0 -DA:360,0 -DA:361,0 -DA:362,0 -DA:363,0 -DA:364,0 -DA:365,0 -DA:366,0 -DA:367,0 -DA:368,0 -DA:369,0 -DA:371,0 -DA:372,0 -DA:374,0 -DA:375,0 -DA:376,0 -DA:377,0 -DA:378,0 -DA:379,0 -DA:380,0 -DA:381,0 -DA:383,0 -DA:386,0 -DA:387,0 -DA:388,0 -DA:389,0 -DA:390,0 -DA:391,0 -DA:392,0 -DA:393,0 -DA:394,0 -DA:395,0 -DA:396,0 -DA:397,0 -DA:398,0 -DA:400,0 -DA:401,0 -DA:402,0 -DA:403,0 -DA:404,0 -DA:405,0 -DA:406,0 -DA:407,0 -DA:408,0 -DA:409,0 -DA:411,0 -DA:412,0 -DA:413,0 -DA:414,0 -DA:415,0 -DA:416,0 -DA:417,0 -DA:418,0 -DA:419,0 -DA:421,0 -DA:422,0 -DA:423,0 -DA:425,0 -DA:426,0 -DA:427,0 -DA:428,0 -DA:429,0 -DA:430,0 -DA:431,0 -DA:432,0 -DA:433,0 -DA:435,0 -DA:436,0 -DA:437,0 -DA:438,0 -DA:439,0 -DA:440,0 -DA:441,0 -DA:442,0 -DA:443,0 -DA:444,0 -DA:446,0 -DA:447,0 -DA:448,0 -DA:450,0 -DA:451,0 -DA:452,0 -DA:453,0 -DA:454,0 -DA:455,0 -DA:456,0 -DA:457,0 -DA:458,0 -DA:459,0 -DA:460,0 -DA:462,0 -DA:464,0 -DA:465,0 -DA:466,0 -DA:467,0 -DA:468,0 -DA:469,0 -DA:470,0 -DA:471,0 -DA:472,0 -DA:473,0 -DA:474,0 -DA:475,0 -DA:476,0 -DA:477,0 -DA:478,0 -DA:479,0 -DA:480,0 -DA:481,0 -DA:483,0 -DA:484,0 -DA:485,0 -DA:486,0 -DA:487,0 -DA:488,0 -DA:490,0 -DA:491,0 -DA:492,0 -DA:493,0 -DA:494,0 -DA:497,0 -DA:498,0 -DA:499,0 -DA:500,0 -DA:501,0 -DA:503,0 -DA:504,0 -DA:505,0 -DA:506,0 -DA:507,0 -DA:508,0 -DA:509,0 -DA:510,0 -DA:511,0 -DA:512,0 -DA:513,0 -DA:514,0 -DA:515,0 -DA:516,0 -DA:517,0 -DA:518,0 -DA:519,0 -DA:520,0 -DA:521,0 -DA:525,0 -DA:527,0 -DA:528,0 -DA:529,0 -DA:533,0 -DA:534,0 -DA:535,0 -DA:539,0 -DA:540,0 -DA:541,0 -DA:545,0 -DA:546,0 -DA:547,0 -DA:551,0 -DA:552,0 -DA:553,0 -DA:554,0 -DA:555,0 -DA:557,0 -DA:558,0 -DA:559,0 -DA:560,0 -DA:562,0 -DA:564,0 -DA:565,0 -DA:566,0 -DA:568,0 -DA:573,0 -DA:574,0 -DA:575,0 -DA:577,0 -DA:581,0 -DA:582,0 -DA:586,0 -DA:587,0 -DA:589,0 -DA:591,0 -DA:592,0 -DA:593,0 -DA:594,0 -DA:596,0 -DA:598,0 -DA:599,0 -DA:600,0 -DA:607,0 -DA:608,0 -DA:609,0 -DA:611,0 -DA:612,0 -DA:614,0 -DA:615,0 -DA:617,0 -DA:620,0 -DA:621,0 -DA:622,0 -DA:623,0 -DA:624,0 -DA:625,0 -DA:626,0 -DA:639,0 -DA:640,0 -DA:641,0 -DA:645,0 -DA:646,0 -DA:651,0 -DA:652,0 -DA:653,0 -DA:654,0 -DA:656,0 -DA:661,0 -DA:662,0 -DA:663,0 -DA:665,0 -DA:667,0 -DA:668,0 -DA:670,0 -DA:672,0 -DA:673,0 -DA:676,0 -DA:678,0 -DA:679,0 -DA:680,0 -DA:682,0 -DA:683,0 -DA:684,0 -DA:686,0 -DA:691,0 -DA:692,0 -DA:693,0 -DA:695,0 -DA:696,0 -DA:697,0 -DA:698,0 -DA:702,0 -DA:703,0 -DA:705,0 -DA:706,0 -DA:711,0 -DA:715,0 -DA:716,0 -DA:717,0 -DA:719,0 -LF:545 -LH:0 -BRDA:1,0,0,0 -BRF:1 -BRH:0 -end_of_record -TN: -SF:src/routes/Transmission.svelte -FN:1,(empty-report) -FNF:1 -FNH:0 -FNDA:0,(empty-report) -DA:1,0 -DA:5,0 -DA:6,0 -DA:7,0 -DA:8,0 -DA:9,0 -DA:10,0 -DA:12,0 -DA:13,0 -DA:14,0 -DA:15,0 -DA:16,0 -DA:17,0 -DA:18,0 -DA:21,0 -DA:22,0 -DA:23,0 -DA:24,0 -DA:25,0 -DA:26,0 -DA:27,0 -DA:28,0 -DA:29,0 -DA:30,0 -DA:32,0 -DA:33,0 -DA:34,0 -DA:35,0 -DA:36,0 -DA:37,0 -DA:38,0 -DA:39,0 -DA:41,0 -DA:42,0 -DA:43,0 -DA:44,0 -DA:45,0 -DA:46,0 -DA:47,0 -DA:48,0 -DA:50,0 -DA:51,0 -DA:52,0 -DA:53,0 -DA:54,0 -DA:55,0 -DA:56,0 -DA:57,0 -DA:59,0 -DA:60,0 -DA:61,0 -DA:62,0 -DA:63,0 -DA:64,0 -DA:66,0 -DA:67,0 -DA:68,0 -DA:70,0 -DA:71,0 -DA:72,0 -DA:73,0 -DA:74,0 -DA:75,0 -DA:76,0 -DA:77,0 -DA:78,0 -DA:80,0 -DA:81,0 -DA:83,0 -DA:84,0 -DA:85,0 -DA:86,0 -DA:88,0 -DA:94,0 -DA:95,0 -DA:98,0 -DA:100,0 -DA:101,0 -DA:102,0 -DA:104,0 -DA:105,0 -DA:108,0 -DA:109,0 -DA:117,0 -DA:118,0 -DA:119,0 -DA:121,0 -DA:122,0 -DA:134,0 -DA:135,0 -DA:136,0 -DA:138,0 -DA:139,0 -DA:151,0 -DA:152,0 -DA:153,0 -DA:155,0 -DA:156,0 -DA:169,0 -DA:170,0 -DA:171,0 -DA:172,0 -DA:173,0 -DA:175,0 -DA:177,0 -DA:178,0 -DA:179,0 -DA:181,0 -DA:187,0 -DA:188,0 -DA:189,0 -DA:190,0 -DA:203,0 -DA:204,0 -DA:205,0 -DA:206,0 -DA:207,0 -DA:208,0 -DA:209,0 -DA:212,0 -DA:213,0 -DA:214,0 -DA:215,0 -DA:216,0 -DA:217,0 -DA:218,0 -DA:225,0 -DA:228,0 -DA:229,0 -DA:230,0 -DA:243,0 -DA:244,0 -DA:245,0 -DA:246,0 -DA:256,0 -DA:257,0 -DA:258,0 -DA:259,0 -DA:260,0 -DA:261,0 -DA:262,0 -DA:263,0 -DA:264,0 -DA:268,0 -DA:273,0 -LF:145 -LH:0 -BRDA:1,0,0,0 -BRF:1 -BRH:0 -end_of_record diff --git a/dashboard/frontend/coverage/prettify.css b/dashboard/frontend/coverage/prettify.css deleted file mode 100644 index b317a7c..0000000 --- a/dashboard/frontend/coverage/prettify.css +++ /dev/null @@ -1 +0,0 @@ -.pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee} diff --git a/dashboard/frontend/coverage/prettify.js b/dashboard/frontend/coverage/prettify.js deleted file mode 100644 index b322523..0000000 --- a/dashboard/frontend/coverage/prettify.js +++ /dev/null @@ -1,2 +0,0 @@ -/* eslint-disable */ -window.PR_SHOULD_USE_CONTINUATION=true;(function(){var h=["break,continue,do,else,for,if,return,while"];var u=[h,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"];var p=[u,"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"];var l=[p,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"];var x=[p,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"];var R=[x,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"];var r="all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes";var w=[p,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"];var s="caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END";var I=[h,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"];var f=[h,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"];var H=[h,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"];var A=[l,R,w,s+I,f,H];var e=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/;var C="str";var z="kwd";var j="com";var O="typ";var G="lit";var L="pun";var F="pln";var m="tag";var E="dec";var J="src";var P="atn";var n="atv";var N="nocode";var M="(?:^^\\.?|[+-]|\\!|\\!=|\\!==|\\#|\\%|\\%=|&|&&|&&=|&=|\\(|\\*|\\*=|\\+=|\\,|\\-=|\\->|\\/|\\/=|:|::|\\;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|\\?|\\@|\\[|\\^|\\^=|\\^\\^|\\^\\^=|\\{|\\||\\|=|\\|\\||\\|\\|=|\\~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\\s*";function k(Z){var ad=0;var S=false;var ac=false;for(var V=0,U=Z.length;V122)){if(!(al<65||ag>90)){af.push([Math.max(65,ag)|32,Math.min(al,90)|32])}if(!(al<97||ag>122)){af.push([Math.max(97,ag)&~32,Math.min(al,122)&~32])}}}}af.sort(function(av,au){return(av[0]-au[0])||(au[1]-av[1])});var ai=[];var ap=[NaN,NaN];for(var ar=0;arat[0]){if(at[1]+1>at[0]){an.push("-")}an.push(T(at[1]))}}an.push("]");return an.join("")}function W(al){var aj=al.source.match(new RegExp("(?:\\[(?:[^\\x5C\\x5D]|\\\\[\\s\\S])*\\]|\\\\u[A-Fa-f0-9]{4}|\\\\x[A-Fa-f0-9]{2}|\\\\[0-9]+|\\\\[^ux0-9]|\\(\\?[:!=]|[\\(\\)\\^]|[^\\x5B\\x5C\\(\\)\\^]+)","g"));var ah=aj.length;var an=[];for(var ak=0,am=0;ak=2&&ai==="["){aj[ak]=X(ag)}else{if(ai!=="\\"){aj[ak]=ag.replace(/[a-zA-Z]/g,function(ao){var ap=ao.charCodeAt(0);return"["+String.fromCharCode(ap&~32,ap|32)+"]"})}}}}return aj.join("")}var aa=[];for(var V=0,U=Z.length;V=0;){S[ac.charAt(ae)]=Y}}var af=Y[1];var aa=""+af;if(!ag.hasOwnProperty(aa)){ah.push(af);ag[aa]=null}}ah.push(/[\0-\uffff]/);V=k(ah)})();var X=T.length;var W=function(ah){var Z=ah.sourceCode,Y=ah.basePos;var ad=[Y,F];var af=0;var an=Z.match(V)||[];var aj={};for(var ae=0,aq=an.length;ae=5&&"lang-"===ap.substring(0,5);if(am&&!(ai&&typeof ai[1]==="string")){am=false;ap=J}if(!am){aj[ag]=ap}}var ab=af;af+=ag.length;if(!am){ad.push(Y+ab,ap)}else{var al=ai[1];var ak=ag.indexOf(al);var ac=ak+al.length;if(ai[2]){ac=ag.length-ai[2].length;ak=ac-al.length}var ar=ap.substring(5);B(Y+ab,ag.substring(0,ak),W,ad);B(Y+ab+ak,al,q(ar,al),ad);B(Y+ab+ac,ag.substring(ac),W,ad)}}ah.decorations=ad};return W}function i(T){var W=[],S=[];if(T.tripleQuotedStrings){W.push([C,/^(?:\'\'\'(?:[^\'\\]|\\[\s\S]|\'{1,2}(?=[^\']))*(?:\'\'\'|$)|\"\"\"(?:[^\"\\]|\\[\s\S]|\"{1,2}(?=[^\"]))*(?:\"\"\"|$)|\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$))/,null,"'\""])}else{if(T.multiLineStrings){W.push([C,/^(?:\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$)|\`(?:[^\\\`]|\\[\s\S])*(?:\`|$))/,null,"'\"`"])}else{W.push([C,/^(?:\'(?:[^\\\'\r\n]|\\.)*(?:\'|$)|\"(?:[^\\\"\r\n]|\\.)*(?:\"|$))/,null,"\"'"])}}if(T.verbatimStrings){S.push([C,/^@\"(?:[^\"]|\"\")*(?:\"|$)/,null])}var Y=T.hashComments;if(Y){if(T.cStyleComments){if(Y>1){W.push([j,/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,null,"#"])}else{W.push([j,/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\r\n]*)/,null,"#"])}S.push([C,/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,null])}else{W.push([j,/^#[^\r\n]*/,null,"#"])}}if(T.cStyleComments){S.push([j,/^\/\/[^\r\n]*/,null]);S.push([j,/^\/\*[\s\S]*?(?:\*\/|$)/,null])}if(T.regexLiterals){var X=("/(?=[^/*])(?:[^/\\x5B\\x5C]|\\x5C[\\s\\S]|\\x5B(?:[^\\x5C\\x5D]|\\x5C[\\s\\S])*(?:\\x5D|$))+/");S.push(["lang-regex",new RegExp("^"+M+"("+X+")")])}var V=T.types;if(V){S.push([O,V])}var U=(""+T.keywords).replace(/^ | $/g,"");if(U.length){S.push([z,new RegExp("^(?:"+U.replace(/[\s,]+/g,"|")+")\\b"),null])}W.push([F,/^\s+/,null," \r\n\t\xA0"]);S.push([G,/^@[a-z_$][a-z_$@0-9]*/i,null],[O,/^(?:[@_]?[A-Z]+[a-z][A-Za-z_$@0-9]*|\w+_t\b)/,null],[F,/^[a-z_$][a-z_$@0-9]*/i,null],[G,new RegExp("^(?:0x[a-f0-9]+|(?:\\d(?:_\\d+)*\\d*(?:\\.\\d*)?|\\.\\d\\+)(?:e[+\\-]?\\d+)?)[a-z]*","i"),null,"0123456789"],[F,/^\\[\s\S]?/,null],[L,/^.[^\s\w\.$@\'\"\`\/\#\\]*/,null]);return g(W,S)}var K=i({keywords:A,hashComments:true,cStyleComments:true,multiLineStrings:true,regexLiterals:true});function Q(V,ag){var U=/(?:^|\s)nocode(?:\s|$)/;var ab=/\r\n?|\n/;var ac=V.ownerDocument;var S;if(V.currentStyle){S=V.currentStyle.whiteSpace}else{if(window.getComputedStyle){S=ac.defaultView.getComputedStyle(V,null).getPropertyValue("white-space")}}var Z=S&&"pre"===S.substring(0,3);var af=ac.createElement("LI");while(V.firstChild){af.appendChild(V.firstChild)}var W=[af];function ae(al){switch(al.nodeType){case 1:if(U.test(al.className)){break}if("BR"===al.nodeName){ad(al);if(al.parentNode){al.parentNode.removeChild(al)}}else{for(var an=al.firstChild;an;an=an.nextSibling){ae(an)}}break;case 3:case 4:if(Z){var am=al.nodeValue;var aj=am.match(ab);if(aj){var ai=am.substring(0,aj.index);al.nodeValue=ai;var ah=am.substring(aj.index+aj[0].length);if(ah){var ak=al.parentNode;ak.insertBefore(ac.createTextNode(ah),al.nextSibling)}ad(al);if(!ai){al.parentNode.removeChild(al)}}}break}}function ad(ak){while(!ak.nextSibling){ak=ak.parentNode;if(!ak){return}}function ai(al,ar){var aq=ar?al.cloneNode(false):al;var ao=al.parentNode;if(ao){var ap=ai(ao,1);var an=al.nextSibling;ap.appendChild(aq);for(var am=an;am;am=an){an=am.nextSibling;ap.appendChild(am)}}return aq}var ah=ai(ak.nextSibling,0);for(var aj;(aj=ah.parentNode)&&aj.nodeType===1;){ah=aj}W.push(ah)}for(var Y=0;Y=S){ah+=2}if(V>=ap){Z+=2}}}var t={};function c(U,V){for(var S=V.length;--S>=0;){var T=V[S];if(!t.hasOwnProperty(T)){t[T]=U}else{if(window.console){console.warn("cannot override language handler %s",T)}}}}function q(T,S){if(!(T&&t.hasOwnProperty(T))){T=/^\s*]*(?:>|$)/],[j,/^<\!--[\s\S]*?(?:-\->|$)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],[L,/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]),["default-markup","htm","html","mxml","xhtml","xml","xsl"]);c(g([[F,/^[\s]+/,null," \t\r\n"],[n,/^(?:\"[^\"]*\"?|\'[^\']*\'?)/,null,"\"'"]],[[m,/^^<\/?[a-z](?:[\w.:-]*\w)?|\/?>$/i],[P,/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^>\'\"\s]*(?:[^>\'\"\s\/]|\/(?=\s)))/],[L,/^[=<>\/]+/],["lang-js",/^on\w+\s*=\s*\"([^\"]+)\"/i],["lang-js",/^on\w+\s*=\s*\'([^\']+)\'/i],["lang-js",/^on\w+\s*=\s*([^\"\'>\s]+)/i],["lang-css",/^style\s*=\s*\"([^\"]+)\"/i],["lang-css",/^style\s*=\s*\'([^\']+)\'/i],["lang-css",/^style\s*=\s*([^\"\'>\s]+)/i]]),["in.tag"]);c(g([],[[n,/^[\s\S]+/]]),["uq.val"]);c(i({keywords:l,hashComments:true,cStyleComments:true,types:e}),["c","cc","cpp","cxx","cyc","m"]);c(i({keywords:"null,true,false"}),["json"]);c(i({keywords:R,hashComments:true,cStyleComments:true,verbatimStrings:true,types:e}),["cs"]);c(i({keywords:x,cStyleComments:true}),["java"]);c(i({keywords:H,hashComments:true,multiLineStrings:true}),["bsh","csh","sh"]);c(i({keywords:I,hashComments:true,multiLineStrings:true,tripleQuotedStrings:true}),["cv","py"]);c(i({keywords:s,hashComments:true,multiLineStrings:true,regexLiterals:true}),["perl","pl","pm"]);c(i({keywords:f,hashComments:true,multiLineStrings:true,regexLiterals:true}),["rb"]);c(i({keywords:w,cStyleComments:true,regexLiterals:true}),["js"]);c(i({keywords:r,hashComments:3,cStyleComments:true,multilineStrings:true,tripleQuotedStrings:true,regexLiterals:true}),["coffee"]);c(g([],[[C,/^[\s\S]+/]]),["regex"]);function d(V){var U=V.langExtension;try{var S=a(V.sourceNode);var T=S.sourceCode;V.sourceCode=T;V.spans=S.spans;V.basePos=0;q(U,T)(V);D(V)}catch(W){if("console" in window){console.log(W&&W.stack?W.stack:W)}}}function y(W,V,U){var S=document.createElement("PRE");S.innerHTML=W;if(U){Q(S,U)}var T={langExtension:V,numberLines:U,sourceNode:S};d(T);return S.innerHTML}function b(ad){function Y(af){return document.getElementsByTagName(af)}var ac=[Y("pre"),Y("code"),Y("xmp")];var T=[];for(var aa=0;aa=0){var ah=ai.match(ab);var am;if(!ah&&(am=o(aj))&&"CODE"===am.tagName){ah=am.className.match(ab)}if(ah){ah=ah[1]}var al=false;for(var ak=aj.parentNode;ak;ak=ak.parentNode){if((ak.tagName==="pre"||ak.tagName==="code"||ak.tagName==="xmp")&&ak.className&&ak.className.indexOf("prettyprint")>=0){al=true;break}}if(!al){var af=aj.className.match(/\blinenums\b(?::(\d+))?/);af=af?af[1]&&af[1].length?+af[1]:true:false;if(af){Q(aj,af)}S={langExtension:ah,sourceNode:aj,numberLines:af};d(S)}}}if(X]*(?:>|$)/],[PR.PR_COMMENT,/^<\!--[\s\S]*?(?:-\->|$)/],[PR.PR_PUNCTUATION,/^(?:<[%?]|[%?]>)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-handlebars",/^]*type\s*=\s*['"]?text\/x-handlebars-template['"]?\b[^>]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i],[PR.PR_DECLARATION,/^{{[#^>/]?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{&?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{{>?\s*[\w.][^}]*}}}/],[PR.PR_COMMENT,/^{{![^}]*}}/]]),["handlebars","hbs"]);PR.registerLangHandler(PR.createSimpleLexer([[PR.PR_PLAIN,/^[ \t\r\n\f]+/,null," \t\r\n\f"]],[[PR.PR_STRING,/^\"(?:[^\n\r\f\\\"]|\\(?:\r\n?|\n|\f)|\\[\s\S])*\"/,null],[PR.PR_STRING,/^\'(?:[^\n\r\f\\\']|\\(?:\r\n?|\n|\f)|\\[\s\S])*\'/,null],["lang-css-str",/^url\(([^\)\"\']*)\)/i],[PR.PR_KEYWORD,/^(?:url|rgb|\!important|@import|@page|@media|@charset|inherit)(?=[^\-\w]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|(?:\\[0-9a-f]+ ?))(?:[_a-z0-9\-]|\\(?:\\[0-9a-f]+ ?))*)\s*:/i],[PR.PR_COMMENT,/^\/\*[^*]*\*+(?:[^\/*][^*]*\*+)*\//],[PR.PR_COMMENT,/^(?:)/],[PR.PR_LITERAL,/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],[PR.PR_LITERAL,/^#(?:[0-9a-f]{3}){1,2}/i],[PR.PR_PLAIN,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i],[PR.PR_PUNCTUATION,/^[^\s\w\'\"]+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_KEYWORD,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_STRING,/^[^\)\"\']+/]]),["css-str"]); diff --git a/dashboard/frontend/coverage/sort-arrow-sprite.png b/dashboard/frontend/coverage/sort-arrow-sprite.png deleted file mode 100644 index 6ed6831..0000000 Binary files a/dashboard/frontend/coverage/sort-arrow-sprite.png and /dev/null differ diff --git a/dashboard/frontend/coverage/sorter.js b/dashboard/frontend/coverage/sorter.js deleted file mode 100644 index 4ed70ae..0000000 --- a/dashboard/frontend/coverage/sorter.js +++ /dev/null @@ -1,210 +0,0 @@ -/* eslint-disable */ -var addSorting = (function() { - 'use strict'; - var cols, - currentSort = { - index: 0, - desc: false - }; - - // returns the summary table element - function getTable() { - return document.querySelector('.coverage-summary'); - } - // returns the thead element of the summary table - function getTableHeader() { - return getTable().querySelector('thead tr'); - } - // returns the tbody element of the summary table - function getTableBody() { - return getTable().querySelector('tbody'); - } - // returns the th element for nth column - function getNthColumn(n) { - return getTableHeader().querySelectorAll('th')[n]; - } - - function onFilterInput() { - const searchValue = document.getElementById('fileSearch').value; - const rows = document.getElementsByTagName('tbody')[0].children; - - // Try to create a RegExp from the searchValue. If it fails (invalid regex), - // it will be treated as a plain text search - let searchRegex; - try { - searchRegex = new RegExp(searchValue, 'i'); // 'i' for case-insensitive - } catch (error) { - searchRegex = null; - } - - for (let i = 0; i < rows.length; i++) { - const row = rows[i]; - let isMatch = false; - - if (searchRegex) { - // If a valid regex was created, use it for matching - isMatch = searchRegex.test(row.textContent); - } else { - // Otherwise, fall back to the original plain text search - isMatch = row.textContent - .toLowerCase() - .includes(searchValue.toLowerCase()); - } - - row.style.display = isMatch ? '' : 'none'; - } - } - - // loads the search box - function addSearchBox() { - var template = document.getElementById('filterTemplate'); - var templateClone = template.content.cloneNode(true); - templateClone.getElementById('fileSearch').oninput = onFilterInput; - template.parentElement.appendChild(templateClone); - } - - // loads all columns - function loadColumns() { - var colNodes = getTableHeader().querySelectorAll('th'), - colNode, - cols = [], - col, - i; - - for (i = 0; i < colNodes.length; i += 1) { - colNode = colNodes[i]; - col = { - key: colNode.getAttribute('data-col'), - sortable: !colNode.getAttribute('data-nosort'), - type: colNode.getAttribute('data-type') || 'string' - }; - cols.push(col); - if (col.sortable) { - col.defaultDescSort = col.type === 'number'; - colNode.innerHTML = - colNode.innerHTML + ''; - } - } - return cols; - } - // attaches a data attribute to every tr element with an object - // of data values keyed by column name - function loadRowData(tableRow) { - var tableCols = tableRow.querySelectorAll('td'), - colNode, - col, - data = {}, - i, - val; - for (i = 0; i < tableCols.length; i += 1) { - colNode = tableCols[i]; - col = cols[i]; - val = colNode.getAttribute('data-value'); - if (col.type === 'number') { - val = Number(val); - } - data[col.key] = val; - } - return data; - } - // loads all row data - function loadData() { - var rows = getTableBody().querySelectorAll('tr'), - i; - - for (i = 0; i < rows.length; i += 1) { - rows[i].data = loadRowData(rows[i]); - } - } - // sorts the table using the data for the ith column - function sortByIndex(index, desc) { - var key = cols[index].key, - sorter = function(a, b) { - a = a.data[key]; - b = b.data[key]; - return a < b ? -1 : a > b ? 1 : 0; - }, - finalSorter = sorter, - tableBody = document.querySelector('.coverage-summary tbody'), - rowNodes = tableBody.querySelectorAll('tr'), - rows = [], - i; - - if (desc) { - finalSorter = function(a, b) { - return -1 * sorter(a, b); - }; - } - - for (i = 0; i < rowNodes.length; i += 1) { - rows.push(rowNodes[i]); - tableBody.removeChild(rowNodes[i]); - } - - rows.sort(finalSorter); - - for (i = 0; i < rows.length; i += 1) { - tableBody.appendChild(rows[i]); - } - } - // removes sort indicators for current column being sorted - function removeSortIndicators() { - var col = getNthColumn(currentSort.index), - cls = col.className; - - cls = cls.replace(/ sorted$/, '').replace(/ sorted-desc$/, ''); - col.className = cls; - } - // adds sort indicators for current column being sorted - function addSortIndicators() { - getNthColumn(currentSort.index).className += currentSort.desc - ? ' sorted-desc' - : ' sorted'; - } - // adds event listeners for all sorter widgets - function enableUI() { - var i, - el, - ithSorter = function ithSorter(i) { - var col = cols[i]; - - return function() { - var desc = col.defaultDescSort; - - if (currentSort.index === i) { - desc = !currentSort.desc; - } - sortByIndex(i, desc); - removeSortIndicators(); - currentSort.index = i; - currentSort.desc = desc; - addSortIndicators(); - }; - }; - for (i = 0; i < cols.length; i += 1) { - if (cols[i].sortable) { - // add the click event handler on the th so users - // dont have to click on those tiny arrows - el = getNthColumn(i).querySelector('.sorter').parentElement; - if (el.addEventListener) { - el.addEventListener('click', ithSorter(i)); - } else { - el.attachEvent('onclick', ithSorter(i)); - } - } - } - } - // adds sorting functionality to the UI - return function() { - if (!getTable()) { - return; - } - cols = loadColumns(); - loadData(); - addSearchBox(); - addSortIndicators(); - enableUI(); - }; -})(); - -window.addEventListener('load', addSorting); diff --git a/dashboard/frontend/coverage/src/App.svelte.html b/dashboard/frontend/coverage/src/App.svelte.html deleted file mode 100644 index f7f391b..0000000 --- a/dashboard/frontend/coverage/src/App.svelte.html +++ /dev/null @@ -1,787 +0,0 @@ - - - - - - Code coverage report for src/App.svelte - - - - - - - - - -
-
-

All files / src App.svelte

-
- -
- 0% - Statements - 0/177 -
- - -
- 0% - Branches - 0/1 -
- - -
- 0% - Functions - 0/1 -
- - -
- 0% - Lines - 0/177 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -178 -179 -180 -181 -182 -183 -184 -185 -186 -187 -188 -189 -190 -191 -192 -193 -194 -195 -196 -197 -198 -199 -200 -201 -202 -203 -204 -205 -206 -207 -208 -209 -210 -211 -212 -213 -214 -215 -216 -217 -218 -219 -220 -221 -222 -223 -224 -225 -226 -227 -228 -229 -230 -231 -232 -233 -234 -235  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  - 
<script>
-  import Sidebar from "./components/Sidebar.svelte";
-  import Dashboard from "./routes/Dashboard.svelte";
-  import Docker from "./routes/Docker.svelte";
-  import Gitea from "./routes/Gitea.svelte";
-  import Files from "./routes/Files.svelte";
-  import Terminal from "./routes/Terminal.svelte";
-  import OpenClaw from "./routes/OpenClaw.svelte";
-  import Settings from "./routes/Settings.svelte";
-  import ChatSummary from "./routes/ChatSummary.svelte";
-  import Conversations from "./routes/Conversations.svelte";
-  import Security from "./routes/Security.svelte";
-  import LiteLLM from "./routes/LiteLLM.svelte";
-  import CcConnect from "./routes/CcConnect.svelte";
-  import InfoEngine from "./routes/InfoEngine.svelte";
-  import OPC from "./routes/OPC.svelte";
-  import Transmission from "./routes/Transmission.svelte";
-  import Login from "./routes/Login.svelte";
-  import { onMount } from "svelte";
-  import { getToken, checkAuth, setToken, currentUser, setCurrentUser, getPreferences, savePreferences, tryRefreshSession, logout as logoutSession } from "./lib/api.js";
- 
-  const KNOWN_PAGES = new Set([
-    "dashboard",
-    "docker",
-    "gitea",
-    "files",
-    "terminal",
-    "openclaw",
-    "chat-digest",
-    "conversations",
-    "settings",
-    "security",
-    "litellm",
-    "cc-connect",
-    "info-engine",
-    "opc",
-    "transmission",
-  ]);
- 
-  let page = $state("dashboard");
-  let dark = $state(false);
-  let sidebarOpen = $state(false);
-  let authorized = $state(false);
-  let loading = $state(true);
-  let userRole = $state("");
-  let allowedPages = $state([]);
-  let allowedSidebarLinks = $state("*");
-  let sidebarOrder = $state(null);
-  let orderSaveTimer = null;
- 
-  function getRequestedPage() {
-    const requestedPage = new URLSearchParams(window.location.search).get("page");
-    return KNOWN_PAGES.has(requestedPage) ? requestedPage : null;
-  }
- 
-  function canOpenPage(pageId) {
-    if (!pageId || !KNOWN_PAGES.has(pageId)) return false;
-    if (pageId === "settings") return userRole === "admin";
-    if (["litellm", "cc-connect", "info-engine"].includes(pageId)) return hasPageAccess("dashboard");
-    return hasPageAccess(pageId);
-  }
- 
-  function syncPageQuery(pageId) {
-    const url = new URL(window.location.href);
-    if (pageId === "dashboard") url.searchParams.delete("page");
-    else url.searchParams.set("page", pageId);
-    if (pageId !== "terminal") url.searchParams.delete("host");
-    history.replaceState({}, "", `${url.pathname}${url.search}${url.hash}`);
-  }
- 
-  async function fetchUserInfo() {
-    try {
-      const data = await checkAuth();
-      setCurrentUser(data);
-      userRole = data.role || "admin";
-      allowedPages = data.pages || "*";
-      allowedSidebarLinks = data.sidebar_links || "*";
-      // Fetch sidebar preferences
-      try {
-        const prefs = await getPreferences();
-        sidebarOrder = prefs.sidebar_order || null;
-      } catch {}
-    } catch (e) {
-      console.error("Failed to fetch user info:", e);
-    }
-  }
- 
-  function handleOrderChange(newOrder) {
-    sidebarOrder = newOrder;
-    clearTimeout(orderSaveTimer);
-    orderSaveTimer = setTimeout(() => {
-      savePreferences({ sidebar_order: newOrder }).catch((e) => {
-        console.error("Failed to save sidebar order:", e);
-      });
-    }, 500);
-  }
- 
-  function hasPageAccess(pageId) {
-    if (allowedPages === "*") return true;
-    return Array.isArray(allowedPages) && allowedPages.includes(pageId);
-  }
- 
-  onMount(async () => {
-    // Theme init
-    if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
-      dark = true;
-      document.documentElement.classList.add('dark');
-    } else {
-      dark = false;
-      document.documentElement.classList.remove('dark');
-    }
- 
-    // Try proxy auth first (Authelia forward-auth sets Remote-User header)
-    try {
-      const proxyRes = await fetch("/api/auth/proxy", { credentials: "same-origin" });
-      if (proxyRes.ok) {
-        const data = await proxyRes.json();
-        setToken(data.access_token);
-        authorized = true;
-        await fetchUserInfo();
-        const requestedPage = getRequestedPage();
-        if (canOpenPage(requestedPage)) page = requestedPage;
-        loading = false;
-        return;
-      }
-    } catch (e) {
-      // Proxy auth not available, fall through to regular auth
-    }
- 
-    // Regular token auth check — always try refresh to get fresh cookies
-    const refreshed = await tryRefreshSession();
-    if (!refreshed && !getToken()) {
-      loading = false;
-      authorized = false;
-      return;
-    }
- 
-    try {
-        await fetchUserInfo();
-        authorized = true;
-        const requestedPage = getRequestedPage();
-        if (canOpenPage(requestedPage)) page = requestedPage;
-    } catch (e) {
-        console.error("Auth check failed:", e);
-        setToken("");
-        authorized = false;
-    } finally {
-        loading = false;
-    }
-  });
- 
-  $effect(() => {
-    if (!authorized || loading || !canOpenPage(page)) return;
-    syncPageQuery(page);
-  });
- 
-  function toggleTheme() {
-    dark = !dark;
-    if (dark) {
-       document.documentElement.classList.add("dark");
-       localStorage.theme = 'dark';
-    } else {
-       document.documentElement.classList.remove("dark");
-       localStorage.theme = 'light';
-    }
-  }
- 
-  async function logout() {
-      try {
-        await logoutSession();
-      } catch (e) {
-        console.error("Logout failed:", e);
-      }
-      setToken("");
-      window.location.reload();
-  }
-</script>
- 
-{#if loading}
-  <div class="flex h-screen items-center justify-center bg-surface-50 dark:bg-surface-950">
-    <div class="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600"></div>
-  </div>
-{:else if !authorized}
-  <Login />
-{:else}
-  <div class="flex h-screen overflow-hidden bg-surface-50 dark:bg-surface-900">
-    <Sidebar bind:page {dark} {toggleTheme} {logout} bind:sidebarOpen {allowedPages} {allowedSidebarLinks} {userRole} {sidebarOrder} onOrderChange={handleOrderChange} />
-    <main class="flex-1 overflow-auto">
-      <div class="max-w-[1400px] mx-auto p-6 lg:p-8">
-        <!-- Mobile hamburger -->
-        <button class="md:hidden mb-4 p-2 rounded-lg text-surface-600 dark:text-surface-300 hover:bg-surface-200 dark:hover:bg-surface-700" onclick={() => sidebarOpen = true} aria-label="Open menu">
-          <svg class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M4 6h16M4 12h16M4 18h16" /></svg>
-        </button>
-        {#if page === "dashboard" && hasPageAccess("dashboard")}
-          <Dashboard />
-        {:else if page === "docker" && hasPageAccess("docker")}
-          <Docker />
-        {:else if page === "gitea" && hasPageAccess("gitea")}
-          <Gitea />
-        {:else if page === "files" && hasPageAccess("files")}
-          <Files />
-        {:else if page === "openclaw" && hasPageAccess("openclaw")}
-          <OpenClaw />
-        {:else if page === "chat-digest" && hasPageAccess("chat-digest")}
-          <ChatSummary />
-        {:else if page === "conversations" && hasPageAccess("conversations")}
-          <Conversations />
-        {:else if page === "settings" && userRole === "admin"}
-          <Settings />
-        {:else if page === "security" && hasPageAccess("security")}
-          <Security />
-        {:else if page === "litellm" && hasPageAccess("dashboard")}
-          <LiteLLM />
-        {:else if page === "cc-connect" && hasPageAccess("dashboard")}
-          <CcConnect />
-        {:else if page === "info-engine" && hasPageAccess("dashboard")}
-          <InfoEngine />
-        {:else if page === "opc" && hasPageAccess("opc")}
-          <OPC />
-        {:else if page === "transmission" && hasPageAccess("transmission")}
-          <Transmission />
-        {:else if page !== "terminal"}
-          <Dashboard />
-        {/if}
- 
-        {#if hasPageAccess("terminal")}
-          <div class:hidden={page !== "terminal"}>
-            <Terminal />
-          </div>
-        {/if}
-      </div>
-    </main>
-  </div>
-{/if}
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/dashboard/frontend/coverage/src/components/Sidebar.svelte.html b/dashboard/frontend/coverage/src/components/Sidebar.svelte.html deleted file mode 100644 index 3faebe3..0000000 --- a/dashboard/frontend/coverage/src/components/Sidebar.svelte.html +++ /dev/null @@ -1,1339 +0,0 @@ - - - - - - Code coverage report for src/components/Sidebar.svelte - - - - - - - - - -
-
-

All files / src/components Sidebar.svelte

-
- -
- 0% - Statements - 0/301 -
- - -
- 0% - Branches - 0/1 -
- - -
- 0% - Functions - 0/1 -
- - -
- 0% - Lines - 0/301 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -178 -179 -180 -181 -182 -183 -184 -185 -186 -187 -188 -189 -190 -191 -192 -193 -194 -195 -196 -197 -198 -199 -200 -201 -202 -203 -204 -205 -206 -207 -208 -209 -210 -211 -212 -213 -214 -215 -216 -217 -218 -219 -220 -221 -222 -223 -224 -225 -226 -227 -228 -229 -230 -231 -232 -233 -234 -235 -236 -237 -238 -239 -240 -241 -242 -243 -244 -245 -246 -247 -248 -249 -250 -251 -252 -253 -254 -255 -256 -257 -258 -259 -260 -261 -262 -263 -264 -265 -266 -267 -268 -269 -270 -271 -272 -273 -274 -275 -276 -277 -278 -279 -280 -281 -282 -283 -284 -285 -286 -287 -288 -289 -290 -291 -292 -293 -294 -295 -296 -297 -298 -299 -300 -301 -302 -303 -304 -305 -306 -307 -308 -309 -310 -311 -312 -313 -314 -315 -316 -317 -318 -319 -320 -321 -322 -323 -324 -325 -326 -327 -328 -329 -330 -331 -332 -333 -334 -335 -336 -337 -338 -339 -340 -341 -342 -343 -344 -345 -346 -347 -348 -349 -350 -351 -352 -353 -354 -355 -356 -357 -358 -359 -360 -361 -362 -363 -364 -365 -366 -367 -368 -369 -370 -371 -372 -373 -374 -375 -376 -377 -378 -379 -380 -381 -382 -383 -384 -385 -386 -387 -388 -389 -390 -391 -392 -393 -394 -395 -396 -397 -398 -399 -400 -401 -402 -403 -404 -405 -406 -407 -408 -409 -410 -411 -412 -413 -414 -415 -416 -417 -418 -419  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  - 
<script>
-  let { page = $bindable("dashboard"), dark = false, toggleTheme = () => {}, logout = () => {}, sidebarOpen = $bindable(false), allowedPages = "*", allowedSidebarLinks = "*", userRole = "admin", sidebarOrder = null, onOrderChange = () => {} } = $props();
- 
-  function canAccess(id) {
-    if (!id) return true;
-    if (allowedPages === "*") return true;
-    if (!Array.isArray(allowedPages)) return false;
-    if (id === "info-engine") return allowedPages.includes("dashboard");
-    return allowedPages.includes(id);
-  }
- 
-  function canSeeSidebarLink(item) {
-    if (allowedSidebarLinks === "*") return true;
-    // Check by label or id
-    const key = item.label || item.id;
-    return Array.isArray(allowedSidebarLinks) && allowedSidebarLinks.includes(key);
-  }
- 
-  const defaultLinks = [
-    { id: "dashboard", label: "Overview", icon: "grid" },
-    { id: "info-engine", label: "Info Engine", icon: "sparkles" },
-    { id: "litellm", label: "LiteLLM", icon: "bolt" },
-    { id: "opc", label: "OPC", icon: "kanban" },
-    { id: "docker", label: "Docker", icon: "box" },
-    { id: "files", label: "Files", icon: "folder" },
-    { id: "terminal", label: "Terminal", icon: "terminal" },
-    { id: "security", label: "Security", icon: "shield" },
-  ];
- 
-  let lanIp = $state("192.168.31.222");
-  let tsIp = $state("100.78.131.124");
-  let lanReachable = $state(false);
-  let serviceUrls = $state({});
- 
-  if (typeof window !== "undefined") {
-    fetch("/api/client-ip").then(r => r.json()).then(d => {
-      if (d.lan) lanReachable = true;
-    }).catch(() => {});
- 
-    // Fetch external service URLs from backend config
-    fetch("/api/system/external-services").then(r => r.json()).then(d => {
-      if (d.lan_ip) lanIp = d.lan_ip;
-      if (d.ts_ip) tsIp = d.ts_ip;
-      if (d.services) serviceUrls = d.services;
-    }).catch(() => {});
-  }
- 
-  const defaultMedia = [
-    { label: "Navidrome", remoteHref: "https://music.jimmygan.com:8443", icon: "music" },
-    { label: "Jellyfin", remoteHref: "https://media.jimmygan.com:8443", icon: "play" },
-    { label: "Audiobookshelf", remoteHref: "https://books.jimmygan.com:8443", icon: "headphones" },
-    { label: "Immich", remoteHref: "https://photos.jimmygan.com:8443", icon: "image" },
-    { id: "transmission", label: "Transmission", icon: "download" },
-  ];
- 
-  const defaultTools = [
-    { id: "openclaw", label: "OpenClaw", icon: "openclaw" },
-    { id: "cc-connect", label: "cc-connect", icon: "users" },
-    { id: "chat-digest", label: "Chat Digest", icon: "chat" },
-    { id: "conversations", label: "Code Sessions", icon: "code" },
-    { id: "gitea", label: "Repos", icon: "git" },
-    { label: "Gitea Web", remoteHref: "https://git.jimmygan.com:8443", icon: "git", external: true },
-    { label: "Stirling PDF", remoteHref: "https://pdf.jimmygan.com:8443", icon: "pdf", external: true },
-    { label: "Vaultwarden", remoteHref: "https://vault.jimmygan.com:8443", icon: "lock", external: true },
-    { label: "n8n", remoteHref: "https://n8n.jimmygan.com:8443", icon: "n8n", external: true },
-    { label: "Speedtest", remoteHref: "https://speed.jimmygan.com:8443", icon: "speedtest", external: true },
-  ];
- 
-  const defaultTools2 = [];
- 
-  function itemKey(item) { return item.id || item.label; }
- 
-  // All items indexed by key for cross-category lookup
-  const allItems = new Map([...defaultLinks, ...defaultMedia, ...defaultTools, ...defaultTools2].map(i => [itemKey(i), i]));
- 
-  // Build category lists from saved order, supporting cross-category moves
-  const categories = ["main", "media", "tools", "tools2"];
-  const categoryDefaults = { main: defaultLinks, media: defaultMedia, tools: defaultTools, tools2: defaultTools2 };
- 
-  function buildLists(savedOrder) {
-    if (!savedOrder) return { main: [...defaultLinks], media: [...defaultMedia], tools: [...defaultTools], tools2: [...defaultTools2] };
- 
-    const used = new Set();
-    const result = { main: [], media: [], tools: [], tools2: [] };
- 
-    for (const cat of categories) {
-      const keys = savedOrder[cat] || [];
-      for (const k of keys) {
-        if (allItems.has(k) && !used.has(k)) {
-          result[cat].push(allItems.get(k));
-          used.add(k);
-        }
-      }
-    }
- 
-    // Append any new items not in saved order to their default category
-    for (const cat of categories) {
-      for (const item of categoryDefaults[cat]) {
-        if (!used.has(itemKey(item))) {
-          result[cat].push(item);
-          used.add(itemKey(item));
-        }
-      }
-    }
- 
-    return result;
-  }
- 
-  let categoryLists = $derived(buildLists(sidebarOrder));
-  let links = $derived(categoryLists.main);
-  let media = $derived(categoryLists.media);
-  let tools = $derived(categoryLists.tools);
-  let tools2 = $derived(categoryLists.tools2);
- 
-  // Collapsed state for categories (persisted in sidebarOrder)
-  let collapsed = $state(sidebarOrder?.collapsed ? { ...sidebarOrder.collapsed } : {});
- 
-  // Drag-and-drop state
-  let dragCategory = $state(null);
-  let dragIndex = $state(-1);
-  let dropCategory = $state(null);
-  let dropIndex = $state(-1);
- 
-  function handleDragStart(category, index, e) {
-    dragCategory = category;
-    dragIndex = index;
-    e.dataTransfer.effectAllowed = "move";
-    e.dataTransfer.setData("text/plain", "");
-  }
- 
-  function handleDragOver(category, index, e) {
-    e.preventDefault();
-    e.dataTransfer.dropEffect = "move";
-    dropCategory = category;
-    dropIndex = index;
-  }
- 
-  function handleDragEnd() {
-    dragCategory = null;
-    dragIndex = -1;
-    dropCategory = null;
-    dropIndex = -1;
-  }
- 
-  function handleDrop(category, index, e) {
-    e.preventDefault();
-    if (dragCategory === null) { handleDragEnd(); return; }
-    if (dragCategory === category && dragIndex === index) { handleDragEnd(); return; }
-    const lists = { main: [...links], media: [...media], tools: [...tools], tools2: [...tools2] };
-    const [moved] = lists[dragCategory].splice(dragIndex, 1);
-    lists[category].splice(index, 0, moved);
-    const newOrder = {
-      main: lists.main.map(itemKey),
-      media: lists.media.map(itemKey),
-      tools: lists.tools.map(itemKey),
-      tools2: lists.tools2.map(itemKey),
-      collapsed,
-    };
-    onOrderChange(newOrder);
-    handleDragEnd();
-  }
- 
-  // Allow dropping at end of a category (on the section divider/header area)
-  function handleCategoryDragOver(category, e) {
-    e.preventDefault();
-    e.dataTransfer.dropEffect = "move";
-    dropCategory = category;
-    dropIndex = -1;
-  }
- 
-  function handleCategoryDrop(category, e) {
-    e.preventDefault();
-    if (dragCategory === null) { handleDragEnd(); return; }
-    const lists = { main: [...links], media: [...media], tools: [...tools], tools2: [...tools2] };
-    const [moved] = lists[dragCategory].splice(dragIndex, 1);
-    lists[category].push(moved);
-    const newOrder = {
-      main: lists.main.map(itemKey),
-      media: lists.media.map(itemKey),
-      tools: lists.tools.map(itemKey),
-      tools2: lists.tools2.map(itemKey),
-      collapsed,
-    };
-    onOrderChange(newOrder);
-    handleDragEnd();
-  }
- 
-  function toPublicHttpsUrl(href) {
-    if (typeof window === "undefined") return href;
-    if (!href) return href;
- 
-    const isStandardHttps = location.protocol === "https:" && (!location.port || location.port === "443");
-    if (!isStandardHttps) return href;
- 
-    try {
-      const url = new URL(href);
-      if (url.protocol === "https:" && url.port === "8443") {
-        url.port = "";
-        return url.toString();
-      }
-      return href;
-    } catch {
-      return href;
-    }
-  }
- 
-  function extHref(svc) {
-    // Look up service URL from fetched config by label (lowercased, underscored)
-    const key = svc.label ? svc.label.toLowerCase().replace(/\s+/g, "_") : "";
-    const configUrl = serviceUrls[key];
-    const remoteHref = configUrl || svc.remoteHref;
- 
-    if (lanReachable && svc.port) return `http://${lanIp}:${svc.port}`;
-    if (remoteHref) return lanReachable ? remoteHref : toPublicHttpsUrl(remoteHref);
-    if (svc.port) {
-      const host = /^\d+\.\d+\.\d+\.\d+$/.test(location.hostname) ? location.hostname : tsIp;
-      return `http://${host}:${svc.port}`;
-    }
-    return svc.href;
-  }
- 
-  function navigate(id) {
-    page = id;
-    sidebarOpen = false;
-  }
- 
-  function dragClasses(category, index) {
-    if (dragCategory === category && index === dragIndex) return "opacity-30";
-    if (dropCategory === category && index === dropIndex) return "border-t-2 border-primary-500";
-    return "";
-  }
- 
-  function toggleCollapse(cat) {
-    collapsed[cat] = !collapsed[cat];
-    const newOrder = {
-      main: links.map(itemKey),
-      media: media.map(itemKey),
-      tools: tools.map(itemKey),
-      tools2: tools2.map(itemKey),
-      collapsed,
-    };
-    onOrderChange(newOrder);
-  }
-</script>
- 
-{#snippet iconSvg(icon)}
-  <span class="w-5 h-5 flex items-center justify-center">
-    {#if icon === "grid"}
-      <svg class="w-[18px] h-[18px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zm10 0a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zm10 0a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" /></svg>
-    {:else if icon === "sparkles"}
-      <svg class="w-[18px] h-[18px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M12 3l1.8 4.2L18 9l-4.2 1.8L12 15l-1.8-4.2L6 9l4.2-1.8L12 3zm6 11l.9 2.1L21 17l-2.1.9L18 20l-.9-2.1L15 17l2.1-.9L18 14zM5 14l.9 2.1L8 17l-2.1.9L5 20l-.9-2.1L2 17l2.1-.9L5 14z" /></svg>
-    {:else if icon === "box"}
-      <svg class="w-[18px] h-[18px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" /></svg>
-    {:else if icon === "bolt"}
-      <svg class="w-[18px] h-[18px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M13 3L4 14h6l-1 7 9-11h-6l1-7z" /></svg>
-    {:else if icon === "folder"}
-      <svg class="w-[18px] h-[18px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" /></svg>
-    {:else if icon === "terminal"}
-      <svg class="w-[18px] h-[18px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" /></svg>
-    {:else if icon === "shield"}
-      <svg class="w-[18px] h-[18px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" /></svg>
-    {:else if icon === "kanban"}
-      <svg class="w-[18px] h-[18px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2zm8 0h-2a2 2 0 00-2 2v8a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2z" /></svg>
-    {:else if icon === "music"}
-      <svg class="w-[16px] h-[16px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M9 19V6l12-3v13M9 19c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zm12-3c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2z" /></svg>
-    {:else if icon === "play"}
-      <svg class="w-[16px] h-[16px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" /><path stroke-linecap="round" stroke-linejoin="round" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
-    {:else if icon === "headphones"}
-      <svg class="w-[16px] h-[16px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M3 18v-6a9 9 0 0118 0v6M3 18a3 3 0 003 3h0a3 3 0 003-3v-2a3 3 0 00-3-3h0a3 3 0 00-3 3v2zm18 0a3 3 0 01-3 3h0a3 3 0 01-3-3v-2a3 3 0 013-3h0a3 3 0 013 3v2z" /></svg>
-    {:else if icon === "image"}
-      <svg class="w-[16px] h-[16px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" /></svg>
-    {:else if icon === "download"}
-      <svg class="w-[16px] h-[16px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M9 19l3 3m0 0l3-3m-3 3V10" /></svg>
-    {:else if icon === "openclaw"}
-      <svg class="w-[16px] h-[16px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z" /></svg>
-    {:else if icon === "chat"}
-      <svg class="w-[16px] h-[16px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M17 8h2a2 2 0 012 2v6a2 2 0 01-2 2h-2v4l-4-4H9a2 2 0 01-2-2v-1m0-3V6a2 2 0 012-2h8a2 2 0 012 2v3a2 2 0 01-2 2H9l-4 4V9z" /></svg>
-    {:else if icon === "code"}
-      <svg class="w-[16px] h-[16px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" /></svg>
-    {:else if icon === "git"}
-      <svg class="w-[16px] h-[16px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" /></svg>
-    {:else if icon === "pdf"}
-      <svg class="w-[16px] h-[16px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" /></svg>
-    {:else if icon === "lock"}
-      <svg class="w-[16px] h-[16px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" /></svg>
-    {:else if icon === "speedtest"}
-      <svg class="w-[16px] h-[16px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M13 10V3L4 14h7v7l9-11h-7z" /></svg>
-    {:else if icon === "users"}
-      <svg class="w-[16px] h-[16px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" /></svg>
-    {:else if icon === "n8n"}
-      <svg class="w-[16px] h-[16px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M4 6h16M4 12h16M4 18h16" /><circle cx="8" cy="6" r="1.5" fill="currentColor"/><circle cx="16" cy="12" r="1.5" fill="currentColor"/><circle cx="10" cy="18" r="1.5" fill="currentColor"/></svg>
-    {/if}
-  </span>
-{/snippet}
- 
-<!-- Mobile overlay -->
-{#if sidebarOpen}
-  <button class="fixed inset-0 bg-black/40 z-40 md:hidden" onclick={() => sidebarOpen = false} aria-label="Close sidebar"></button>
-{/if}
- 
-<aside class="fixed inset-y-0 left-0 z-50 w-[240px] h-screen flex flex-col shrink-0 border-r border-surface-200 dark:border-surface-700 bg-white dark:bg-surface-800 transition-transform duration-200 md:static md:translate-x-0 {sidebarOpen ? 'translate-x-0' : '-translate-x-full'}">
-  <!-- Logo -->
-  <div class="px-5 py-5 flex items-center gap-3">
-    <div class="w-8 h-8 rounded-lg bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center shadow-md">
-      <svg class="w-4 h-4 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2" /></svg>
-    </div>
-    <div>
-      <h1 class="text-sm font-bold tracking-tight text-surface-900 dark:text-white">NAS Dashboard</h1>
-      <p class="text-[10px] text-surface-400 font-medium">DS224+ · Synology</p>
-    </div>
-  </div>
- 
-  <!-- Nav Links -->
-  <nav class="flex-1 px-3 mt-2 overflow-y-auto">
-    {#each [["main", "Main", links], ["media", "Media", media], ["tools", "Tools", tools], ["tools2", "Tools 2", tools2]] as [cat, label, items], ci}
-      {#if ci > 0}
-        <div class="my-4 border-t border-surface-200 dark:border-surface-700"></div>
-      {/if}
-      <button
-        class="w-full text-left text-[10px] font-semibold uppercase tracking-wider text-surface-400 px-3 mb-2 flex items-center justify-between {cat !== 'main' ? 'cursor-pointer' : ''}"
-        ondragover={(e) => handleCategoryDragOver(cat, e)}
-        ondrop={(e) => handleCategoryDrop(cat, e)}
-        onclick={() => cat !== 'main' && toggleCollapse(cat)}
-        disabled={cat === 'main'}
-      >
-        {label}
-        {#if cat !== "main"}
-          <svg class="w-3 h-3 opacity-30 hover:opacity-50 transition-all duration-150 {collapsed[cat] ? '-rotate-90' : ''}" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7" /></svg>
-        {/if}
-      </button>
-      {#if cat === "main" || !collapsed[cat]}
-      {#if items.length === 0}
-        <div
-          class="px-3 py-3 mx-1 rounded-lg border border-dashed text-[11px] text-center transition-colors duration-150
-            {dropCategory === cat ? 'border-primary-400 bg-primary-50 dark:bg-primary-900/20 text-primary-500' : 'border-surface-300 dark:border-surface-600 text-surface-400'}"
-          ondragover={(e) => handleCategoryDragOver(cat, e)}
-          ondrop={(e) => handleCategoryDrop(cat, e)}
-        >
-          Drop items here
-        </div>
-      {/if}
-      {#each items as item, i}
-        {#if canAccess(item.id) && canSeeSidebarLink(item)}
-          {#if item.external || item.remoteHref || item.port}
-            <a
-              href={extHref(item)}
-              target="_blank"
-              rel="noopener"
-              draggable="true"
-              ondragstart={(e) => handleDragStart(cat, i, e)}
-              ondragover={(e) => handleDragOver(cat, i, e)}
-              ondrop={(e) => handleDrop(cat, i, e)}
-              ondragend={handleDragEnd}
-              class="w-full text-left px-3 py-2 rounded-lg text-[13px] font-medium flex items-center gap-2.5 mb-0.5 text-surface-500 hover:bg-surface-100 dark:hover:bg-surface-700 hover:text-surface-700 dark:hover:text-surface-200 dark:text-surface-400 transition-all duration-150 group {dragClasses(cat, i)}"
-            >
-              <svg class="w-3 h-3 opacity-0 group-hover:opacity-30 -ml-1 mr-0.5 shrink-0 cursor-grab" fill="currentColor" viewBox="0 0 24 24"><circle cx="9" cy="6" r="2"/><circle cx="15" cy="6" r="2"/><circle cx="9" cy="12" r="2"/><circle cx="15" cy="12" r="2"/><circle cx="9" cy="18" r="2"/><circle cx="15" cy="18" r="2"/></svg>
-              {@render iconSvg(item.icon)}
-              {item.label}
-              <svg class="w-3 h-3 ml-auto opacity-40" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" /></svg>
-            </a>
-          {:else}
-            <button
-              draggable="true"
-              ondragstart={(e) => handleDragStart(cat, i, e)}
-              ondragover={(e) => handleDragOver(cat, i, e)}
-              ondrop={(e) => handleDrop(cat, i, e)}
-              ondragend={handleDragEnd}
-              class="w-full text-left px-3 py-2.5 rounded-lg text-[13px] font-medium flex items-center gap-2.5 mb-0.5 transition-all duration-150 group
-                {page === item.id
-                  ? 'bg-primary-50 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300 shadow-sm'
-                  : 'text-surface-500 dark:text-surface-400 hover:bg-surface-100 dark:hover:bg-surface-700 hover:text-surface-700 dark:hover:text-surface-200'
-                } {dragClasses(cat, i)}"
-              onclick={() => navigate(item.id)}
-            >
-              <svg class="w-3 h-3 opacity-0 group-hover:opacity-30 -ml-1 mr-0.5 shrink-0 cursor-grab" fill="currentColor" viewBox="0 0 24 24"><circle cx="9" cy="6" r="2"/><circle cx="15" cy="6" r="2"/><circle cx="9" cy="12" r="2"/><circle cx="15" cy="12" r="2"/><circle cx="9" cy="18" r="2"/><circle cx="15" cy="18" r="2"/></svg>
-              {@render iconSvg(item.icon)}
-              {item.label}
-            </button>
-          {/if}
-        {/if}
-      {/each}
-      {/if}
-    {/each}
-  </nav>
- 
-  <!-- Bottom actions -->
-  <div class="px-3 pb-3 pt-2 border-t border-surface-200 dark:border-surface-700">
-    {#if userRole === "admin"}
-    <button
-      class="w-full px-3 py-2 rounded-lg text-[13px] font-medium flex items-center gap-2.5 text-surface-500 dark:text-surface-400 hover:bg-surface-100 dark:hover:bg-surface-700 transition-all duration-150 mb-1 {page === 'settings' ? 'bg-primary-50 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300 shadow-sm' : ''}"
-      onclick={() => navigate('settings')}
-    >
-      <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" /><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /></svg>
-      Settings
-    </button>
-    {/if}
-    <div class="flex items-center gap-1">
-      <button
-        onclick={logout}
-        class="flex-1 px-3 py-2 rounded-lg text-[13px] font-medium flex items-center gap-2.5 text-surface-500 dark:text-surface-400 hover:bg-surface-100 dark:hover:bg-surface-700 transition-all duration-150 hover:text-red-600 dark:hover:text-red-400"
-      >
-        <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" /></svg>
-        Sign out
-      </button>
-      <button
-        onclick={toggleTheme}
-        class="p-2 rounded-lg text-surface-400 hover:bg-surface-100 dark:hover:bg-surface-700 hover:text-surface-600 dark:hover:text-surface-300 transition-all duration-150"
-        title={dark ? 'Light Mode' : 'Dark Mode'}
-      >
-        {#if dark}
-          <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" /></svg>
-        {:else}
-          <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" /></svg>
-        {/if}
-      </button>
-    </div>
-  </div>
-</aside>
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/dashboard/frontend/coverage/src/components/index.html b/dashboard/frontend/coverage/src/components/index.html deleted file mode 100644 index 2b32033..0000000 --- a/dashboard/frontend/coverage/src/components/index.html +++ /dev/null @@ -1,116 +0,0 @@ - - - - - - Code coverage report for src/components - - - - - - - - - -
-
-

All files src/components

-
- -
- 0% - Statements - 0/301 -
- - -
- 0% - Branches - 0/1 -
- - -
- 0% - Functions - 0/1 -
- - -
- 0% - Lines - 0/301 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FileStatementsBranchesFunctionsLines
Sidebar.svelte -
-
0%0/3010%0/10%0/10%0/301
-
-
-
- - - - - - - - \ No newline at end of file diff --git a/dashboard/frontend/coverage/src/components/opc/AgentPanel.svelte.html b/dashboard/frontend/coverage/src/components/opc/AgentPanel.svelte.html deleted file mode 100644 index fea9785..0000000 --- a/dashboard/frontend/coverage/src/components/opc/AgentPanel.svelte.html +++ /dev/null @@ -1,688 +0,0 @@ - - - - - - Code coverage report for src/components/opc/AgentPanel.svelte - - - - - - - - - -
-
-

All files / src/components/opc AgentPanel.svelte

-
- -
- 0% - Statements - 0/117 -
- - -
- 0% - Branches - 0/1 -
- - -
- 0% - Functions - 0/1 -
- - -
- 0% - Lines - 0/117 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -178 -179 -180 -181 -182 -183 -184 -185 -186 -187 -188 -189 -190 -191 -192 -193 -194 -195 -196 -197 -198 -199 -200 -201 -202  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  - 
<script>
-  import { onMount } from "svelte";
-  import { getAgents, getExecutions } from "../../lib/opc-api.js";
- 
-  let agents = $state([]);
-  let executions = $state([]);
-  let loading = $state(true);
-  let selectedAgent = $state(null);
- 
-  async function loadData() {
-    loading = true;
-    try {
-      const [agentsRes, executionsRes] = await Promise.all([
-        getAgents(),
-        getExecutions({ limit: 20 })
-      ]);
-      agents = agentsRes.items || [];
-      executions = executionsRes.items || [];
-    } catch (e) {
-      console.error("Failed to load agent data:", e);
-    } finally {
-      loading = false;
-    }
-  }
- 
-  function getAgentIcon(agentId) {
-    const icons = {
-      pm: "📋",
-      cto: "💻",
-      coo: "⚙️",
-      ceo: "🎯",
-      marketing: "📢",
-      social_media: "📱"
-    };
-    return icons[agentId] || "🤖";
-  }
- 
-  function getStatusColor(status) {
-    const colors = {
-      pending: "bg-slate-100 text-slate-700",
-      running: "bg-blue-100 text-blue-700",
-      completed: "bg-emerald-100 text-emerald-700",
-      failed: "bg-rose-100 text-rose-700",
-      pending_approval: "bg-amber-100 text-amber-700"
-    };
-    return colors[status] || colors.pending;
-  }
- 
-  function getAgentExecutions(agentId) {
-    return executions.filter(e => e.agent_id === agentId);
-  }
- 
-  function formatTimestamp(timestamp) {
-    if (!timestamp) return "N/A";
-    return new Date(timestamp).toLocaleString();
-  }
- 
-  onMount(() => {
-    loadData();
-    // Refresh every 10 seconds
-    const interval = setInterval(loadData, 10000);
-    return () => clearInterval(interval);
-  });
-</script>
- 
-<div class="space-y-6">
-  <h2 class="text-xl font-semibold text-surface-900 dark:text-surface-100">
-    AI Agents
-  </h2>
- 
-  {#if loading}
-    <div class="text-surface-600 dark:text-surface-400">Loading agents...</div>
-  {:else}
-    <!-- Agent Cards -->
-    <div class="grid grid-cols-1 md:grid-cols-3 gap-4">
-      {#each agents as agent}
-        {@const agentExecs = getAgentExecutions(agent.id)}
-        {@const runningExecs = agentExecs.filter(e => e.status === "running")}
-        {@const completedExecs = agentExecs.filter(e => e.status === "completed")}
- 
-        <button
-          onclick={() => selectedAgent = selectedAgent?.id === agent.id ? null : agent}
-          class="bg-white dark:bg-surface-800 rounded-lg p-4 border-2 transition-all text-left hover:shadow-lg {selectedAgent?.id === agent.id ? 'border-primary-500' : 'border-surface-200 dark:border-surface-700'}"
-        >
-          <!-- Agent Header -->
-          <div class="flex items-start justify-between mb-3">
-            <div class="flex items-center gap-2">
-              <span class="text-3xl">{getAgentIcon(agent.id)}</span>
-              <div>
-                <h3 class="font-semibold text-surface-900 dark:text-surface-100">
-                  {agent.name}
-                </h3>
-                <p class="text-xs text-surface-600 dark:text-surface-400">
-                  {agent.role}
-                </p>
-              </div>
-            </div>
-            {#if runningExecs.length > 0}
-              <span class="flex items-center gap-1 text-xs text-blue-600 dark:text-blue-400">
-                <span class="animate-pulse">●</span>
-                Working
-              </span>
-            {:else}
-              <span class="text-xs text-surface-500">Idle</span>
-            {/if}
-          </div>
- 
-          <!-- Agent Stats -->
-          <div class="grid grid-cols-2 gap-2 text-sm">
-            <div class="bg-surface-50 dark:bg-surface-900 rounded p-2">
-              <div class="text-xs text-surface-600 dark:text-surface-400">Total Tasks</div>
-              <div class="text-lg font-semibold text-surface-900 dark:text-surface-100">
-                {agentExecs.length}
-              </div>
-            </div>
-            <div class="bg-surface-50 dark:bg-surface-900 rounded p-2">
-              <div class="text-xs text-surface-600 dark:text-surface-400">Completed</div>
-              <div class="text-lg font-semibold text-emerald-600 dark:text-emerald-400">
-                {completedExecs.length}
-              </div>
-            </div>
-          </div>
- 
-          <!-- Capabilities -->
-          <div class="mt-3">
-            <div class="text-xs text-surface-600 dark:text-surface-400 mb-1">Capabilities:</div>
-            <div class="flex flex-wrap gap-1">
-              {#each agent.capabilities.slice(0, 3) as capability}
-                <span class="text-xs px-2 py-0.5 rounded bg-primary-50 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300">
-                  {capability.replace(/_/g, ' ')}
-                </span>
-              {/each}
-              {#if agent.capabilities.length > 3}
-                <span class="text-xs text-surface-500">+{agent.capabilities.length - 3}</span>
-              {/if}
-            </div>
-          </div>
-        </button>
-      {/each}
-    </div>
- 
-    <!-- Execution Log -->
-    <div class="bg-white dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700">
-      <div class="p-4 border-b border-surface-200 dark:border-surface-700">
-        <h3 class="font-semibold text-surface-900 dark:text-surface-100">
-          {selectedAgent ? `${selectedAgent.name} Execution Log` : "Recent Executions"}
-        </h3>
-      </div>
- 
-      <div class="divide-y divide-surface-200 dark:divide-surface-700 max-h-96 overflow-y-auto">
-        {#each (selectedAgent ? getAgentExecutions(selectedAgent.id) : executions) as execution}
-          <div class="p-4 hover:bg-surface-50 dark:hover:bg-surface-900/50">
-            <div class="flex items-start justify-between mb-2">
-              <div class="flex items-center gap-2">
-                <span class="text-xl">{getAgentIcon(execution.agent_id)}</span>
-                <div>
-                  <div class="font-medium text-surface-900 dark:text-surface-100">
-                    Task #{execution.task_id}
-                  </div>
-                  <div class="text-xs text-surface-600 dark:text-surface-400">
-                    {formatTimestamp(execution.started_at)}
-                  </div>
-                </div>
-              </div>
-              <span class="text-xs px-2 py-1 rounded {getStatusColor(execution.status)}">
-                {execution.status.replace(/_/g, ' ')}
-              </span>
-            </div>
- 
-            {#if execution.output_result?.reasoning}
-              <p class="text-sm text-surface-700 dark:text-surface-300 mb-2">
-                {execution.output_result.reasoning}
-              </p>
-            {/if}
- 
-            {#if execution.actions_proposed && execution.actions_proposed.length > 0}
-              <div class="text-xs text-surface-600 dark:text-surface-400">
-                Actions: {execution.actions_proposed.length}
-                {#each execution.actions_proposed.slice(0, 2) as action}
-                  <span class="ml-2 px-1.5 py-0.5 rounded bg-surface-100 dark:bg-surface-700">
-                    {action.type}
-                  </span>
-                {/each}
-              </div>
-            {/if}
- 
-            {#if execution.error_message}
-              <div class="mt-2 text-xs text-rose-600 dark:text-rose-400">
-                Error: {execution.error_message}
-              </div>
-            {/if}
-          </div>
-        {:else}
-          <div class="p-8 text-center text-surface-500">
-            No executions yet
-          </div>
-        {/each}
-      </div>
-    </div>
-  {/if}
-</div>
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/dashboard/frontend/coverage/src/components/opc/KanbanBoard.svelte.html b/dashboard/frontend/coverage/src/components/opc/KanbanBoard.svelte.html deleted file mode 100644 index b79b9fb..0000000 --- a/dashboard/frontend/coverage/src/components/opc/KanbanBoard.svelte.html +++ /dev/null @@ -1,205 +0,0 @@ - - - - - - Code coverage report for src/components/opc/KanbanBoard.svelte - - - - - - - - - -
-
-

All files / src/components/opc KanbanBoard.svelte

-
- -
- 0% - Statements - 0/26 -
- - -
- 0% - Branches - 0/1 -
- - -
- 0% - Functions - 0/1 -
- - -
- 0% - Lines - 0/26 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  - 
<script>
-  import KanbanColumn from "./KanbanColumn.svelte";
-  import { moveTask } from "../../lib/opc-api.js";
- 
-  let { tasks = [], agents = [], onEdit, onDelete, onAssign, onTaskMoved } = $props();
- 
-  const columns = [
-    { id: "parking_lot", title: "Parking Lot", status: "parking_lot" },
-    { id: "in_progress", title: "In Progress", status: "in_progress" },
-    { id: "done", title: "Done", status: "done" }
-  ];
- 
-  function getTasksByStatus(status) {
-    return tasks.filter(t => t.status === status);
-  }
- 
-  async function handleDrop(taskId, newStatus) {
-    try {
-      await moveTask(taskId, newStatus);
-      onTaskMoved?.(taskId, newStatus);
-    } catch (e) {
-      alert("Failed to move task: " + e.message);
-    }
-  }
-</script>
- 
-<div class="grid grid-cols-1 md:grid-cols-3 gap-4 h-full">
-  {#each columns as column}
-    <KanbanColumn
-      title={column.title}
-      status={column.status}
-      tasks={getTasksByStatus(column.status)}
-      {agents}
-      {onEdit}
-      {onDelete}
-      {onAssign}
-      onDrop={handleDrop}
-    />
-  {/each}
-</div>
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/dashboard/frontend/coverage/src/components/opc/KanbanColumn.svelte.html b/dashboard/frontend/coverage/src/components/opc/KanbanColumn.svelte.html deleted file mode 100644 index 1c9a2ae..0000000 --- a/dashboard/frontend/coverage/src/components/opc/KanbanColumn.svelte.html +++ /dev/null @@ -1,340 +0,0 @@ - - - - - - Code coverage report for src/components/opc/KanbanColumn.svelte - - - - - - - - - -
-
-

All files / src/components/opc KanbanColumn.svelte

-
- -
- 0% - Statements - 0/46 -
- - -
- 0% - Branches - 0/1 -
- - -
- 0% - Functions - 0/1 -
- - -
- 0% - Lines - 0/46 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  - 
<script>
-  import TaskCard from "./TaskCard.svelte";
- 
-  let {
-    title,
-    status,
-    tasks = [],
-    agents = [],
-    onEdit,
-    onDelete,
-    onAssign,
-    onDrop
-  } = $props();
- 
-  const columnColors = {
-    parking_lot: "bg-slate-100 dark:bg-slate-800 border-slate-300",
-    in_progress: "bg-blue-50 dark:bg-blue-900/20 border-blue-300",
-    done: "bg-emerald-50 dark:bg-emerald-900/20 border-emerald-300"
-  };
- 
-  function handleDragOver(e) {
-    e.preventDefault();
-    e.currentTarget.classList.add("ring-2", "ring-primary-400");
-  }
- 
-  function handleDragLeave(e) {
-    e.currentTarget.classList.remove("ring-2", "ring-primary-400");
-  }
- 
-  function handleDrop(e) {
-    e.preventDefault();
-    e.currentTarget.classList.remove("ring-2", "ring-primary-400");
- 
-    const taskId = parseInt(e.dataTransfer.getData("taskId"));
-    const fromStatus = e.dataTransfer.getData("fromStatus");
- 
-    if (fromStatus !== status) {
-      onDrop?.(taskId, status);
-    }
-  }
- 
-  function handleDragStart(e, task) {
-    e.dataTransfer.setData("taskId", task.id);
-    e.dataTransfer.setData("fromStatus", task.status);
-    e.dataTransfer.effectAllowed = "move";
-  }
-</script>
- 
-<div class="flex flex-col h-full">
-  <!-- Column header -->
-  <div class="flex items-center justify-between mb-3 pb-2 border-b border-surface-300 dark:border-surface-600">
-    <h3 class="font-semibold text-surface-900 dark:text-surface-100">
-      {title}
-    </h3>
-    <span class="text-sm text-surface-600 dark:text-surface-400 bg-surface-200 dark:bg-surface-700 px-2 py-0.5 rounded-full">
-      {tasks.length}
-    </span>
-  </div>
- 
-  <!-- Drop zone -->
-  <div
-    class="flex-1 min-h-[200px] p-2 rounded-lg border-2 border-dashed transition-all {columnColors[status] || columnColors.parking_lot}"
-    ondragover={handleDragOver}
-    ondragleave={handleDragLeave}
-    ondrop={handleDrop}
-  >
-    {#if tasks.length === 0}
-      <div class="flex items-center justify-center h-32 text-surface-400 dark:text-surface-500 text-sm">
-        Drop tasks here
-      </div>
-    {:else}
-      {#each tasks as task (task.id)}
-        <div draggable="true" ondragstart={(e) => handleDragStart(e, task)}>
-          <TaskCard
-            {task}
-            {agents}
-            {onEdit}
-            {onDelete}
-            {onAssign}
-          />
-        </div>
-      {/each}
-    {/if}
-  </div>
-</div>
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/dashboard/frontend/coverage/src/components/opc/TaskCard.svelte.html b/dashboard/frontend/coverage/src/components/opc/TaskCard.svelte.html deleted file mode 100644 index 6838464..0000000 --- a/dashboard/frontend/coverage/src/components/opc/TaskCard.svelte.html +++ /dev/null @@ -1,592 +0,0 @@ - - - - - - Code coverage report for src/components/opc/TaskCard.svelte - - - - - - - - - -
-
-

All files / src/components/opc TaskCard.svelte

-
- -
- 0% - Statements - 0/94 -
- - -
- 0% - Branches - 0/1 -
- - -
- 0% - Functions - 0/1 -
- - -
- 0% - Lines - 0/94 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  - 
<script>
-  import { deleteTask, assignTask, getTaskTime } from "../../lib/opc-api.js";
- 
-  let { task, agents = [], onEdit, onDelete, onAssign } = $props();
- 
-  let showActions = $state(false);
-  let timeInfo = $state(null);
- 
-  const priorityColors = {
-    low: "bg-slate-200 text-slate-700",
-    medium: "bg-blue-100 text-blue-700",
-    high: "bg-amber-100 text-amber-700",
-    urgent: "bg-rose-100 text-rose-700"
-  };
- 
-  async function loadTimeInfo() {
-    if (task.status === "in_progress") {
-      try {
-        timeInfo = await getTaskTime(task.id);
-      } catch (e) {
-        console.error("Failed to load time info:", e);
-      }
-    }
-  }
- 
-  $effect(() => {
-    loadTimeInfo();
-  });
- 
-  async function handleDelete() {
-    if (confirm(`Delete task "${task.title}"?`)) {
-      try {
-        await deleteTask(task.id);
-        onDelete?.(task.id);
-      } catch (e) {
-        alert("Failed to delete task: " + e.message);
-      }
-    }
-  }
- 
-  async function handleAssignAgent(agentId) {
-    try {
-      await assignTask(task.id, agentId, "agent");
-      onAssign?.(task.id, agentId, "agent");
-    } catch (e) {
-      alert("Failed to assign agent: " + e.message);
-    }
-  }
- 
-  function getAgentIcon(agentId) {
-    const icons = {
-      pm: "📋",
-      cto: "💻",
-      coo: "⚙️",
-      ceo: "🎯",
-      marketing: "📢",
-      social_media: "📱"
-    };
-    return icons[agentId] || "🤖";
-  }
-</script>
- 
-<div
-  class="bg-white dark:bg-surface-800 rounded-lg p-3 mb-2 shadow-sm border border-surface-200 dark:border-surface-700 cursor-move hover:shadow-md transition-shadow"
-  onmouseenter={() => showActions = true}
-  onmouseleave={() => showActions = false}
->
-  <!-- Priority badge -->
-  <div class="flex items-start justify-between mb-2">
-    <span class="text-xs px-2 py-0.5 rounded {priorityColors[task.priority] || priorityColors.medium}">
-      {task.priority}
-    </span>
-    {#if showActions}
-      <div class="flex gap-1">
-        <button
-          onclick={() => onEdit?.(task)}
-          class="text-xs text-surface-600 hover:text-primary-600 dark:text-surface-400"
-          title="Edit"
-        >
-          ✏️
-        </button>
-        <button
-          onclick={handleDelete}
-          class="text-xs text-surface-600 hover:text-rose-600 dark:text-surface-400"
-          title="Delete"
-        >
-          🗑️
-        </button>
-      </div>
-    {/if}
-  </div>
- 
-  <!-- Title -->
-  <h4 class="font-medium text-surface-900 dark:text-surface-100 mb-1 text-sm">
-    {task.title}
-  </h4>
- 
-  <!-- Description preview -->
-  {#if task.description}
-    <p class="text-xs text-surface-600 dark:text-surface-400 mb-2 line-clamp-2">
-      {task.description}
-    </p>
-  {/if}
- 
-  <!-- Tags -->
-  {#if task.tags && task.tags.length > 0}
-    <div class="flex flex-wrap gap-1 mb-2">
-      {#each task.tags as tag}
-        <span class="text-xs px-1.5 py-0.5 rounded bg-surface-100 dark:bg-surface-700 text-surface-700 dark:text-surface-300">
-          {tag}
-        </span>
-      {/each}
-    </div>
-  {/if}
- 
-  <!-- Assignee and time -->
-  <div class="flex items-center justify-between text-xs text-surface-600 dark:text-surface-400">
-    <div class="flex items-center gap-1">
-      {#if task.assigned_to}
-        {#if task.assigned_type === "agent"}
-          <span title={task.assigned_to}>
-            {getAgentIcon(task.assigned_to)}
-          </span>
-        {:else}
-          <span>👤 {task.assigned_to}</span>
-        {/if}
-      {:else}
-        <button
-          onclick={() => showActions = true}
-          class="text-surface-500 hover:text-primary-600"
-          title="Assign"
-        >
-          Unassigned
-        </button>
-      {/if}
-    </div>
- 
-    {#if timeInfo && timeInfo.total_hours > 0}
-      <span class="text-xs text-surface-500">
-        ⏱️ {timeInfo.total_hours}h
-      </span>
-    {/if}
-  </div>
- 
-  <!-- Due date -->
-  {#if task.due_date}
-    <div class="text-xs text-surface-500 mt-1">
-      📅 {new Date(task.due_date).toLocaleDateString()}
-    </div>
-  {/if}
- 
-  <!-- Quick assign to agent (when hovering) -->
-  {#if showActions && !task.assigned_to}
-    <div class="mt-2 pt-2 border-t border-surface-200 dark:border-surface-700">
-      <div class="text-xs text-surface-600 mb-1">Assign to agent:</div>
-      <div class="flex gap-1">
-        {#each agents as agent}
-          <button
-            onclick={() => handleAssignAgent(agent.id)}
-            class="text-lg hover:scale-110 transition-transform"
-            title={agent.name}
-          >
-            {getAgentIcon(agent.id)}
-          </button>
-        {/each}
-      </div>
-    </div>
-  {/if}
-</div>
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/dashboard/frontend/coverage/src/components/opc/TaskModal.svelte.html b/dashboard/frontend/coverage/src/components/opc/TaskModal.svelte.html deleted file mode 100644 index a85267a..0000000 --- a/dashboard/frontend/coverage/src/components/opc/TaskModal.svelte.html +++ /dev/null @@ -1,835 +0,0 @@ - - - - - - Code coverage report for src/components/opc/TaskModal.svelte - - - - - - - - - -
-
-

All files / src/components/opc TaskModal.svelte

-
- -
- 0% - Statements - 0/110 -
- - -
- 0% - Branches - 0/1 -
- - -
- 0% - Functions - 0/1 -
- - -
- 0% - Lines - 0/110 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -178 -179 -180 -181 -182 -183 -184 -185 -186 -187 -188 -189 -190 -191 -192 -193 -194 -195 -196 -197 -198 -199 -200 -201 -202 -203 -204 -205 -206 -207 -208 -209 -210 -211 -212 -213 -214 -215 -216 -217 -218 -219 -220 -221 -222 -223 -224 -225 -226 -227 -228 -229 -230 -231 -232 -233 -234 -235 -236 -237 -238 -239 -240 -241 -242 -243 -244 -245 -246 -247 -248 -249 -250 -251  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  - 
<script>
-  import { createTask, updateTask } from "../../lib/opc-api.js";
- 
-  let { task = null, agents = [], onClose, onSave } = $props();
- 
-  let isEdit = $state(!!task);
-  let formData = $state({
-    title: task?.title || "",
-    description: task?.description || "",
-    status: task?.status || "parking_lot",
-    priority: task?.priority || "medium",
-    assigned_to: task?.assigned_to || "",
-    assigned_type: task?.assigned_type || "human",
-    tags: task?.tags || [],
-    due_date: task?.due_date ? task.due_date.split("T")[0] : ""
-  });
- 
-  let tagInput = $state("");
-  let saving = $state(false);
- 
-  function addTag() {
-    if (tagInput.trim() && !formData.tags.includes(tagInput.trim())) {
-      formData.tags = [...formData.tags, tagInput.trim()];
-      tagInput = "";
-    }
-  }
- 
-  function removeTag(tag) {
-    formData.tags = formData.tags.filter(t => t !== tag);
-  }
- 
-  async function handleSubmit(e) {
-    e.preventDefault();
-    saving = true;
- 
-    try {
-      const payload = {
-        ...formData,
-        due_date: formData.due_date ? new Date(formData.due_date).toISOString() : null,
-        assigned_to: formData.assigned_to || null
-      };
- 
-      if (isEdit) {
-        await updateTask(task.id, payload);
-      } else {
-        await createTask(payload);
-      }
- 
-      onSave?.();
-      onClose?.();
-    } catch (e) {
-      alert("Failed to save task: " + e.message);
-    } finally {
-      saving = false;
-    }
-  }
-</script>
- 
-<!-- Modal backdrop -->
-<div
-  class="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4"
-  onclick={(e) => e.target === e.currentTarget && onClose?.()}
->
-  <!-- Modal content -->
-  <div class="bg-white dark:bg-surface-800 rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto">
-    <!-- Header -->
-    <div class="flex items-center justify-between p-4 border-b border-surface-200 dark:border-surface-700">
-      <h2 class="text-xl font-semibold text-surface-900 dark:text-surface-100">
-        {isEdit ? "Edit Task" : "Create Task"}
-      </h2>
-      <button
-        onclick={onClose}
-        class="text-surface-500 hover:text-surface-700 dark:hover:text-surface-300"
-      >
-        ✕
-      </button>
-    </div>
- 
-    <!-- Form -->
-    <form onsubmit={handleSubmit} class="p-4 space-y-4">
-      <!-- Title -->
-      <div>
-        <label class="block text-sm font-medium text-surface-700 dark:text-surface-300 mb-1">
-          Title *
-        </label>
-        <input
-          type="text"
-          bind:value={formData.title}
-          required
-          class="w-full px-3 py-2 border border-surface-300 dark:border-surface-600 rounded-lg bg-white dark:bg-surface-900 text-surface-900 dark:text-surface-100"
-          placeholder="Task title"
-        />
-      </div>
- 
-      <!-- Description -->
-      <div>
-        <label class="block text-sm font-medium text-surface-700 dark:text-surface-300 mb-1">
-          Description
-        </label>
-        <textarea
-          bind:value={formData.description}
-          rows="4"
-          class="w-full px-3 py-2 border border-surface-300 dark:border-surface-600 rounded-lg bg-white dark:bg-surface-900 text-surface-900 dark:text-surface-100"
-          placeholder="Task description"
-        ></textarea>
-      </div>
- 
-      <!-- Status and Priority -->
-      <div class="grid grid-cols-2 gap-4">
-        <div>
-          <label class="block text-sm font-medium text-surface-700 dark:text-surface-300 mb-1">
-            Status
-          </label>
-          <select
-            bind:value={formData.status}
-            class="w-full px-3 py-2 border border-surface-300 dark:border-surface-600 rounded-lg bg-white dark:bg-surface-900 text-surface-900 dark:text-surface-100"
-          >
-            <option value="parking_lot">Parking Lot</option>
-            <option value="in_progress">In Progress</option>
-            <option value="done">Done</option>
-          </select>
-        </div>
- 
-        <div>
-          <label class="block text-sm font-medium text-surface-700 dark:text-surface-300 mb-1">
-            Priority
-          </label>
-          <select
-            bind:value={formData.priority}
-            class="w-full px-3 py-2 border border-surface-300 dark:border-surface-600 rounded-lg bg-white dark:bg-surface-900 text-surface-900 dark:text-surface-100"
-          >
-            <option value="low">Low</option>
-            <option value="medium">Medium</option>
-            <option value="high">High</option>
-            <option value="urgent">Urgent</option>
-          </select>
-        </div>
-      </div>
- 
-      <!-- Assignee -->
-      <div class="grid grid-cols-2 gap-4">
-        <div>
-          <label class="block text-sm font-medium text-surface-700 dark:text-surface-300 mb-1">
-            Assign to
-          </label>
-          <select
-            bind:value={formData.assigned_type}
-            class="w-full px-3 py-2 border border-surface-300 dark:border-surface-600 rounded-lg bg-white dark:bg-surface-900 text-surface-900 dark:text-surface-100"
-          >
-            <option value="human">Human</option>
-            <option value="agent">Agent</option>
-          </select>
-        </div>
- 
-        <div>
-          <label class="block text-sm font-medium text-surface-700 dark:text-surface-300 mb-1">
-            {formData.assigned_type === "agent" ? "Agent" : "Username"}
-          </label>
-          {#if formData.assigned_type === "agent"}
-            <select
-              bind:value={formData.assigned_to}
-              class="w-full px-3 py-2 border border-surface-300 dark:border-surface-600 rounded-lg bg-white dark:bg-surface-900 text-surface-900 dark:text-surface-100"
-            >
-              <option value="">Unassigned</option>
-              {#each agents as agent}
-                <option value={agent.id}>{agent.name}</option>
-              {/each}
-            </select>
-          {:else}
-            <input
-              type="text"
-              bind:value={formData.assigned_to}
-              class="w-full px-3 py-2 border border-surface-300 dark:border-surface-600 rounded-lg bg-white dark:bg-surface-900 text-surface-900 dark:text-surface-100"
-              placeholder="Username"
-            />
-          {/if}
-        </div>
-      </div>
- 
-      <!-- Due date -->
-      <div>
-        <label class="block text-sm font-medium text-surface-700 dark:text-surface-300 mb-1">
-          Due Date
-        </label>
-        <input
-          type="date"
-          bind:value={formData.due_date}
-          class="w-full px-3 py-2 border border-surface-300 dark:border-surface-600 rounded-lg bg-white dark:bg-surface-900 text-surface-900 dark:text-surface-100"
-        />
-      </div>
- 
-      <!-- Tags -->
-      <div>
-        <label class="block text-sm font-medium text-surface-700 dark:text-surface-300 mb-1">
-          Tags
-        </label>
-        <div class="flex gap-2 mb-2">
-          <input
-            type="text"
-            bind:value={tagInput}
-            onkeydown={(e) => e.key === "Enter" && (e.preventDefault(), addTag())}
-            class="flex-1 px-3 py-2 border border-surface-300 dark:border-surface-600 rounded-lg bg-white dark:bg-surface-900 text-surface-900 dark:text-surface-100"
-            placeholder="Add tag and press Enter"
-          />
-          <button
-            type="button"
-            onclick={addTag}
-            class="px-4 py-2 bg-surface-200 dark:bg-surface-700 text-surface-700 dark:text-surface-300 rounded-lg hover:bg-surface-300 dark:hover:bg-surface-600"
-          >
-            Add
-          </button>
-        </div>
-        {#if formData.tags.length > 0}
-          <div class="flex flex-wrap gap-2">
-            {#each formData.tags as tag}
-              <span class="inline-flex items-center gap-1 px-2 py-1 bg-primary-100 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300 rounded text-sm">
-                {tag}
-                <button
-                  type="button"
-                  onclick={() => removeTag(tag)}
-                  class="hover:text-primary-900 dark:hover:text-primary-100"
-                >
-                  ✕
-                </button>
-              </span>
-            {/each}
-          </div>
-        {/if}
-      </div>
- 
-      <!-- Actions -->
-      <div class="flex justify-end gap-2 pt-4 border-t border-surface-200 dark:border-surface-700">
-        <button
-          type="button"
-          onclick={onClose}
-          class="px-4 py-2 text-surface-700 dark:text-surface-300 hover:bg-surface-100 dark:hover:bg-surface-700 rounded-lg"
-        >
-          Cancel
-        </button>
-        <button
-          type="submit"
-          disabled={saving}
-          class="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 disabled:opacity-50"
-        >
-          {saving ? "Saving..." : isEdit ? "Update" : "Create"}
-        </button>
-      </div>
-    </form>
-  </div>
-</div>
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/dashboard/frontend/coverage/src/components/opc/index.html b/dashboard/frontend/coverage/src/components/opc/index.html deleted file mode 100644 index 037d2be..0000000 --- a/dashboard/frontend/coverage/src/components/opc/index.html +++ /dev/null @@ -1,176 +0,0 @@ - - - - - - Code coverage report for src/components/opc - - - - - - - - - -
-
-

All files src/components/opc

-
- -
- 0% - Statements - 0/393 -
- - -
- 0% - Branches - 0/5 -
- - -
- 0% - Functions - 0/5 -
- - -
- 0% - Lines - 0/393 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FileStatementsBranchesFunctionsLines
AgentPanel.svelte -
-
0%0/1170%0/10%0/10%0/117
KanbanBoard.svelte -
-
0%0/260%0/10%0/10%0/26
KanbanColumn.svelte -
-
0%0/460%0/10%0/10%0/46
TaskCard.svelte -
-
0%0/940%0/10%0/10%0/94
TaskModal.svelte -
-
0%0/1100%0/10%0/10%0/110
-
-
-
- - - - - - - - \ No newline at end of file diff --git a/dashboard/frontend/coverage/src/index.html b/dashboard/frontend/coverage/src/index.html deleted file mode 100644 index 2b0b65c..0000000 --- a/dashboard/frontend/coverage/src/index.html +++ /dev/null @@ -1,131 +0,0 @@ - - - - - - Code coverage report for src - - - - - - - - - -
-
-

All files src

-
- -
- 0% - Statements - 0/182 -
- - -
- 0% - Branches - 0/2 -
- - -
- 0% - Functions - 0/2 -
- - -
- 0% - Lines - 0/182 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FileStatementsBranchesFunctionsLines
App.svelte -
-
0%0/1770%0/10%0/10%0/177
main.js -
-
0%0/50%0/10%0/10%0/5
-
-
-
- - - - - - - - \ No newline at end of file diff --git a/dashboard/frontend/coverage/src/lib/api.js.html b/dashboard/frontend/coverage/src/lib/api.js.html deleted file mode 100644 index 17047f4..0000000 --- a/dashboard/frontend/coverage/src/lib/api.js.html +++ /dev/null @@ -1,787 +0,0 @@ - - - - - - Code coverage report for src/lib/api.js - - - - - - - - - -
-
-

All files / src/lib api.js

-
- -
- 64.92% - Statements - 124/191 -
- - -
- 90.69% - Branches - 39/43 -
- - -
- 57.69% - Functions - 15/26 -
- - -
- 64.92% - Lines - 124/191 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -178 -179 -180 -181 -182 -183 -184 -185 -186 -187 -188 -189 -190 -191 -192 -193 -194 -195 -196 -197 -198 -199 -200 -201 -202 -203 -204 -205 -206 -207 -208 -209 -210 -211 -212 -213 -214 -215 -216 -217 -218 -219 -220 -221 -222 -223 -224 -225 -226 -227 -228 -229 -230 -231 -232 -233 -234 -2351x -1x -  -1x -1x -  -1x -1x -1x -  -1x -30x -30x -12x -30x -  -1x -5x -5x -  -1x -1x -  -1x -  -3x -3x -3x -  -3x -3x -3x -  -6x -6x -6x -6x -6x -6x -6x -6x -3x -6x -6x -6x -  -1x -5x -5x -5x -5x -5x -2x -2x -2x -  -3x -5x -1x -1x -1x -1x -1x -  -  -  -  -5x -5x -5x -5x -5x -5x -  -20x -20x -20x -  -20x -3x -3x -3x -3x -  -20x -20x -  -20x -20x -  -20x -2x -2x -1x -1x -1x -2x -  -20x -1x -1x -1x -  -20x -2x -2x -2x -2x -2x -16x -20x -4x -4x -4x -20x -  -1x -13x -13x -  -1x -2x -2x -  -1x -1x -1x -  -1x -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -1x -1x -1x -  -1x -1x -1x -  -1x -1x -1x -  -1x -  -  -  -1x -  -  -  -1x -  -  -  -1x -  -  -  -1x -  -  -  -1x -  -  -  -1x -  -  -  -  -  -  -  -  -  -  -1x -  -  -  -1x -1x -1x -  -1x -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  - 
const BASE = "/api";
-let token = "";
- 
-// Current user info populated after auth
-export let currentUser = { username: "", role: "", pages: [] };
- 
-export function setCurrentUser(u) {
-  currentUser = u;
-}
- 
-export function setToken(t) {
-  token = t;
-  if (t) localStorage.setItem("token", t);
-  else localStorage.removeItem("token");
-}
- 
-export function getToken() {
-  return token;
-}
- 
-// Refresh token is cookie-managed server-side.
-export function setRefreshToken() {}
- 
-let refreshPromise = null;
- 
-function getLegacyRefreshToken() {
-  return localStorage.getItem("refresh_token") || "";
-}
- 
-function clearLegacyRefreshToken() {
-  localStorage.removeItem("refresh_token");
-}
- 
-async function requestRefresh(body) {
-  const r = await fetch(BASE + "/auth/refresh", {
-    method: "POST",
-    headers: { "Content-Type": "application/json" },
-    credentials: "same-origin",
-    body: body ? JSON.stringify(body) : undefined,
-  });
-  if (!r.ok) return false;
-  const data = await r.json();
-  setToken(data.access_token || "");
-  return !!data.access_token;
-}
- 
-export async function tryRefreshSession() {
-  if (refreshPromise) return refreshPromise;
-  refreshPromise = (async () => {
-    try {
-      const cookieRefreshed = await requestRefresh();
-      if (cookieRefreshed) {
-        clearLegacyRefreshToken();
-        return true;
-      }
- 
-      const legacyRefreshToken = getLegacyRefreshToken();
-      if (!legacyRefreshToken) return false;
-      const legacyRefreshed = await requestRefresh({ refresh_token: legacyRefreshToken });
-      if (legacyRefreshed) {
-        clearLegacyRefreshToken();
-        return true;
-      }
-      clearLegacyRefreshToken();
-      return false;
-    } catch {
-      return false;
-    } finally {
-      refreshPromise = null;
-    }
-  })();
-  return refreshPromise;
-}
- 
-async function request(path, opts = {}) {
-  const headers = opts.headers || {};
-  if (token) headers["Authorization"] = `Bearer ${token}`;
- 
-  if (opts.json) {
-    headers["Content-Type"] = "application/json";
-    opts.body = JSON.stringify(opts.json);
-    delete opts.json;
-  }
- 
-  const fetchOpts = { ...opts, headers };
-  if (path.startsWith("/auth/")) fetchOpts.credentials = "same-origin";
- 
-  try {
-    let r = await fetch(BASE + path, fetchOpts);
- 
-    if (r.status === 401 && !path.includes("/auth/refresh")) {
-      const refreshed = await tryRefreshSession();
-      if (refreshed) {
-        headers["Authorization"] = `Bearer ${token}`;
-        r = await fetch(BASE + path, fetchOpts);
-      }
-    }
- 
-    if (r.status === 401) {
-      setToken("");
-      throw new Error("Unauthorized");
-    }
- 
-    if (!r.ok) {
-      const error = new Error(`HTTP ${r.status}`);
-      error.status = r.status;
-      try { error.body = await r.json(); } catch {}
-      throw error;
-    }
-    return r.json();
-  } catch (e) {
-    console.error(`API ${opts.method || "GET"} ${path}:`, e);
-    throw e;
-  }
-}
- 
-export function get(path) {
-  return request(path);
-}
- 
-export function post(path, data) {
-  return request(path, { method: "POST", json: data });
-}
- 
-export function del(path) {
-  return request(path, { method: "DELETE" });
-}
- 
-export async function upload(path, file) {
-  const headers = {};
-  if (token) headers["Authorization"] = `Bearer ${token}`;
- 
-  const form = new FormData();
-  form.append("file", file);
- 
-  const r = await fetch(`${BASE}/files/upload?path=${encodeURIComponent(path)}`, {
-    method: "POST",
-    headers,
-    body: form,
-  });
- 
-  if (!r.ok) throw new Error(`HTTP ${r.status}`);
-  return r.json();
-}
- 
-export function login(creds) {
-  return request("/auth/login", { method: "POST", json: creds });
-}
- 
-export function checkAuth() {
-  return request("/auth/me");
-}
- 
-export function logout() {
-  return request("/auth/logout", { method: "POST" });
-}
- 
-export function getPreferences() {
-  return get("/auth/preferences");
-}
- 
-export function savePreferences(data) {
-  return request("/auth/preferences", { method: "PUT", json: data });
-}
- 
-export function getLiteLLMHealth() {
-  return get("/litellm/health");
-}
- 
-export function getCcConnectHealth() {
-  return get("/cc-connect/health");
-}
- 
-export function startCcConnect() {
-  return post("/cc-connect/start");
-}
- 
-export function stopCcConnect() {
-  return post("/cc-connect/stop");
-}
- 
-export function getInfoEngineItems(params = {}) {
-  const query = new URLSearchParams();
-  if (params.limit !== undefined) query.set("limit", String(params.limit));
-  if (params.offset !== undefined) query.set("offset", String(params.offset));
-  if (params.source) query.set("source", params.source);
-  if (params.tag) query.set("tag", params.tag);
-  if (params.since) query.set("since", params.since);
-  const suffix = query.toString() ? `?${query.toString()}` : "";
-  return get(`/info-engine/items${suffix}`);
-}
- 
-export function getInfoEngineItem(id) {
-  return get(`/info-engine/items/${id}`);
-}
- 
-export function put(path, data) {
-  return request(path, { method: "PUT", json: data });
-}
- 
-export async function download(path, filename) {
-  const headers = {};
-  if (token) headers["Authorization"] = `Bearer ${token}`;
- 
-  try {
-    // Get a temporary download token
-    const tokenResponse = await fetch(`${BASE}/files/download-token?path=${encodeURIComponent(path)}`, {
-      method: "POST",
-      headers,
-    });
- 
-    if (!tokenResponse.ok) {
-      const errorText = await tokenResponse.text();
-      console.error(`Download token failed: ${tokenResponse.status} - ${errorText}`);
-      throw new Error(`Download failed: ${tokenResponse.status}`);
-    }
- 
-    const { token: downloadToken } = await tokenResponse.json();
- 
-    // Use hidden iframe for download - avoids page navigation and popup blockers
-    const iframe = document.createElement('iframe');
-    iframe.style.display = 'none';
-    iframe.src = `${BASE}/files/download?token=${downloadToken}`;
-    document.body.appendChild(iframe);
- 
-    // Clean up iframe after download starts
-    setTimeout(() => {
-      document.body.removeChild(iframe);
-    }, 5000);
-  } catch (e) {
-    console.error("Download error:", e);
-    throw e;
-  }
-}
- -
-
- - - - - - - - \ No newline at end of file diff --git a/dashboard/frontend/coverage/src/lib/index.html b/dashboard/frontend/coverage/src/lib/index.html deleted file mode 100644 index acd6863..0000000 --- a/dashboard/frontend/coverage/src/lib/index.html +++ /dev/null @@ -1,161 +0,0 @@ - - - - - - Code coverage report for src/lib - - - - - - - - - -
-
-

All files src/lib

-
- -
- 22.06% - Statements - 124/562 -
- - -
- 84.78% - Branches - 39/46 -
- - -
- 51.72% - Functions - 15/29 -
- - -
- 22.06% - Lines - 124/562 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FileStatementsBranchesFunctionsLines
api.js -
-
64.92%124/19190.69%39/4357.69%15/2664.92%124/191
opc-api.js -
-
0%0/640%0/10%0/10%0/64
opc-ws.js -
-
0%0/750%0/10%0/10%0/75
voice.js -
-
0%0/2320%0/10%0/10%0/232
-
-
-
- - - - - - - - \ No newline at end of file diff --git a/dashboard/frontend/coverage/src/lib/opc-api.js.html b/dashboard/frontend/coverage/src/lib/opc-api.js.html deleted file mode 100644 index d9a456b..0000000 --- a/dashboard/frontend/coverage/src/lib/opc-api.js.html +++ /dev/null @@ -1,325 +0,0 @@ - - - - - - Code coverage report for src/lib/opc-api.js - - - - - - - - - -
-
-

All files / src/lib opc-api.js

-
- -
- 0% - Statements - 0/64 -
- - -
- 0% - Branches - 0/1 -
- - -
- 0% - Functions - 0/1 -
- - -
- 0% - Lines - 0/64 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  - 
/**
- * OPC API Client
- */
-import { get, post, put, del } from "./api.js";
- 
-// Tasks
-export async function getTasks(filters = {}) {
-  const params = new URLSearchParams();
-  if (filters.status) params.append("status", filters.status);
-  if (filters.assigned_to) params.append("assigned_to", filters.assigned_to);
-  if (filters.project_id) params.append("project_id", filters.project_id);
-  if (filters.priority) params.append("priority", filters.priority);
-  if (filters.tags) params.append("tags", filters.tags.join(","));
-  if (filters.limit) params.append("limit", filters.limit);
-  if (filters.offset) params.append("offset", filters.offset);
- 
-  const query = params.toString();
-  return get(`/opc/tasks${query ? "?" + query : ""}`);
-}
- 
-export async function createTask(task) {
-  return post("/opc/tasks", task);
-}
- 
-export async function updateTask(taskId, updates) {
-  return put(`/opc/tasks/${taskId}`, updates);
-}
- 
-export async function deleteTask(taskId) {
-  return del(`/opc/tasks/${taskId}`);
-}
- 
-export async function moveTask(taskId, status) {
-  return put(`/opc/tasks/${taskId}/move`, { status });
-}
- 
-export async function assignTask(taskId, assigned_to, assigned_type = "human") {
-  return post(`/opc/tasks/${taskId}/assign`, { assigned_to, assigned_type });
-}
- 
-export async function getTaskHistory(taskId) {
-  return get(`/opc/tasks/${taskId}/history`);
-}
- 
-export async function getTaskTime(taskId) {
-  return get(`/opc/tasks/${taskId}/time`);
-}
- 
-// Agents
-export async function getAgents(enabledOnly = true) {
-  return get(`/opc/agents?enabled_only=${enabledOnly}`);
-}
- 
-export async function getAgent(agentId) {
-  return get(`/opc/agents/${agentId}`);
-}
- 
-export async function triggerAgent(agentId, taskId) {
-  return post(`/opc/agents/${agentId}/execute?task_id=${taskId}`);
-}
- 
-// Executions
-export async function getExecutions(filters = {}) {
-  const params = new URLSearchParams();
-  if (filters.task_id) params.append("task_id", filters.task_id);
-  if (filters.agent_id) params.append("agent_id", filters.agent_id);
-  if (filters.status) params.append("status", filters.status);
-  if (filters.limit) params.append("limit", filters.limit);
- 
-  const query = params.toString();
-  return get(`/opc/executions${query ? "?" + query : ""}`);
-}
- 
-export async function getExecution(executionId) {
-  return get(`/opc/executions/${executionId}`);
-}
- 
-export async function approveExecution(executionId, approved) {
-  return post(`/opc/executions/${executionId}/approve`, { approved });
-}
- -
-
- - - - - - - - \ No newline at end of file diff --git a/dashboard/frontend/coverage/src/lib/opc-ws.js.html b/dashboard/frontend/coverage/src/lib/opc-ws.js.html deleted file mode 100644 index c01cf73..0000000 --- a/dashboard/frontend/coverage/src/lib/opc-ws.js.html +++ /dev/null @@ -1,352 +0,0 @@ - - - - - - Code coverage report for src/lib/opc-ws.js - - - - - - - - - -
-
-

All files / src/lib opc-ws.js

-
- -
- 0% - Statements - 0/75 -
- - -
- 0% - Branches - 0/1 -
- - -
- 0% - Functions - 0/1 -
- - -
- 0% - Lines - 0/75 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  - 
/**
- * OPC WebSocket Client - Real-time updates
- */
- 
-import { getToken } from "./api.js";
- 
-let ws = null;
-let reconnectTimer = null;
-let listeners = new Set();
- 
-export function connect() {
-  if (ws && ws.readyState === WebSocket.OPEN) {
-    return;
-  }
- 
-  const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
-  const token = getToken();
-  const tokenParam = token ? `?token=${encodeURIComponent(token)}` : "";
-  const wsUrl = `${protocol}//${window.location.host}/ws/opc${tokenParam}`;
- 
-  ws = new WebSocket(wsUrl);
- 
-  ws.onopen = () => {
-    console.log("OPC WebSocket connected");
-    if (reconnectTimer) {
-      clearTimeout(reconnectTimer);
-      reconnectTimer = null;
-    }
- 
-    // Send ping every 30 seconds to keep connection alive
-    const pingInterval = setInterval(() => {
-      if (ws && ws.readyState === WebSocket.OPEN) {
-        ws.send("ping");
-      } else {
-        clearInterval(pingInterval);
-      }
-    }, 30000);
-  };
- 
-  ws.onmessage = (event) => {
-    try {
-      const message = JSON.parse(event.data);
-      notifyListeners(message);
-    } catch (e) {
-      console.error("Failed to parse WebSocket message:", e);
-    }
-  };
- 
-  ws.onerror = (error) => {
-    console.error("WebSocket error:", error);
-  };
- 
-  ws.onclose = () => {
-    console.log("OPC WebSocket disconnected");
-    ws = null;
- 
-    // Reconnect after 5 seconds
-    reconnectTimer = setTimeout(() => {
-      console.log("Reconnecting WebSocket...");
-      connect();
-    }, 5000);
-  };
-}
- 
-export function disconnect() {
-  if (ws) {
-    ws.close();
-    ws = null;
-  }
-  if (reconnectTimer) {
-    clearTimeout(reconnectTimer);
-    reconnectTimer = null;
-  }
-}
- 
-export function subscribe(callback) {
-  listeners.add(callback);
-  return () => listeners.delete(callback);
-}
- 
-function notifyListeners(message) {
-  listeners.forEach((callback) => {
-    try {
-      callback(message);
-    } catch (e) {
-      console.error("Listener error:", e);
-    }
-  });
-}
- -
-
- - - - - - - - \ No newline at end of file diff --git a/dashboard/frontend/coverage/src/lib/voice.js.html b/dashboard/frontend/coverage/src/lib/voice.js.html deleted file mode 100644 index 22d40a3..0000000 --- a/dashboard/frontend/coverage/src/lib/voice.js.html +++ /dev/null @@ -1,847 +0,0 @@ - - - - - - Code coverage report for src/lib/voice.js - - - - - - - - - -
-
-

All files / src/lib voice.js

-
- -
- 0% - Statements - 0/232 -
- - -
- 0% - Branches - 0/1 -
- - -
- 0% - Functions - 0/1 -
- - -
- 0% - Lines - 0/232 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -178 -179 -180 -181 -182 -183 -184 -185 -186 -187 -188 -189 -190 -191 -192 -193 -194 -195 -196 -197 -198 -199 -200 -201 -202 -203 -204 -205 -206 -207 -208 -209 -210 -211 -212 -213 -214 -215 -216 -217 -218 -219 -220 -221 -222 -223 -224 -225 -226 -227 -228 -229 -230 -231 -232 -233 -234 -235 -236 -237 -238 -239 -240 -241 -242 -243 -244 -245 -246 -247 -248 -249 -250 -251 -252 -253 -254 -255  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  - 
const LANGS = ["en-US", "zh-CN", "zh-HK"];
-const LANG_LABELS = ["EN", "普", "粤"];
-const TALK_MODES = ["auto", "tap", "hold"];
-const MODE_LABELS = { auto: "Auto", tap: "Tap", hold: "Hold" };
-const NOISE = /^[\s│┃┆┇┊┋─━┄┅┈┉═╌╍╔╗╚╝╠╣╦╩╬├┤┬┴┼╭╮╯╰░▒▓█▌▐▀▄⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏◐◑◒◓⣾⣽⣻⢿⡿⣟⣯⣷⠁⠂⠄⡀⢀⠠⠐⠈\-–—_=+*#~<>/.\\|,;:!?(){}\[\]]+$/;
-const SKIP = /shortcuts|\/help|\/exit|press esc|ctrl[+-]|⌘|⎋|❯|^\$\s|^root@|^claude|^jimmy|^\[.*\]$/i;
-const MODE_COMMANDS = {
-  "hold mode": "hold", "tap mode": "tap", "auto mode": "auto",
-  "按住模式": "hold", "点击模式": "tap", "自动模式": "auto",
-};
- 
-export class VoiceSession {
-  constructor(onChange) {
-    this.onChange = onChange;
-    this.ttsEnabled = false;
-    this.speaking = false;
-    this.listening = false;
-    this.sttAvailable = !!(window.SpeechRecognition || window.webkitSpeechRecognition);
-    this.langIdx = 0;
-    this.voiceMode = false;
-    this.transcript = "";
-    this.talkMode = "auto";
-    this.voices = [];
-    this.voiceIdx = -1;
-    this.term = null;
-    this.ws = null;
-    this._lastLine = 0;
-    this._debounce = null;
-    this._recog = null;
-    this._onWriteDispose = null;
-    this._ttsUnlocked = false;
-    this._silenceTimer = null;
-  }
- 
-  get lang() { return LANGS[this.langIdx]; }
-  get langLabel() { return LANG_LABELS[this.langIdx]; }
-  get modeLabel() { return MODE_LABELS[this.talkMode]; }
-  get voiceName() { return this.voices[this.voiceIdx]?.name || "Default"; }
- 
-  loadVoices() {
-    const all = speechSynthesis.getVoices();
-    const prefix = this.lang.split("-")[0];
-    this.voices = all.filter(v => v.lang.startsWith(prefix));
-    // Try to keep current selection
-    if (this.voiceIdx >= this.voices.length) this.voiceIdx = 0;
-    if (!this.voices.length) this.voiceIdx = -1;
-    this._notify();
-  }
- 
-  setVoice(idx) {
-    this.voiceIdx = idx;
-    this._notify();
-  }
- 
-  attach(term, ws) {
-    this.term = term;
-    this.ws = ws;
-    this._lastLine = term.buffer.active.cursorY + term.buffer.active.baseY;
-    this._onWriteDispose = term.onWriteParsed(() => this._onOutput());
-  }
- 
-  _notify() { this.onChange?.(); }
- 
-  // --- Voice Mode ---
-  enterVoiceMode() {
-    this.voiceMode = true;
-    this.ttsEnabled = true;
-    if (!this._ttsUnlocked) {
-      speechSynthesis.speak(new SpeechSynthesisUtterance(""));
-      this._ttsUnlocked = true;
-    }
-    this.loadVoices();
-    speechSynthesis.onvoiceschanged = () => this.loadVoices();
-    if (this.term) {
-      this._lastLine = this.term.buffer.active.baseY + this.term.buffer.active.cursorY;
-    }
-    if (this.talkMode === "auto") this._startSTT(true);
-    this._notify();
-  }
- 
-  exitVoiceMode() {
-    this.voiceMode = false;
-    this.ttsEnabled = false;
-    this.transcript = "";
-    speechSynthesis.cancel();
-    this.speaking = false;
-    this._stopSTT();
-    this._notify();
-  }
- 
-  setTalkMode(mode) {
-    this.talkMode = mode;
-    this._stopSTT();
-    this.transcript = "";
-    if (mode === "auto" && this.voiceMode) this._startSTT(true);
-    this._notify();
-  }
- 
-  cycleTalkMode() {
-    const i = (TALK_MODES.indexOf(this.talkMode) + 1) % TALK_MODES.length;
-    this.setTalkMode(TALK_MODES[i]);
-  }
- 
-  // Hold mode
-  startHold() {
-    if (this.talkMode !== "hold") return;
-    this._startSTT(false);
-  }
- 
-  releaseHold() {
-    if (this.talkMode !== "hold") return;
-    this._sendAndStop();
-  }
- 
-  // Tap mode
-  tapMic() {
-    if (this.talkMode !== "tap") return;
-    if (this.listening) { this._sendAndStop(); }
-    else { this._startSTT(false); }
-  }
- 
-  _sendAndStop() {
-    const text = this.transcript.trim();
-    this._stopSTT();
-    if (text && this.ws?.readyState === 1) this._typeText(text + "\r");
-    this.transcript = "";
-    this._notify();
-  }
- 
-  // --- STT ---
-  _startSTT(continuous) {
-    const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
-    if (!SR) return;
-    speechSynthesis.cancel();
-    this.speaking = false;
-    this._recog = new SR();
-    this._recog.continuous = continuous;
-    this._recog.interimResults = true;
-    this._recog.lang = this.lang;
-    this._finalText = "";
-    this._recog.onresult = (e) => {
-      let interim = "";
-      for (let i = e.resultIndex; i < e.results.length; i++) {
-        const t = e.results[i][0].transcript;
-        if (e.results[i].isFinal) {
-          // Check voice commands
-          const cmd = MODE_COMMANDS[t.trim().toLowerCase()];
-          if (cmd) { this.setTalkMode(cmd); return; }
-          this._finalText += t;
-        } else { interim = t; }
-      }
-      this.transcript = (this._finalText + interim).trim();
-      this._notify();
-      // Auto mode: reset silence timer on new results
-      if (this.talkMode === "auto" && this._finalText) {
-        clearTimeout(this._silenceTimer);
-        this._silenceTimer = setTimeout(() => this._autoSend(), 2000);
-      }
-    };
-    this._recog.onerror = () => { this.listening = false; this._notify(); };
-    this._recog.onend = () => {
-      if (this.listening && continuous) {
-        // Auto mode: send accumulated text, restart
-        if (this.talkMode === "auto" && this._finalText.trim()) this._autoSend();
-        try { this._recog.start(); } catch { this.listening = false; this._notify(); }
-      } else { this.listening = false; this._notify(); }
-    };
-    this._recog.start();
-    this.listening = true;
-    this._notify();
-  }
- 
-  _stopSTT() {
-    this.listening = false;
-    clearTimeout(this._silenceTimer);
-    this._recog?.stop();
-    this._recog = null;
-    this._finalText = "";
-  }
- 
-  _autoSend() {
-    clearTimeout(this._silenceTimer);
-    const text = this._finalText.trim();
-    this._finalText = "";
-    this.transcript = "";
-    if (text && this.ws?.readyState === 1) this._typeText(text + "\r");
-    this._notify();
-  }
- 
-  // --- TTS ---
-  _onOutput() {
-    if (!this.ttsEnabled || this.transcript) return;
-    clearTimeout(this._debounce);
-    this._debounce = setTimeout(() => this._readBuffer(), 2000);
-  }
- 
-  _readBuffer() {
-    const buf = this.term?.buffer?.active;
-    if (!buf || buf.type !== "normal") return;
-    const curLine = buf.baseY + buf.cursorY;
-    const lines = [];
-    for (let y = this._lastLine + 1; y <= curLine; y++) {
-      const row = buf.getLine(y);
-      if (!row) continue;
-      const text = row.translateToString(true).trim();
-      if (text.length >= 4 && !NOISE.test(text) && !SKIP.test(text)) lines.push(text);
-    }
-    this._lastLine = curLine;
-    if (lines.length) this._speak(lines.join(". "));
-  }
- 
-  _speak(text) {
-    if (!text) return;
-    speechSynthesis.cancel();
-    const u = new SpeechSynthesisUtterance(text);
-    u.lang = this.lang;
-    if (this.voices[this.voiceIdx]) u.voice = this.voices[this.voiceIdx];
-    u.onstart = () => { this.speaking = true; this._notify(); };
-    u.onend = () => { this.speaking = false; this._notify(); };
-    u.onerror = () => { this.speaking = false; this._notify(); };
-    speechSynthesis.speak(u);
-  }
- 
-  _typeText(text) {
-    let i = 0;
-    const next = () => {
-      if (i < text.length && this.ws?.readyState === 1) {
-        this.ws.send(new TextEncoder().encode(text[i++]));
-        setTimeout(next, 20);
-      }
-    };
-    next();
-  }
- 
-  cycleLang() {
-    this.langIdx = (this.langIdx + 1) % LANGS.length;
-    this.loadVoices();
-    if (this.listening) {
-      const wasContinuous = this._recog?.continuous;
-      this._stopSTT();
-      this._startSTT(!!wasContinuous);
-    }
-    this._notify();
-  }
- 
-  destroy() {
-    clearTimeout(this._debounce);
-    clearTimeout(this._silenceTimer);
-    this._onWriteDispose?.dispose();
-    this._stopSTT();
-    speechSynthesis.cancel();
-    this.term = null;
-    this.ws = null;
-  }
-}
- -
-
- - - - - - - - \ No newline at end of file diff --git a/dashboard/frontend/coverage/src/main.js.html b/dashboard/frontend/coverage/src/main.js.html deleted file mode 100644 index 3761d6b..0000000 --- a/dashboard/frontend/coverage/src/main.js.html +++ /dev/null @@ -1,103 +0,0 @@ - - - - - - Code coverage report for src/main.js - - - - - - - - - -
-
-

All files / src main.js

-
- -
- 0% - Statements - 0/5 -
- - -
- 0% - Branches - 0/1 -
- - -
- 0% - Functions - 0/1 -
- - -
- 0% - Lines - 0/5 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7  -  -  -  -  -  - 
import "./app.css";
-import App from "./App.svelte";
-import { mount } from "svelte";
- 
-const app = mount(App, { target: document.getElementById("app") });
-export default app;
- -
-
- - - - - - - - \ No newline at end of file diff --git a/dashboard/frontend/coverage/src/routes/CcConnect.svelte.html b/dashboard/frontend/coverage/src/routes/CcConnect.svelte.html deleted file mode 100644 index 413f0f2..0000000 --- a/dashboard/frontend/coverage/src/routes/CcConnect.svelte.html +++ /dev/null @@ -1,550 +0,0 @@ - - - - - - Code coverage report for src/routes/CcConnect.svelte - - - - - - - - - -
-
-

All files / src/routes CcConnect.svelte

-
- -
- 0% - Statements - 0/87 -
- - -
- 0% - Branches - 0/1 -
- - -
- 0% - Functions - 0/1 -
- - -
- 0% - Lines - 0/87 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  - 
<script>
-  import { onMount } from "svelte";
-  import { getCcConnectHealth, startCcConnect, stopCcConnect } from "../lib/api.js";
- 
-  let loading = $state(true);
-  let health = $state(null);
-  let requestError = $state("");
-  let actionLoading = $state("");
- 
-  async function loadHealth() {
-    loading = true;
-    requestError = "";
-    try {
-      health = await getCcConnectHealth();
-    } catch (e) {
-      requestError = e?.body?.detail || e?.message || "Failed to load cc-connect health";
-      health = null;
-    } finally {
-      loading = false;
-    }
-  }
- 
-  async function runAction(action) {
-    actionLoading = action;
-    requestError = "";
-    try {
-      const result = action === "start" ? await startCcConnect() : await stopCcConnect();
-      if (!result?.ok) {
-        requestError = result?.error || `Failed to ${action} cc-connect`;
-      }
-    } catch (e) {
-      requestError = e?.body?.detail || e?.message || `Failed to ${action} cc-connect`;
-    } finally {
-      actionLoading = "";
-      await loadHealth();
-    }
-  }
- 
-  function statusBadgeClass(status) {
-    if (status === "up") {
-      return "bg-emerald-100 text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-300";
-    }
-    return "bg-rose-100 text-rose-700 dark:bg-rose-900/40 dark:text-rose-300";
-  }
- 
-  function valueOrDash(value) {
-    return value === null || value === undefined || value === "" ? "—" : value;
-  }
- 
-  onMount(loadHealth);
-</script>
- 
-<div class="space-y-6">
-  <div class="flex items-center justify-between">
-    <div>
-      <h1 class="text-2xl font-bold text-surface-900 dark:text-white tracking-tight">cc-connect</h1>
-      <p class="text-sm text-surface-400 mt-1">Mobile bridge operational status</p>
-      <div class="mt-2 flex flex-wrap items-center gap-2 text-[11px] font-medium">
-        <span class="px-2 py-0.5 rounded-full bg-emerald-100 text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-300">up</span>
-        <span class="px-2 py-0.5 rounded-full bg-rose-100 text-rose-700 dark:bg-rose-900/40 dark:text-rose-300">down</span>
-      </div>
-    </div>
-    <div class="flex items-center gap-2">
-      {#if health?.container_status === "running"}
-        <button
-          onclick={() => runAction("stop")}
-          disabled={loading || actionLoading === "stop"}
-          class="px-3 py-1.5 text-xs font-medium text-rose-600 bg-rose-50 hover:bg-rose-100 rounded-lg transition-colors disabled:opacity-50"
-        >
-          {actionLoading === "stop" ? "Stopping..." : "Stop"}
-        </button>
-      {:else}
-        <button
-          onclick={() => runAction("start")}
-          disabled={loading || actionLoading === "start"}
-          class="px-3 py-1.5 text-xs font-medium text-emerald-600 bg-emerald-50 hover:bg-emerald-100 rounded-lg transition-colors disabled:opacity-50"
-        >
-          {actionLoading === "start" ? "Starting..." : "Start"}
-        </button>
-      {/if}
-      <button onclick={loadHealth} disabled={loading || Boolean(actionLoading)} class="px-3 py-1.5 text-xs font-medium text-primary-600 bg-primary-50 hover:bg-primary-100 rounded-lg transition-colors disabled:opacity-50 dark:bg-primary-900/30 dark:text-primary-300">
-        {loading ? "Loading..." : "Refresh"}
-      </button>
-    </div>
-  </div>
- 
-  {#if requestError}
-    <div class="bg-rose-50 dark:bg-rose-900/20 border border-rose-200 dark:border-rose-700 rounded-xl p-4 shadow-sm">
-      <p class="text-sm font-medium text-rose-700 dark:text-rose-300">cc-connect action failed</p>
-      <p class="text-xs text-rose-600 dark:text-rose-400 mt-1">{requestError}</p>
-    </div>
-  {/if}
- 
-  <div class="bg-white dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 p-5 shadow-sm">
-    {#if loading && !health}
-      <div class="space-y-3">
-        <div class="h-6 w-36 rounded bg-surface-100 dark:bg-surface-700 animate-pulse"></div>
-        <div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
-          {#each Array(8) as _}
-            <div class="h-16 rounded-lg bg-surface-100 dark:bg-surface-700 animate-pulse"></div>
-          {/each}
-        </div>
-      </div>
-    {:else if health}
-      <div class="flex items-center justify-between mb-4">
-        <h3 class="text-sm font-semibold text-surface-700 dark:text-surface-200">Health Status</h3>
-        <span class="px-2.5 py-1 text-xs font-semibold rounded-full {statusBadgeClass(health.status)}">{valueOrDash(health.status)}</span>
-      </div>
- 
-      <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
-        <div class="rounded-lg border border-surface-200 dark:border-surface-700 p-3">
-          <p class="text-xs font-medium text-surface-400 uppercase tracking-wider">OK</p>
-          <p class="text-sm font-semibold text-surface-800 dark:text-surface-100 mt-1">{String(Boolean(health.ok))}</p>
-        </div>
- 
-        <div class="rounded-lg border border-surface-200 dark:border-surface-700 p-3">
-          <p class="text-xs font-medium text-surface-400 uppercase tracking-wider">Source</p>
-          <p class="text-sm font-semibold text-surface-800 dark:text-surface-100 mt-1">{valueOrDash(health.source)}</p>
-        </div>
- 
-        <div class="rounded-lg border border-surface-200 dark:border-surface-700 p-3">
-          <p class="text-xs font-medium text-surface-400 uppercase tracking-wider">Container</p>
-          <p class="text-sm font-semibold text-surface-800 dark:text-surface-100 mt-1">{valueOrDash(health.container_name)}</p>
-        </div>
- 
-        <div class="rounded-lg border border-surface-200 dark:border-surface-700 p-3">
-          <p class="text-xs font-medium text-surface-400 uppercase tracking-wider">Container Status</p>
-          <p class="text-sm font-semibold text-surface-800 dark:text-surface-100 mt-1">{valueOrDash(health.container_status)}</p>
-        </div>
- 
-        <div class="rounded-lg border border-surface-200 dark:border-surface-700 p-3">
-          <p class="text-xs font-medium text-surface-400 uppercase tracking-wider">Endpoint</p>
-          <p class="text-sm font-semibold text-surface-800 dark:text-surface-100 mt-1 break-all">{valueOrDash(health.endpoint)}</p>
-        </div>
- 
-        <div class="rounded-lg border border-surface-200 dark:border-surface-700 p-3">
-          <p class="text-xs font-medium text-surface-400 uppercase tracking-wider">HTTP Status</p>
-          <p class="text-sm font-semibold text-surface-800 dark:text-surface-100 mt-1">{valueOrDash(health.http_status)}</p>
-        </div>
- 
-        <div class="rounded-lg border border-surface-200 dark:border-surface-700 p-3">
-          <p class="text-xs font-medium text-surface-400 uppercase tracking-wider">Latency</p>
-          <p class="text-sm font-semibold text-surface-800 dark:text-surface-100 mt-1">{health.latency_ms === null || health.latency_ms === undefined ? "—" : `${health.latency_ms} ms`}</p>
-        </div>
- 
-        <div class="rounded-lg border border-surface-200 dark:border-surface-700 p-3 sm:col-span-2 lg:col-span-2">
-          <p class="text-xs font-medium text-surface-400 uppercase tracking-wider">Error</p>
-          <p class="text-sm font-semibold text-surface-800 dark:text-surface-100 mt-1 break-all">{valueOrDash(health.error)}</p>
-        </div>
-      </div>
-    {:else}
-      <p class="text-sm text-surface-400">No health data available.</p>
-    {/if}
-  </div>
-</div>
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/dashboard/frontend/coverage/src/routes/ChatSummary.svelte.html b/dashboard/frontend/coverage/src/routes/ChatSummary.svelte.html deleted file mode 100644 index f5032c4..0000000 --- a/dashboard/frontend/coverage/src/routes/ChatSummary.svelte.html +++ /dev/null @@ -1,454 +0,0 @@ - - - - - - Code coverage report for src/routes/ChatSummary.svelte - - - - - - - - - -
-
-

All files / src/routes ChatSummary.svelte

-
- -
- 0% - Statements - 0/91 -
- - -
- 0% - Branches - 0/1 -
- - -
- 0% - Functions - 0/1 -
- - -
- 0% - Lines - 0/91 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  - 
<script>
-  import { onMount } from "svelte";
-  import { get, post } from "../lib/api.js";
- 
-  let dates = $state([]);
-  let selectedDate = $state("");
-  let summary = $state(null);
-  let messages = $state([]);
-  let showMessages = $state(false);
-  let loading = $state(false);
-  let triggering = $state(false);
-  let error = $state("");
- 
-  onMount(async () => {
-    try {
-      dates = await get("/api/chat-summary/dates");
-      if (dates.length) selectDate(dates[0]);
-    } catch (e) {
-      error = e.body?.detail || "Failed to load dates";
-    }
-  });
- 
-  async function selectDate(d) {
-    selectedDate = d;
-    showMessages = false;
-    loading = true;
-    error = "";
-    try {
-      summary = await get(`/api/chat-summary/summary/${d}`);
-    } catch (e) {
-      summary = null;
-      if (e.status !== 404) error = e.body?.detail || "Failed to load summary";
-    } finally {
-      loading = false;
-    }
-  }
- 
-  async function loadMessages() {
-    if (showMessages) { showMessages = false; return; }
-    try {
-      messages = await get(`/api/chat-summary/messages/${selectedDate}`);
-      showMessages = true;
-    } catch (e) {
-      error = "Failed to load messages";
-    }
-  }
- 
-  async function trigger() {
-    triggering = true;
-    try {
-      await post("/api/chat-summary/trigger");
-    } catch (e) {
-      error = "Trigger failed";
-    } finally {
-      triggering = false;
-    }
-  }
- 
-  function markdownToHtml(md) {
-    if (!md) return "";
-    return md
-      .replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")
-      .replace(/^### (.+)$/gm, '<h3 class="text-lg font-semibold mt-4 mb-2">$1</h3>')
-      .replace(/^## (.+)$/gm, '<h2 class="text-xl font-bold mt-5 mb-2">$1</h2>')
-      .replace(/^# (.+)$/gm, '<h1 class="text-2xl font-bold mt-6 mb-3">$1</h1>')
-      .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
-      .replace(/\*(.+?)\*/g, '<em>$1</em>')
-      .replace(/^- (.+)$/gm, '<li class="ml-4">$1</li>')
-      .replace(/\n{2,}/g, '<br><br>')
-      .replace(/\n/g, '<br>');
-  }
-</script>
- 
-<div>
-  <div class="flex items-center justify-between mb-6">
-    <h2 class="text-xl font-bold text-surface-800 dark:text-surface-100">Chat Digest</h2>
-    <button onclick={trigger} disabled={triggering}
-      class="px-3 py-1.5 text-sm font-medium rounded-lg bg-primary-600 text-white hover:bg-primary-700 disabled:opacity-50">
-      {triggering ? "Generating..." : "Generate Now"}
-    </button>
-  </div>
- 
-  {#if error}
-    <div class="mb-4 p-3 rounded-lg bg-red-50 text-red-700 text-sm dark:bg-red-900/20 dark:text-red-400">{error}</div>
-  {/if}
- 
-  <div class="flex gap-2 mb-6 flex-wrap">
-    {#each dates as d}
-      <button onclick={() => selectDate(d)}
-        class="px-3 py-1.5 text-sm rounded-lg transition-colors {selectedDate === d ? 'bg-primary-600 text-white' : 'bg-surface-100 text-surface-600 hover:bg-surface-200 dark:bg-surface-700 dark:text-surface-300 dark:hover:bg-surface-600'}">
-        {d}
-      </button>
-    {/each}
-    {#if !dates.length && !error}
-      <p class="text-sm text-surface-400">No summaries yet.</p>
-    {/if}
-  </div>
- 
-  {#if loading}
-    <div class="flex justify-center py-12"><div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600"></div></div>
-  {:else if summary}
-    <div class="prose dark:prose-invert max-w-none bg-white dark:bg-surface-800 rounded-xl p-6 shadow-sm border border-surface-200 dark:border-surface-700">
-      {@html markdownToHtml(summary.content)}
-    </div>
- 
-    <button onclick={loadMessages}
-      class="mt-4 text-sm text-primary-600 hover:text-primary-700 dark:text-primary-400">
-      {showMessages ? "Hide raw messages" : "Show raw messages"}
-    </button>
- 
-    {#if showMessages}
-      <div class="mt-3 bg-white dark:bg-surface-800 rounded-xl p-4 shadow-sm border border-surface-200 dark:border-surface-700 max-h-96 overflow-y-auto">
-        {#each messages as m}
-          <div class="py-1.5 border-b border-surface-100 dark:border-surface-700 last:border-0 text-sm">
-            <span class="text-surface-400 text-xs">{m.time?.slice(11,16)}</span>
-            <span class="font-medium text-primary-600 dark:text-primary-400 ml-1">[{m.group}] {m.sender}:</span>
-            <span class="text-surface-700 dark:text-surface-300 ml-1">{m.text}</span>
-          </div>
-        {/each}
-      </div>
-    {/if}
-  {/if}
-</div>
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/dashboard/frontend/coverage/src/routes/Conversations.svelte.html b/dashboard/frontend/coverage/src/routes/Conversations.svelte.html deleted file mode 100644 index e00af43..0000000 --- a/dashboard/frontend/coverage/src/routes/Conversations.svelte.html +++ /dev/null @@ -1,1003 +0,0 @@ - - - - - - Code coverage report for src/routes/Conversations.svelte - - - - - - - - - -
-
-

All files / src/routes Conversations.svelte

-
- -
- 0% - Statements - 0/201 -
- - -
- 0% - Branches - 0/1 -
- - -
- 0% - Functions - 0/1 -
- - -
- 0% - Lines - 0/201 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -178 -179 -180 -181 -182 -183 -184 -185 -186 -187 -188 -189 -190 -191 -192 -193 -194 -195 -196 -197 -198 -199 -200 -201 -202 -203 -204 -205 -206 -207 -208 -209 -210 -211 -212 -213 -214 -215 -216 -217 -218 -219 -220 -221 -222 -223 -224 -225 -226 -227 -228 -229 -230 -231 -232 -233 -234 -235 -236 -237 -238 -239 -240 -241 -242 -243 -244 -245 -246 -247 -248 -249 -250 -251 -252 -253 -254 -255 -256 -257 -258 -259 -260 -261 -262 -263 -264 -265 -266 -267 -268 -269 -270 -271 -272 -273 -274 -275 -276 -277 -278 -279 -280 -281 -282 -283 -284 -285 -286 -287 -288 -289 -290 -291 -292 -293 -294 -295 -296 -297 -298 -299 -300 -301 -302 -303 -304 -305 -306 -307  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  - 
<script>
-  import { onMount } from "svelte";
-  import { get, post } from "../lib/api.js";
- 
-  let dates = $state([]);
-  let selectedDate = $state("");
-  let summary = $state(null);
-  let conversations = $state([]);
-  let selectedConversation = $state(null);
-  let messages = $state([]);
-  let searchQuery = $state("");
-  let searchResults = $state([]);
-  let stats = $state(null);
-  let loading = $state(false);
-  let triggering = $state(false);
-  let error = $state("");
-  let view = $state("summary"); // summary, list, detail, search, stats
- 
-  onMount(async () => {
-    try {
-      dates = await get("/conversations/dates");
-      if (dates.length) selectDate(dates[0]);
-      loadStats();
-    } catch (e) {
-      error = e.body?.detail || "Failed to load dates";
-    }
-  });
- 
-  async function selectDate(d) {
-    selectedDate = d;
-    view = "summary";
-    loading = true;
-    error = "";
-    try {
-      summary = await get(`/conversations/summary/${d}`);
-      conversations = await get(`/conversations/list?date=${d}`);
-    } catch (e) {
-      summary = null;
-      if (e.status !== 404) error = e.body?.detail || "Failed to load summary";
-    } finally {
-      loading = false;
-    }
-  }
- 
-  async function selectConversation(sessionId) {
-    view = "detail";
-    loading = true;
-    try {
-      selectedConversation = await get(`/conversations/${sessionId}`);
-      messages = await get(`/conversations/${sessionId}/messages`);
-    } catch (e) {
-      error = "Failed to load conversation";
-    } finally {
-      loading = false;
-    }
-  }
- 
-  async function search() {
-    if (!searchQuery || searchQuery.length < 2) return;
-    view = "search";
-    loading = true;
-    try {
-      searchResults = await get(`/conversations/search?q=${encodeURIComponent(searchQuery)}`);
-    } catch (e) {
-      error = "Search failed";
-    } finally {
-      loading = false;
-    }
-  }
- 
-  async function loadStats() {
-    try {
-      stats = await get("/conversations/stats");
-    } catch (e) {
-      console.error("Failed to load stats", e);
-    }
-  }
- 
-  async function trigger() {
-    triggering = true;
-    try {
-      await post("/conversations/trigger");
-      setTimeout(() => selectDate(selectedDate), 5000);
-    } catch (e) {
-      error = "Trigger failed";
-    } finally {
-      triggering = false;
-    }
-  }
- 
-  function markdownToHtml(md) {
-    if (!md) return "";
-    return md
-      .replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")
-      .replace(/^### (.+)$/gm, '<h3 class="text-lg font-semibold mt-4 mb-2">$1</h3>')
-      .replace(/^## (.+)$/gm, '<h2 class="text-xl font-bold mt-5 mb-2">$1</h2>')
-      .replace(/^# (.+)$/gm, '<h1 class="text-2xl font-bold mt-6 mb-3">$1</h1>')
-      .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
-      .replace(/\*(.+?)\*/g, '<em>$1</em>')
-      .replace(/^- (.+)$/gm, '<li class="ml-4">$1</li>')
-      .replace(/\n{2,}/g, '<br><br>')
-      .replace(/\n/g, '<br>');
-  }
- 
-  function formatTokens(n) {
-    return n ? n.toLocaleString() : "0";
-  }
-</script>
- 
-<div>
-  <div class="flex items-center justify-between mb-6">
-    <h2 class="text-xl font-bold text-surface-800 dark:text-surface-100">Code Sessions</h2>
-    <div class="flex gap-2">
-      <button onclick={() => view = "stats"} class="px-3 py-1.5 text-sm font-medium rounded-lg bg-surface-100 hover:bg-surface-200 dark:bg-surface-700 dark:hover:bg-surface-600 transition-colors">
-        Stats
-      </button>
-      <button onclick={trigger} disabled={triggering}
-        class="px-3 py-1.5 text-sm font-medium rounded-lg bg-primary-600 text-white hover:bg-primary-700 disabled:opacity-50">
-        {triggering ? "Generating..." : "Generate Summary"}
-      </button>
-    </div>
-  </div>
- 
-  {#if error}
-    <div class="mb-4 p-3 rounded-lg bg-red-50 text-red-700 text-sm dark:bg-red-900/20 dark:text-red-400">{error}</div>
-  {/if}
- 
-  <!-- Search Bar -->
-  <div class="mb-4">
-    <input
-      type="text"
-      bind:value={searchQuery}
-      onkeydown={(e) => e.key === "Enter" && search()}
-      placeholder="Search conversations..."
-      class="w-full px-4 py-2 text-sm border border-surface-200 dark:border-surface-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 dark:bg-surface-700 dark:text-white"
-    />
-  </div>
- 
-  <!-- Date Selector -->
-  {#if view !== "search" && view !== "stats"}
-    <div class="flex gap-2 mb-6 flex-wrap">
-      {#each dates as d}
-        <button onclick={() => selectDate(d)}
-          class="px-3 py-1.5 text-sm rounded-lg transition-colors {selectedDate === d ? 'bg-primary-600 text-white' : 'bg-surface-100 text-surface-600 hover:bg-surface-200 dark:bg-surface-700 dark:text-surface-300 dark:hover:bg-surface-600'}">
-          {d}
-        </button>
-      {/each}
-      {#if !dates.length && !error}
-        <p class="text-sm text-surface-400">No conversations yet.</p>
-      {/if}
-    </div>
-  {/if}
- 
-  {#if loading}
-    <div class="flex justify-center py-12"><div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600"></div></div>
-  {:else if view === "summary" && summary}
-    <!-- Summary View -->
-    <div class="mb-4 grid grid-cols-4 gap-3">
-      <div class="bg-white dark:bg-surface-800 rounded-lg p-3 border border-surface-200 dark:border-surface-700">
-        <div class="text-xs text-surface-400">Conversations</div>
-        <div class="text-xl font-bold text-surface-900 dark:text-white">{summary.conversation_count}</div>
-      </div>
-      <div class="bg-white dark:bg-surface-800 rounded-lg p-3 border border-surface-200 dark:border-surface-700">
-        <div class="text-xs text-surface-400">Messages</div>
-        <div class="text-xl font-bold text-surface-900 dark:text-white">{summary.total_messages}</div>
-      </div>
-      <div class="bg-white dark:bg-surface-800 rounded-lg p-3 border border-surface-200 dark:border-surface-700">
-        <div class="text-xs text-surface-400">Tokens</div>
-        <div class="text-xl font-bold text-surface-900 dark:text-white">{formatTokens(summary.total_tokens)}</div>
-      </div>
-      <div class="bg-white dark:bg-surface-800 rounded-lg p-3 border border-surface-200 dark:border-surface-700">
-        <div class="text-xs text-surface-400">Projects</div>
-        <div class="text-sm font-medium text-surface-900 dark:text-white truncate">{summary.projects?.split(',')[0] || 'N/A'}</div>
-      </div>
-    </div>
- 
-    <div class="prose dark:prose-invert max-w-none bg-white dark:bg-surface-800 rounded-xl p-6 shadow-sm border border-surface-200 dark:border-surface-700 mb-4">
-      {@html markdownToHtml(summary.content)}
-    </div>
- 
-    <!-- Conversation List -->
-    <div class="bg-white dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 shadow-sm overflow-hidden">
-      <div class="px-4 py-2 text-xs font-semibold uppercase tracking-wider text-surface-400 border-b border-surface-100 dark:border-surface-700">
-        Conversations ({conversations.length})
-      </div>
-      {#each conversations as conv}
-        <button
-          onclick={() => selectConversation(conv.session_id)}
-          class="w-full px-4 py-3 hover:bg-surface-50 dark:hover:bg-surface-700 border-b border-surface-100 dark:border-surface-700 last:border-0 transition-colors text-left">
-          <div class="flex items-center justify-between">
-            <div class="flex-1 min-w-0">
-              <div class="text-sm font-medium text-surface-700 dark:text-surface-200 truncate">{conv.slug || 'Untitled'}</div>
-              <div class="text-xs text-surface-400 truncate">{conv.project_path?.split('/').pop() || 'unknown'}</div>
-            </div>
-            <div class="flex items-center gap-3 text-xs text-surface-400">
-              <span>{conv.message_count} msgs</span>
-              <span>{formatTokens(conv.total_tokens)} tokens</span>
-            </div>
-          </div>
-        </button>
-      {/each}
-    </div>
- 
-  {:else if view === "detail" && selectedConversation}
-    <!-- Conversation Detail -->
-    <button onclick={() => view = "summary"} class="mb-4 text-sm text-primary-600 hover:text-primary-700">← Back to summary</button>
- 
-    <div class="bg-white dark:bg-surface-800 rounded-xl p-4 border border-surface-200 dark:border-surface-700 mb-4">
-      <h3 class="text-lg font-bold text-surface-900 dark:text-white mb-2">{selectedConversation.slug || 'Untitled'}</h3>
-      <div class="grid grid-cols-2 gap-2 text-sm">
-        <div><span class="text-surface-400">Project:</span> <span class="text-surface-700 dark:text-surface-300">{selectedConversation.project_path?.split('/').pop()}</span></div>
-        <div><span class="text-surface-400">Branch:</span> <span class="text-surface-700 dark:text-surface-300">{selectedConversation.git_branch || 'N/A'}</span></div>
-        <div><span class="text-surface-400">Messages:</span> <span class="text-surface-700 dark:text-surface-300">{selectedConversation.message_count}</span></div>
-        <div><span class="text-surface-400">Tokens:</span> <span class="text-surface-700 dark:text-surface-300">{formatTokens(selectedConversation.total_input_tokens + selectedConversation.total_output_tokens)}</span></div>
-      </div>
-    </div>
- 
-    <!-- Messages -->
-    <div class="space-y-3">
-      {#each messages as msg}
-        <div class="bg-white dark:bg-surface-800 rounded-lg p-4 border border-surface-200 dark:border-surface-700">
-          <div class="flex items-center justify-between mb-2">
-            <span class="text-xs font-semibold uppercase {msg.role === 'user' ? 'text-blue-600' : 'text-green-600'}">{msg.role}</span>
-            <span class="text-xs text-surface-400">{msg.timestamp?.slice(11, 19)}</span>
-          </div>
-          <div class="text-sm text-surface-700 dark:text-surface-300 whitespace-pre-wrap">{msg.content?.slice(0, 500)}{msg.content?.length > 500 ? '...' : ''}</div>
-          {#if msg.tool_calls?.length}
-            <div class="mt-2 pt-2 border-t border-surface-100 dark:border-surface-700">
-              <div class="text-xs font-medium text-surface-400 mb-1">Tools: {msg.tool_calls.map(t => t.name).join(', ')}</div>
-            </div>
-          {/if}
-          {#if msg.input_tokens || msg.output_tokens}
-            <div class="mt-2 text-xs text-surface-400">
-              {msg.input_tokens ? `${msg.input_tokens} in` : ''} {msg.output_tokens ? `${msg.output_tokens} out` : ''}
-            </div>
-          {/if}
-        </div>
-      {/each}
-    </div>
- 
-  {:else if view === "search"}
-    <!-- Search Results -->
-    <button onclick={() => view = "summary"} class="mb-4 text-sm text-primary-600 hover:text-primary-700">← Back</button>
-    <div class="bg-white dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 shadow-sm overflow-hidden">
-      <div class="px-4 py-2 text-xs font-semibold uppercase tracking-wider text-surface-400 border-b border-surface-100 dark:border-surface-700">
-        Search Results ({searchResults.length})
-      </div>
-      {#each searchResults as conv}
-        <button
-          onclick={() => selectConversation(conv.session_id)}
-          class="w-full px-4 py-3 hover:bg-surface-50 dark:hover:bg-surface-700 border-b border-surface-100 dark:border-surface-700 last:border-0 transition-colors text-left">
-          <div class="flex items-center justify-between">
-            <div class="flex-1 min-w-0">
-              <div class="text-sm font-medium text-surface-700 dark:text-surface-200 truncate">{conv.slug || 'Untitled'}</div>
-              <div class="text-xs text-surface-400">{conv.started_at?.slice(0, 10)} • {conv.project_path?.split('/').pop()}</div>
-            </div>
-            <div class="text-xs text-surface-400">{conv.message_count} msgs</div>
-          </div>
-        </button>
-      {/each}
-      {#if searchResults.length === 0}
-        <div class="px-4 py-8 text-center text-sm text-surface-400">No results found</div>
-      {/if}
-    </div>
- 
-  {:else if view === "stats" && stats}
-    <!-- Statistics -->
-    <button onclick={() => view = "summary"} class="mb-4 text-sm text-primary-600 hover:text-primary-700">← Back</button>
-    <div class="grid grid-cols-3 gap-4 mb-6">
-      <div class="bg-white dark:bg-surface-800 rounded-lg p-4 border border-surface-200 dark:border-surface-700">
-        <div class="text-sm text-surface-400">Total Conversations</div>
-        <div class="text-2xl font-bold text-surface-900 dark:text-white">{stats.total_conversations}</div>
-      </div>
-      <div class="bg-white dark:bg-surface-800 rounded-lg p-4 border border-surface-200 dark:border-surface-700">
-        <div class="text-sm text-surface-400">Total Messages</div>
-        <div class="text-2xl font-bold text-surface-900 dark:text-white">{stats.total_messages}</div>
-      </div>
-      <div class="bg-white dark:bg-surface-800 rounded-lg p-4 border border-surface-200 dark:border-surface-700">
-        <div class="text-sm text-surface-400">Total Tokens</div>
-        <div class="text-2xl font-bold text-surface-900 dark:text-white">{formatTokens(stats.total_tokens)}</div>
-      </div>
-    </div>
- 
-    <div class="grid grid-cols-2 gap-4">
-      <div class="bg-white dark:bg-surface-800 rounded-lg p-4 border border-surface-200 dark:border-surface-700">
-        <h3 class="text-sm font-semibold text-surface-700 dark:text-surface-200 mb-3">Top Projects</h3>
-        {#each stats.top_projects as proj}
-          <div class="flex justify-between text-sm py-1">
-            <span class="text-surface-600 dark:text-surface-300 truncate">{proj.path?.split('/').pop()}</span>
-            <span class="text-surface-400">{proj.count}</span>
-          </div>
-        {/each}
-      </div>
- 
-      <div class="bg-white dark:bg-surface-800 rounded-lg p-4 border border-surface-200 dark:border-surface-700">
-        <h3 class="text-sm font-semibold text-surface-700 dark:text-surface-200 mb-3">Top Tools</h3>
-        {#each stats.top_tools as tool}
-          <div class="flex justify-between text-sm py-1">
-            <span class="text-surface-600 dark:text-surface-300">{tool.name}</span>
-            <span class="text-surface-400">{tool.count}</span>
-          </div>
-        {/each}
-      </div>
-    </div>
-  {/if}
-</div>
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/dashboard/frontend/coverage/src/routes/Dashboard.svelte.html b/dashboard/frontend/coverage/src/routes/Dashboard.svelte.html deleted file mode 100644 index 77e7b51..0000000 --- a/dashboard/frontend/coverage/src/routes/Dashboard.svelte.html +++ /dev/null @@ -1,622 +0,0 @@ - - - - - - Code coverage report for src/routes/Dashboard.svelte - - - - - - - - - -
-
-

All files / src/routes Dashboard.svelte

-
- -
- 0% - Statements - 0/87 -
- - -
- 0% - Branches - 0/1 -
- - -
- 0% - Functions - 0/1 -
- - -
- 0% - Lines - 0/87 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -178 -179 -180  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  - 
<script>
-  import { onMount, onDestroy } from "svelte";
-  import { get } from "../lib/api.js";
- 
-  let containers = $state([]);
-  let repos = $state([]);
-  let stats = $state(null);
-  let loading = $state(true);
- 
-  async function load() {
-    const [c, g, s] = await Promise.all([
-      get("/docker/containers"),
-      get("/gitea/repos"),
-      get("/system/stats"),
-    ]);
-    containers = c || [];
-    repos = Array.isArray(g) ? g : g?.data || [];
-    stats = s;
-    loading = false;
-  }
- 
-  function fmtBytes(b) {
-    if (!b) return "0 B";
-    const u = ["B", "KB", "MB", "GB", "TB"];
-    const i = Math.floor(Math.log(b) / Math.log(1024));
-    return (b / Math.pow(1024, i)).toFixed(1) + " " + u[i];
-  }
- 
-  function pctColor(pct) {
-    if (pct > 90) return "text-rose-500";
-    if (pct > 70) return "text-amber-500";
-    return "text-emerald-500";
-  }
- 
-  let interval;
- 
-  onMount(() => { load(); interval = setInterval(load, 10000); });
-  onDestroy(() => clearInterval(interval));
-</script>
- 
-<div class="space-y-8">
-  <div>
-    <h1 class="text-2xl font-bold text-surface-900 dark:text-white tracking-tight">Overview</h1>
-    <p class="text-sm text-surface-400 mt-1">System status at a glance</p>
-  </div>
- 
-  {#if loading}
-    <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
-      {#each Array(4) as _}
-        <div class="h-[120px] rounded-xl bg-surface-100 dark:bg-surface-800 animate-pulse"></div>
-      {/each}
-    </div>
-  {:else}
-    <!-- Stats Cards -->
-    <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
-      <!-- CPU -->
-      <div class="bg-white dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 p-5 shadow-sm hover:shadow-md transition-shadow duration-200">
-        <div class="flex items-center justify-between mb-3">
-          <p class="text-xs font-semibold uppercase tracking-wider text-surface-400">CPU</p>
-          <div class="w-8 h-8 rounded-lg bg-sky-50 flex items-center justify-center">
-            <svg class="w-4 h-4 text-sky-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2z" /></svg>
-          </div>
-        </div>
-        <p class="text-2xl font-bold {pctColor(stats?.cpu_percent || 0)}">{stats?.cpu_percent || 0}%</p>
-        <p class="text-xs text-surface-400 mt-1">{stats?.cpu_count || 0} cores · Load {stats?.load_avg?.[0] || 0}</p>
-      </div>
- 
-      <!-- Memory -->
-      <div class="bg-white dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 p-5 shadow-sm hover:shadow-md transition-shadow duration-200">
-        <div class="flex items-center justify-between mb-3">
-          <p class="text-xs font-semibold uppercase tracking-wider text-surface-400">Memory</p>
-          <div class="w-8 h-8 rounded-lg bg-violet-50 flex items-center justify-center">
-            <svg class="w-4 h-4 text-violet-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
-          </div>
-        </div>
-        <p class="text-2xl font-bold {pctColor(stats?.memory?.percent || 0)}">{stats?.memory?.percent || 0}%</p>
-        <p class="text-xs text-surface-400 mt-1">{fmtBytes(stats?.memory?.used)} / {fmtBytes(stats?.memory?.total)}</p>
-      </div>
- 
-      <!-- Disk -->
-      <div class="bg-white dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 p-5 shadow-sm hover:shadow-md transition-shadow duration-200">
-        <div class="flex items-center justify-between mb-3">
-          <p class="text-xs font-semibold uppercase tracking-wider text-surface-400">Disk</p>
-          <div class="w-8 h-8 rounded-lg bg-amber-50 flex items-center justify-center">
-            <svg class="w-4 h-4 text-amber-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4" /></svg>
-          </div>
-        </div>
-        <p class="text-2xl font-bold {pctColor(stats?.disk?.percent || 0)}">{stats?.disk?.percent || 0}%</p>
-        <p class="text-xs text-surface-400 mt-1">{fmtBytes(stats?.disk?.free)} free of {fmtBytes(stats?.disk?.total)}</p>
-      </div>
- 
-      <!-- Uptime -->
-      <div class="bg-white dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 p-5 shadow-sm hover:shadow-md transition-shadow duration-200">
-        <div class="flex items-center justify-between mb-3">
-          <p class="text-xs font-semibold uppercase tracking-wider text-surface-400">Uptime</p>
-          <div class="w-8 h-8 rounded-lg bg-emerald-50 flex items-center justify-center">
-            <svg class="w-4 h-4 text-emerald-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M13 10V3L4 14h7v7l9-11h-7z" /></svg>
-          </div>
-        </div>
-        <p class="text-2xl font-bold text-surface-800 dark:text-surface-100">{stats?.uptime || "—"}</p>
-        <p class="text-xs text-surface-400 mt-1 truncate" title={stats?.platform}>{stats?.platform || "—"}</p>
-      </div>
-    </div>
- 
-    <!-- Services Grid -->
-    <div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
-      <!-- Docker Summary -->
-      <div class="bg-white dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 p-5 shadow-sm">
-        <div class="flex items-center justify-between mb-4">
-          <h3 class="text-sm font-semibold text-surface-700 dark:text-surface-200">Docker Containers</h3>
-          <span class="text-xs font-medium text-emerald-600 bg-emerald-50 px-2 py-0.5 rounded-full">{containers.filter(c => c.status === "running").length}/{containers.length} running</span>
-        </div>
-        <div class="space-y-2">
-          {#each containers.slice(0, 5) as c}
-            <div class="flex items-center justify-between py-1.5 text-sm">
-              <div class="flex items-center gap-2">
-                <span class="w-1.5 h-1.5 rounded-full {c.status !== 'running' ? 'bg-surface-300' : c.health === 'unhealthy' ? 'bg-rose-400' : c.health === 'healthy' ? 'bg-emerald-400' : 'bg-sky-400'}"></span>
-                <span class="font-medium text-surface-700 dark:text-surface-200">{c.name}</span>
-              </div>
-              <span class="text-xs text-surface-400 truncate max-w-[180px]">{c.image}</span>
-            </div>
-          {/each}
-          {#if containers.length > 5}
-            <p class="text-xs text-primary-500 font-medium pt-1">+{containers.length - 5} more</p>
-          {/if}
-        </div>
-      </div>
- 
-      <!-- Repos Summary -->
-      <div class="bg-white dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 p-5 shadow-sm">
-        <div class="flex items-center justify-between mb-4">
-          <h3 class="text-sm font-semibold text-surface-700 dark:text-surface-200">Git Repositories</h3>
-          <span class="text-xs font-medium text-primary-600 bg-primary-50 px-2 py-0.5 rounded-full">{repos.length} repos</span>
-        </div>
-        <div class="space-y-2">
-          {#each repos.slice(0, 5) as r}
-            <div class="flex items-center justify-between py-1.5 text-sm">
-              <div class="flex items-center gap-2">
-                <svg class="w-3.5 h-3.5 text-surface-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" /></svg>
-                <span class="font-medium text-surface-700 dark:text-surface-200">{r.name}</span>
-              </div>
-              <span class="text-xs text-surface-400">{r.language || "—"}</span>
-            </div>
-          {/each}
-          {#if repos.length > 5}
-            <p class="text-xs text-primary-500 font-medium pt-1">+{repos.length - 5} more</p>
-          {/if}
-        </div>
-      </div>
-    </div>
- 
-    <!-- Storage Usage -->
-    {#if stats?.volumes?.length}
-      <div class="bg-white dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 p-5 shadow-sm">
-        <h3 class="text-sm font-semibold text-surface-700 dark:text-surface-200 mb-3">Storage Usage</h3>
-        <div class="space-y-3">
-          {#each stats.volumes as vol}
-            <div>
-              <div class="flex justify-between text-xs text-surface-500 mb-1">
-                <span class="font-medium">{vol.mount}</span>
-                <span>{vol.percent}%</span>
-              </div>
-              <div class="w-full h-3 bg-surface-100 dark:bg-surface-700 rounded-full overflow-hidden">
-                <div
-                  class="h-full rounded-full transition-all duration-700 ease-out {vol.percent > 90 ? 'bg-gradient-to-r from-rose-400 to-rose-500' : vol.percent > 70 ? 'bg-gradient-to-r from-amber-400 to-amber-500' : 'bg-gradient-to-r from-emerald-400 to-emerald-500'}"
-                  style="width: {vol.percent}%"
-                ></div>
-              </div>
-              <div class="flex justify-between mt-1 text-xs text-surface-400">
-                <span>{fmtBytes(vol.used)} used</span>
-                <span>{fmtBytes(vol.free)} free</span>
-              </div>
-            </div>
-          {/each}
-        </div>
-      </div>
-    {/if}
-  {/if}
-</div>
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/dashboard/frontend/coverage/src/routes/Docker.svelte.html b/dashboard/frontend/coverage/src/routes/Docker.svelte.html deleted file mode 100644 index d65c762..0000000 --- a/dashboard/frontend/coverage/src/routes/Docker.svelte.html +++ /dev/null @@ -1,565 +0,0 @@ - - - - - - Code coverage report for src/routes/Docker.svelte - - - - - - - - - -
-
-

All files / src/routes Docker.svelte

-
- -
- 0% - Statements - 0/94 -
- - -
- 0% - Branches - 0/1 -
- - -
- 0% - Functions - 0/1 -
- - -
- 0% - Lines - 0/94 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  - 
<script>
-  import { onMount } from "svelte";
-  import { get, post } from "../lib/api.js";
- 
-  let containers = $state([]);
-  let loading = $state(true);
-  let error = $state("");
-  let logTarget = $state("");
-  let logContent = $state("");
-  let loadingLogs = $state(false);
-  let actionLoading = $state("");
- 
-  async function load() {
-    loading = true;
-    error = "";
-    try {
-      containers = (await get("/docker/containers")) || [];
-    } catch (e) {
-      error = e.message || "Docker API unavailable";
-      containers = [];
-    }
-    loading = false;
-  }
- 
-  async function action(id, act) {
-    actionLoading = id + act;
-    try {
-      await post(`/docker/containers/${id}/${act}`);
-      await load();
-    } catch (e) {
-      error = e.message || "Action failed";
-    }
-    actionLoading = "";
-  }
- 
-  async function showLogs(id, name) {
-    logTarget = name;
-    loadingLogs = true;
-    try {
-      const r = await get(`/docker/containers/${id}/logs?tail=200`);
-      logContent = r?.logs || "No logs available";
-    } catch (e) {
-      logContent = "Failed to load logs";
-    }
-    loadingLogs = false;
-  }
- 
-  onMount(load);
-</script>
- 
-<div class="space-y-6">
-  <div class="flex items-center justify-between">
-    <div>
-      <h1 class="text-2xl font-bold text-surface-900 dark:text-white tracking-tight">Docker</h1>
-      <p class="text-sm text-surface-400 mt-1">{containers.length} containers · {containers.filter(c => c.status === "running").length} running</p>
-    </div>
-    <button onclick={load} class="text-xs font-medium text-primary-600 bg-primary-50 hover:bg-primary-100 px-3 py-1.5 rounded-lg transition-colors">
-      Refresh
-    </button>
-  </div>
- 
-  {#if loading}
-    <div class="space-y-3">
-      {#each Array(5) as _}
-        <div class="h-[72px] rounded-xl bg-surface-100 dark:bg-surface-800 animate-pulse"></div>
-      {/each}
-    </div>
-  {:else if error}
-    <div class="bg-rose-50 dark:bg-rose-900/20 border border-rose-200 dark:border-rose-800 rounded-xl p-4">
-      <div class="flex items-center justify-between">
-        <div class="flex items-center gap-2">
-          <svg class="w-5 h-5 text-rose-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z" /></svg>
-          <p class="text-sm font-medium text-rose-700 dark:text-rose-300">{error}</p>
-        </div>
-        <button onclick={load} class="text-xs font-medium text-rose-600 bg-rose-100 hover:bg-rose-200 px-3 py-1.5 rounded-lg transition-colors">
-          Retry
-        </button>
-      </div>
-    </div>
-  {:else}
-    <div class="space-y-2">
-      {#each containers as c}
-        <div class="bg-white dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 p-4 flex items-center justify-between shadow-sm hover:shadow-md transition-all duration-200 group">
-          <div class="flex items-center gap-3 min-w-0">
-            <span class="relative flex h-2.5 w-2.5 shrink-0">
-              {#if c.status === "running"}
-                <span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
-                <span class="relative inline-flex rounded-full h-2.5 w-2.5 bg-emerald-500"></span>
-              {:else}
-                <span class="relative inline-flex rounded-full h-2.5 w-2.5 bg-surface-300"></span>
-              {/if}
-            </span>
-            <div class="min-w-0">
-              <p class="text-sm font-semibold text-surface-800 dark:text-surface-100 truncate">{c.name}</p>
-              <p class="text-xs text-surface-400 truncate mt-0.5">{c.image}</p>
-            </div>
-          </div>
-          <div class="flex items-center gap-1.5 opacity-70 group-hover:opacity-100 transition-opacity">
-            {#if c.status === "running"}
-              <button
-                class="text-xs font-medium px-2.5 py-1.5 rounded-lg text-rose-600 bg-rose-50 hover:bg-rose-100 transition-colors disabled:opacity-50"
-                onclick={() => action(c.id, "stop")}
-                disabled={actionLoading === c.id + "stop"}
-              >
-                {actionLoading === c.id + "stop" ? "..." : "Stop"}
-              </button>
-              <button
-                class="text-xs font-medium px-2.5 py-1.5 rounded-lg text-amber-600 bg-amber-50 hover:bg-amber-100 transition-colors disabled:opacity-50"
-                onclick={() => action(c.id, "restart")}
-                disabled={actionLoading === c.id + "restart"}
-              >
-                {actionLoading === c.id + "restart" ? "..." : "Restart"}
-              </button>
-            {:else}
-              <button
-                class="text-xs font-medium px-2.5 py-1.5 rounded-lg text-emerald-600 bg-emerald-50 hover:bg-emerald-100 transition-colors disabled:opacity-50"
-                onclick={() => action(c.id, "start")}
-                disabled={actionLoading === c.id + "start"}
-              >
-                {actionLoading === c.id + "start" ? "..." : "Start"}
-              </button>
-            {/if}
-            <button
-              class="text-xs font-medium px-2.5 py-1.5 rounded-lg text-surface-600 bg-surface-100 hover:bg-surface-200 transition-colors"
-              onclick={() => showLogs(c.id, c.name)}
-            >
-              Logs
-            </button>
-          </div>
-        </div>
-      {/each}
-    </div>
-  {/if}
- 
-  <!-- Log Modal -->
-  {#if logTarget}
-    <div class="fixed inset-0 bg-black/40 backdrop-blur-sm flex items-center justify-center z-50" role="dialog" aria-modal="true" onclick={() => { logTarget = ""; logContent = ""; }} onkeydown={(e) => e.key === "Escape" && (logTarget = "", logContent = "")}>
-      <div class="bg-white dark:bg-surface-800 rounded-2xl shadow-2xl w-[90vw] max-w-4xl max-h-[80vh] flex flex-col" role="document" onclick={(e) => e.stopPropagation()} onkeydown={(e) => e.stopPropagation()}>
-        <div class="flex items-center justify-between px-5 py-4 border-b border-surface-200 dark:border-surface-700">
-          <div>
-            <h3 class="text-sm font-bold text-surface-800 dark:text-surface-100">Container Logs</h3>
-            <p class="text-xs text-surface-400 mt-0.5">{logTarget}</p>
-          </div>
-          <button aria-label="Close logs" class="text-surface-400 hover:text-surface-600 transition-colors" onclick={() => { logTarget = ""; logContent = ""; }}>
-            <svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" /></svg>
-          </button>
-        </div>
-        <div class="flex-1 overflow-auto p-1">
-          {#if loadingLogs}
-            <div class="flex items-center justify-center h-40">
-              <div class="w-6 h-6 border-2 border-primary-500 border-t-transparent rounded-full animate-spin"></div>
-            </div>
-          {:else}
-            <pre class="bg-surface-900 text-emerald-400 text-xs p-4 rounded-xl whitespace-pre-wrap leading-relaxed font-mono min-h-[200px]">{logContent}</pre>
-          {/if}
-        </div>
-      </div>
-    </div>
-  {/if}
-</div>
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/dashboard/frontend/coverage/src/routes/Files.svelte.html b/dashboard/frontend/coverage/src/routes/Files.svelte.html deleted file mode 100644 index 1b71af6..0000000 --- a/dashboard/frontend/coverage/src/routes/Files.svelte.html +++ /dev/null @@ -1,616 +0,0 @@ - - - - - - Code coverage report for src/routes/Files.svelte - - - - - - - - - -
-
-

All files / src/routes Files.svelte

-
- -
- 0% - Statements - 0/105 -
- - -
- 0% - Branches - 0/1 -
- - -
- 0% - Functions - 0/1 -
- - -
- 0% - Lines - 0/105 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -178  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  - 
<script>
-  import { onMount } from "svelte";
-  import { get, del, upload, download } from "../lib/api.js";
- 
-  let entries = $state([]);
-  let currentPath = $state("");
-  let loading = $state(true);
-  let uploading = $state(false);
-  let error = $state("");
-  let fileInput;
- 
-  async function browse(path) {
-    loading = true;
-    currentPath = path;
-    const data = await get(`/files/browse?path=${encodeURIComponent(path)}`);
-    entries = data?.entries || [];
-    loading = false;
-  }
- 
-  function goUp() {
-    const parts = currentPath.split("/").filter(Boolean);
-    parts.pop();
-    browse(parts.join("/"));
-  }
- 
-  async function remove(name) {
-    if (!confirm(`Delete "${name}"? This cannot be undone.`)) return;
-    const path = currentPath ? `${currentPath}/${name}` : name;
-    await del(`/files/delete?path=${encodeURIComponent(path)}`);
-    browse(currentPath);
-  }
- 
-  async function handleDownload(name) {
-    const path = currentPath ? `${currentPath}/${name}` : name;
-    error = "";
-    try {
-      await download(path, name);
-    } catch (e) {
-      error = `Failed to download "${name}": ${e.message}`;
-      console.error("Download failed:", e);
-    }
-  }
- 
-  async function handleUpload() {
-    const file = fileInput?.files?.[0];
-    if (!file) return;
-    uploading = true;
-    await upload(currentPath, file);
-    fileInput.value = "";
-    uploading = false;
-    browse(currentPath);
-  }
- 
-  function formatSize(bytes) {
-    if (!bytes) return "—";
-    const u = ["B", "KB", "MB", "GB", "TB"];
-    const i = Math.floor(Math.log(bytes) / Math.log(1024));
-    return (bytes / Math.pow(1024, i)).toFixed(1) + " " + u[i];
-  }
- 
-  function fileIcon(name) {
-    const ext = name.split(".").pop().toLowerCase();
-    if (["jpg", "jpeg", "png", "gif", "webp", "svg"].includes(ext)) return "image";
-    if (["mp4", "mkv", "avi", "mov"].includes(ext)) return "video";
-    if (["mp3", "flac", "wav", "m4a", "ogg"].includes(ext)) return "audio";
-    if (["zip", "tar", "gz", "7z", "rar"].includes(ext)) return "archive";
-    if (["py", "js", "ts", "go", "rs", "sh", "yml", "yaml", "json", "md"].includes(ext)) return "code";
-    return "file";
-  }
- 
-  const crumbs = $derived(currentPath.split("/").filter(Boolean));
- 
-  onMount(() => browse(""));
-</script>
- 
-<div class="space-y-5">
-  <div class="flex items-center justify-between">
-    <div>
-      <h1 class="text-2xl font-bold text-surface-900 dark:text-white tracking-tight">Files</h1>
-      <p class="text-sm text-surface-400 mt-1">/volume1{currentPath ? "/" + currentPath : ""}</p>
-    </div>
-    <div class="flex items-center gap-2">
-      <input type="file" bind:this={fileInput} onchange={handleUpload} class="hidden" />
-      <button
-        onclick={() => fileInput.click()}
-        class="text-xs font-medium text-primary-600 bg-primary-50 hover:bg-primary-100 px-3 py-1.5 rounded-lg transition-colors disabled:opacity-50"
-        disabled={uploading}
-      >
-        {uploading ? "Uploading..." : "Upload"}
-      </button>
-    </div>
-  </div>
- 
-  <!-- Error message -->
-  {#if error}
-    <div class="bg-rose-50 dark:bg-rose-900/20 border border-rose-200 dark:border-rose-800 rounded-lg px-4 py-3 flex items-start gap-3">
-      <svg class="w-5 h-5 text-rose-500 shrink-0 mt-0.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
-        <path stroke-linecap="round" stroke-linejoin="round" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
-      </svg>
-      <div class="flex-1">
-        <p class="text-sm text-rose-700 dark:text-rose-300">{error}</p>
-      </div>
-      <button onclick={() => error = ""} class="text-rose-400 hover:text-rose-600 transition-colors">
-        <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
-          <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
-        </svg>
-      </button>
-    </div>
-  {/if}
- 
-  <!-- Breadcrumbs -->
-  <div class="flex items-center gap-1 text-sm flex-wrap">
-    <button class="text-primary-600 hover:text-primary-700 font-medium transition-colors" onclick={() => browse("")}>volume1</button>
-    {#each crumbs as part, i}
-      <svg class="w-3.5 h-3.5 text-surface-300" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7" /></svg>
-      <button
-        class="text-primary-600 hover:text-primary-700 font-medium transition-colors"
-        onclick={() => browse(crumbs.slice(0, i + 1).join("/"))}
-      >{part}</button>
-    {/each}
-    {#if currentPath}
-      <button
-        class="ml-auto text-xs font-medium text-surface-500 bg-surface-100 dark:bg-surface-700 hover:bg-surface-200 dark:hover:bg-surface-600 px-2.5 py-1 rounded-lg transition-colors flex items-center gap-1"
-        onclick={goUp}
-      >
-        <svg class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M5 10l7-7m0 0l7 7m-7-7v18" /></svg>
-        Up
-      </button>
-    {/if}
-  </div>
- 
-  <!-- File List -->
-  {#if loading}
-    <div class="space-y-1">
-      {#each Array(8) as _}
-        <div class="h-11 rounded-lg bg-surface-100 dark:bg-surface-800 animate-pulse"></div>
-      {/each}
-    </div>
-  {:else}
-    <div class="bg-white dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 shadow-sm overflow-hidden">
-      <!-- Header -->
-      <div class="grid grid-cols-[1fr_100px_100px] px-4 py-2 text-[11px] font-semibold uppercase tracking-wider text-surface-400 border-b border-surface-100 dark:border-surface-700">
-        <span>Name</span>
-        <span class="text-right">Size</span>
-        <span class="text-right">Actions</span>
-      </div>
-      <!-- Entries -->
-      {#each entries as e}
-        <div class="grid grid-cols-[1fr_100px_100px] items-center px-4 py-2.5 hover:bg-surface-50 dark:hover:bg-surface-700 border-b border-surface-100 dark:border-surface-700 last:border-0 transition-colors group">
-          {#if e.is_dir}
-            <button class="flex items-center gap-2.5 text-sm font-medium text-surface-700 dark:text-surface-200 hover:text-primary-600 transition-colors text-left truncate" onclick={() => browse(currentPath ? `${currentPath}/${e.name}` : e.name)}>
-              <svg class="w-4 h-4 text-amber-400 shrink-0" fill="currentColor" viewBox="0 0 20 20"><path d="M2 6a2 2 0 012-2h5l2 2h5a2 2 0 012 2v6a2 2 0 01-2 2H4a2 2 0 01-2-2V6z" /></svg>
-              {e.name}
-            </button>
-          {:else}
-            <span class="flex items-center gap-2.5 text-sm text-surface-600 dark:text-surface-300 truncate">
-              <svg class="w-4 h-4 text-surface-300 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" /></svg>
-              {e.name}
-            </span>
-          {/if}
-          <span class="text-xs text-surface-400 text-right">{e.is_dir ? "—" : formatSize(e.size)}</span>
-          <div class="flex items-center justify-end gap-1 opacity-100 md:opacity-0 md:group-hover:opacity-100 transition-opacity">
-            {#if !e.is_dir}
-              <button onclick={() => handleDownload(e.name)} class="text-[11px] text-primary-500 hover:text-primary-700 font-medium transition-colors">
-                DL
-              </button>
-            {/if}
-            <button class="text-[11px] text-rose-400 hover:text-rose-600 font-medium transition-colors" onclick={() => remove(e.name)}>Del</button>
-          </div>
-        </div>
-      {/each}
-      {#if entries.length === 0}
-        <div class="px-4 py-8 text-center text-sm text-surface-400">Empty directory</div>
-      {/if}
-    </div>
-  {/if}
-</div>
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/dashboard/frontend/coverage/src/routes/Gitea.svelte.html b/dashboard/frontend/coverage/src/routes/Gitea.svelte.html deleted file mode 100644 index b454fd7..0000000 --- a/dashboard/frontend/coverage/src/routes/Gitea.svelte.html +++ /dev/null @@ -1,457 +0,0 @@ - - - - - - Code coverage report for src/routes/Gitea.svelte - - - - - - - - - -
-
-

All files / src/routes Gitea.svelte

-
- -
- 0% - Statements - 0/74 -
- - -
- 0% - Branches - 0/1 -
- - -
- 0% - Functions - 0/1 -
- - -
- 0% - Lines - 0/74 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  - 
<script>
-  import { onMount } from "svelte";
-  import { get } from "../lib/api.js";
- 
-  let repos = $state([]);
-  let commits = $state([]);
-  let selectedRepo = $state("");
-  let loading = $state(true);
-  let loadingCommits = $state(false);
- 
-  async function load() {
-    loading = true;
-    const data = await get("/gitea/repos");
-    repos = Array.isArray(data) ? data : data?.data || [];
-    loading = false;
-  }
- 
-  async function showCommits(owner, repo) {
-    selectedRepo = `${owner}/${repo}`;
-    loadingCommits = true;
-    const data = await get(`/gitea/repos/${owner}/${repo}/commits`);
-    commits = Array.isArray(data) ? data : [];
-    loadingCommits = false;
-  }
- 
-  function timeAgo(d) {
-    const s = Math.floor((Date.now() - new Date(d)) / 1000);
-    if (s < 60) return "just now";
-    if (s < 3600) return Math.floor(s / 60) + "m ago";
-    if (s < 86400) return Math.floor(s / 3600) + "h ago";
-    return Math.floor(s / 86400) + "d ago";
-  }
- 
-  const langColors = {
-    Python: "#3572A5", JavaScript: "#f1e05a", Shell: "#89e051", Go: "#00ADD8",
-    TypeScript: "#3178c6", Markdown: "#083fa1", CSS: "#563d7c", HTML: "#e34c26",
-  };
- 
-  onMount(load);
-</script>
- 
-<div class="space-y-6">
-  <div>
-    <h1 class="text-2xl font-bold text-surface-900 dark:text-white tracking-tight">Repositories</h1>
-    <p class="text-sm text-surface-400 mt-1">{repos.length} repositories on Gitea</p>
-  </div>
- 
-  {#if loading}
-    <div class="grid grid-cols-1 md:grid-cols-2 gap-3">
-      {#each Array(4) as _}
-        <div class="h-[100px] rounded-xl bg-surface-100 dark:bg-surface-800 animate-pulse"></div>
-      {/each}
-    </div>
-  {:else}
-    <div class="grid grid-cols-1 md:grid-cols-2 gap-3">
-      {#each repos as r}
-        <button
-          class="bg-white dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 p-4 text-left shadow-sm hover:shadow-md hover:border-primary-200 dark:hover:border-primary-700 transition-all duration-200 group {selectedRepo === `${r.owner?.login}/${r.name}` ? 'ring-2 ring-primary-500 border-primary-300' : ''}"
-          onclick={() => showCommits(r.owner?.login, r.name)}
-        >
-          <div class="flex items-start justify-between">
-            <div class="min-w-0">
-              <p class="text-sm font-semibold text-surface-800 dark:text-surface-100 group-hover:text-primary-600 transition-colors truncate">{r.name}</p>
-              <p class="text-xs text-surface-400 mt-1 line-clamp-1">{r.description || "No description"}</p>
-            </div>
-            {#if r.language}
-              <span class="flex items-center gap-1 text-[11px] text-surface-500 shrink-0 ml-2">
-                <span class="w-2 h-2 rounded-full" style="background: {langColors[r.language] || '#888'}"></span>
-                {r.language}
-              </span>
-            {/if}
-          </div>
-          <p class="text-[11px] text-surface-400 mt-2.5">Updated {timeAgo(r.updated_at)}</p>
-        </button>
-      {/each}
-    </div>
-  {/if}
- 
-  <!-- Commits Panel -->
-  {#if selectedRepo}
-    <div class="bg-white dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 p-5 shadow-sm">
-      <div class="flex items-center justify-between mb-4">
-        <div>
-          <h3 class="text-sm font-bold text-surface-800 dark:text-surface-100">Recent Commits</h3>
-          <p class="text-xs text-surface-400 mt-0.5">{selectedRepo}</p>
-        </div>
-        <button
-          aria-label="Close commits"
-          class="text-xs text-surface-400 hover:text-surface-600 transition-colors"
-          onclick={() => { selectedRepo = ""; commits = []; }}
-        >
-          <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" /></svg>
-        </button>
-      </div>
-      {#if loadingCommits}
-        <div class="space-y-3">
-          {#each Array(3) as _}
-            <div class="h-12 rounded-lg bg-surface-100 animate-pulse"></div>
-          {/each}
-        </div>
-      {:else}
-        <div class="space-y-0">
-          {#each commits.slice(0, 15) as c, i}
-            <div class="flex gap-3 py-2.5 {i < commits.length - 1 ? 'border-b border-surface-100 dark:border-surface-700' : ''}">
-              <div class="flex flex-col items-center pt-1">
-                <div class="w-2 h-2 rounded-full bg-primary-400 shrink-0"></div>
-                {#if i < commits.length - 1}
-                  <div class="w-px flex-1 bg-surface-200 mt-1"></div>
-                {/if}
-              </div>
-              <div class="min-w-0 flex-1">
-                <p class="text-sm text-surface-700 dark:text-surface-200 truncate">{c.commit?.message?.split("\n")[0]}</p>
-                <p class="text-[11px] text-surface-400 mt-0.5">
-                  <span class="font-medium">{c.commit?.author?.name}</span> · {timeAgo(c.commit?.author?.date)}
-                </p>
-              </div>
-              <span class="text-[10px] font-mono text-surface-400 bg-surface-50 dark:bg-surface-700 px-1.5 py-0.5 rounded h-fit shrink-0">{c.sha?.slice(0, 7)}</span>
-            </div>
-          {/each}
-        </div>
-      {/if}
-    </div>
-  {/if}
-</div>
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/dashboard/frontend/coverage/src/routes/InfoEngine.svelte.html b/dashboard/frontend/coverage/src/routes/InfoEngine.svelte.html deleted file mode 100644 index 40aa46d..0000000 --- a/dashboard/frontend/coverage/src/routes/InfoEngine.svelte.html +++ /dev/null @@ -1,502 +0,0 @@ - - - - - - Code coverage report for src/routes/InfoEngine.svelte - - - - - - - - - -
-
-

All files / src/routes InfoEngine.svelte

-
- -
- 0% - Statements - 0/92 -
- - -
- 0% - Branches - 0/1 -
- - -
- 0% - Functions - 0/1 -
- - -
- 0% - Lines - 0/92 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  - 
<script>
-  import { onMount } from "svelte";
-  import { getInfoEngineItems, getInfoEngineItem } from "../lib/api.js";
- 
-  let loading = $state(true);
-  let loadingDetail = $state(false);
-  let requestError = $state("");
-  let items = $state([]);
-  let total = $state(0);
-  let selectedId = $state(null);
-  let selectedItem = $state(null);
- 
-  async function loadItems() {
-    loading = true;
-    requestError = "";
-    try {
-      const res = await getInfoEngineItems({ limit: 50, offset: 0 });
-      items = res.items || [];
-      total = res.total || 0;
-      if (items.length > 0) {
-        await selectItem(items[0].id);
-      } else {
-        selectedId = null;
-        selectedItem = null;
-      }
-    } catch (e) {
-      requestError = e?.body?.detail || e?.message || "Failed to load items";
-      items = [];
-      total = 0;
-      selectedId = null;
-      selectedItem = null;
-    } finally {
-      loading = false;
-    }
-  }
- 
-  async function selectItem(id) {
-    selectedId = id;
-    loadingDetail = true;
-    requestError = "";
-    try {
-      selectedItem = await getInfoEngineItem(id);
-    } catch (e) {
-      selectedItem = null;
-      requestError = e?.body?.detail || e?.message || "Failed to load item";
-    } finally {
-      loadingDetail = false;
-    }
-  }
- 
-  function formatDate(value) {
-    if (!value) return "—";
-    try {
-      return new Date(value).toLocaleString();
-    } catch {
-      return value;
-    }
-  }
- 
-  onMount(loadItems);
-</script>
- 
-<div class="space-y-6">
-  <div class="flex items-center justify-between">
-    <div>
-      <h1 class="text-2xl font-bold text-surface-900 dark:text-white tracking-tight">Info Engine</h1>
-      <p class="text-sm text-surface-400 mt-1">Context-aware stream of curated signals</p>
-    </div>
-    <button onclick={loadItems} disabled={loading} class="px-3 py-1.5 text-xs font-medium text-primary-600 bg-primary-50 hover:bg-primary-100 rounded-lg transition-colors disabled:opacity-50 dark:bg-primary-900/30 dark:text-primary-300">
-      {loading ? "Loading..." : "Refresh"}
-    </button>
-  </div>
- 
-  {#if requestError}
-    <div class="bg-rose-50 dark:bg-rose-900/20 border border-rose-200 dark:border-rose-700 rounded-xl p-4 shadow-sm">
-      <p class="text-sm font-medium text-rose-700 dark:text-rose-300">{requestError}</p>
-    </div>
-  {/if}
- 
-  <div class="grid grid-cols-1 lg:grid-cols-3 gap-4">
-    <div class="lg:col-span-1 bg-white dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 shadow-sm">
-      <div class="px-4 py-3 border-b border-surface-200 dark:border-surface-700 text-xs text-surface-400">
-        Latest items ({total})
-      </div>
-      {#if loading}
-        <div class="p-4 space-y-2">
-          {#each Array(6) as _}
-            <div class="h-12 rounded-lg bg-surface-100 dark:bg-surface-700 animate-pulse"></div>
-          {/each}
-        </div>
-      {:else if items.length === 0}
-        <div class="p-4 text-sm text-surface-400">No items yet.</div>
-      {:else}
-        <div class="max-h-[560px] overflow-y-auto p-2">
-          {#each items as item}
-            <button onclick={() => selectItem(item.id)} class="w-full text-left px-3 py-2 rounded-lg mb-1 transition-colors {selectedId === item.id ? 'bg-primary-50 dark:bg-primary-900/30' : 'hover:bg-surface-100 dark:hover:bg-surface-700'}">
-              <p class="text-sm font-semibold text-surface-800 dark:text-surface-100 line-clamp-2">{item.title}</p>
-              <p class="text-xs text-surface-400 mt-1">{item.source}</p>
-            </button>
-          {/each}
-        </div>
-      {/if}
-    </div>
- 
-    <div class="lg:col-span-2 bg-white dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 p-5 shadow-sm">
-      {#if loadingDetail}
-        <div class="space-y-3">
-          <div class="h-7 w-3/4 rounded bg-surface-100 dark:bg-surface-700 animate-pulse"></div>
-          <div class="h-4 w-full rounded bg-surface-100 dark:bg-surface-700 animate-pulse"></div>
-          <div class="h-4 w-5/6 rounded bg-surface-100 dark:bg-surface-700 animate-pulse"></div>
-          <div class="h-24 w-full rounded bg-surface-100 dark:bg-surface-700 animate-pulse"></div>
-        </div>
-      {:else if selectedItem}
-        <h2 class="text-xl font-bold text-surface-900 dark:text-white">{selectedItem.title}</h2>
-        <div class="mt-2 text-xs text-surface-400 space-y-1">
-          <p>Source: {selectedItem.source}</p>
-          <p>Published: {formatDate(selectedItem.published_at)}</p>
-          <p>Collected: {formatDate(selectedItem.collected_at)}</p>
-        </div>
- 
-        {#if selectedItem.tags?.length}
-          <div class="mt-3 flex flex-wrap gap-2">
-            {#each selectedItem.tags as tag}
-              <span class="px-2 py-0.5 text-xs rounded-full bg-surface-100 text-surface-600 dark:bg-surface-700 dark:text-surface-300">{tag}</span>
-            {/each}
-          </div>
-        {/if}
- 
-        <p class="mt-4 text-sm text-surface-700 dark:text-surface-300 whitespace-pre-wrap">{selectedItem.summary || selectedItem.content_text || "No summary available."}</p>
- 
-        <a href={selectedItem.url} target="_blank" rel="noopener" class="inline-flex mt-4 px-3 py-1.5 text-xs font-medium rounded-lg bg-primary-600 text-white hover:bg-primary-700">
-          Open source
-        </a>
-      {:else}
-        <p class="text-sm text-surface-400">Select an item to view details.</p>
-      {/if}
-    </div>
-  </div>
-</div>
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/dashboard/frontend/coverage/src/routes/LiteLLM.svelte.html b/dashboard/frontend/coverage/src/routes/LiteLLM.svelte.html deleted file mode 100644 index 70fb837..0000000 --- a/dashboard/frontend/coverage/src/routes/LiteLLM.svelte.html +++ /dev/null @@ -1,409 +0,0 @@ - - - - - - Code coverage report for src/routes/LiteLLM.svelte - - - - - - - - - -
-
-

All files / src/routes LiteLLM.svelte

-
- -
- 0% - Statements - 0/58 -
- - -
- 0% - Branches - 0/1 -
- - -
- 0% - Functions - 0/1 -
- - -
- 0% - Lines - 0/58 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  - 
<script>
-  import { onMount } from "svelte";
-  import { getLiteLLMHealth } from "../lib/api.js";
- 
-  let loading = $state(true);
-  let health = $state(null);
-  let requestError = $state("");
- 
-  async function loadHealth() {
-    loading = true;
-    requestError = "";
-    try {
-      health = await getLiteLLMHealth();
-    } catch (e) {
-      requestError = e?.body?.detail || e?.message || "Failed to load LiteLLM health";
-      health = null;
-    } finally {
-      loading = false;
-    }
-  }
- 
-  function statusBadgeClass(status) {
-    if (status === "up") {
-      return "bg-emerald-100 text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-300";
-    }
-    if (status === "auth_required") {
-      return "bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-300";
-    }
-    return "bg-rose-100 text-rose-700 dark:bg-rose-900/40 dark:text-rose-300";
-  }
- 
-  function valueOrDash(value) {
-    return value === null || value === undefined || value === "" ? "—" : value;
-  }
- 
-  onMount(loadHealth);
-</script>
- 
-<div class="space-y-6">
-  <div class="flex items-center justify-between">
-    <div>
-      <h1 class="text-2xl font-bold text-surface-900 dark:text-white tracking-tight">LiteLLM</h1>
-      <p class="text-sm text-surface-400 mt-1">Gateway health and connectivity status</p>
-      <div class="mt-2 flex flex-wrap items-center gap-2 text-[11px] font-medium">
-        <span class="px-2 py-0.5 rounded-full bg-emerald-100 text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-300">up</span>
-        <span class="px-2 py-0.5 rounded-full bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-300">auth_required</span>
-        <span class="px-2 py-0.5 rounded-full bg-rose-100 text-rose-700 dark:bg-rose-900/40 dark:text-rose-300">down</span>
-      </div>
-    </div>
-    <button onclick={loadHealth} disabled={loading} class="px-3 py-1.5 text-xs font-medium text-primary-600 bg-primary-50 hover:bg-primary-100 rounded-lg transition-colors disabled:opacity-50 dark:bg-primary-900/30 dark:text-primary-300">
-      {loading ? "Loading..." : "Refresh"}
-    </button>
-  </div>
- 
-  {#if requestError}
-    <div class="bg-rose-50 dark:bg-rose-900/20 border border-rose-200 dark:border-rose-700 rounded-xl p-4 shadow-sm">
-      <p class="text-sm font-medium text-rose-700 dark:text-rose-300">Failed to fetch LiteLLM health</p>
-      <p class="text-xs text-rose-600 dark:text-rose-400 mt-1">{requestError}</p>
-    </div>
-  {/if}
- 
-  <div class="bg-white dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 p-5 shadow-sm">
-    {#if loading && !health}
-      <div class="space-y-3">
-        <div class="h-6 w-36 rounded bg-surface-100 dark:bg-surface-700 animate-pulse"></div>
-        <div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
-          {#each Array(6) as _}
-            <div class="h-16 rounded-lg bg-surface-100 dark:bg-surface-700 animate-pulse"></div>
-          {/each}
-        </div>
-      </div>
-    {:else if health}
-      <div class="flex items-center justify-between mb-4">
-        <h3 class="text-sm font-semibold text-surface-700 dark:text-surface-200">Health Status</h3>
-        <span class="px-2.5 py-1 text-xs font-semibold rounded-full {statusBadgeClass(health.status)}">{valueOrDash(health.status)}</span>
-      </div>
- 
-      <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
-        <div class="rounded-lg border border-surface-200 dark:border-surface-700 p-3">
-          <p class="text-xs font-medium text-surface-400 uppercase tracking-wider">OK</p>
-          <p class="text-sm font-semibold text-surface-800 dark:text-surface-100 mt-1">{String(Boolean(health.ok))}</p>
-        </div>
- 
-        <div class="rounded-lg border border-surface-200 dark:border-surface-700 p-3">
-          <p class="text-xs font-medium text-surface-400 uppercase tracking-wider">Latency</p>
-          <p class="text-sm font-semibold text-surface-800 dark:text-surface-100 mt-1">{health.latency_ms === null || health.latency_ms === undefined ? "—" : `${health.latency_ms} ms`}</p>
-        </div>
- 
-        <div class="rounded-lg border border-surface-200 dark:border-surface-700 p-3">
-          <p class="text-xs font-medium text-surface-400 uppercase tracking-wider">Endpoint</p>
-          <p class="text-sm font-semibold text-surface-800 dark:text-surface-100 mt-1">{valueOrDash(health.endpoint)}</p>
-        </div>
- 
-        <div class="rounded-lg border border-surface-200 dark:border-surface-700 p-3">
-          <p class="text-xs font-medium text-surface-400 uppercase tracking-wider">HTTP Status</p>
-          <p class="text-sm font-semibold text-surface-800 dark:text-surface-100 mt-1">{valueOrDash(health.http_status)}</p>
-        </div>
- 
-        <div class="rounded-lg border border-surface-200 dark:border-surface-700 p-3 sm:col-span-2 lg:col-span-2">
-          <p class="text-xs font-medium text-surface-400 uppercase tracking-wider">Error</p>
-          <p class="text-sm font-semibold text-surface-800 dark:text-surface-100 mt-1 break-all">{valueOrDash(health.error)}</p>
-        </div>
-      </div>
-    {:else}
-      <p class="text-sm text-surface-400">No health data available.</p>
-    {/if}
-  </div>
-</div>
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/dashboard/frontend/coverage/src/routes/Login.svelte.html b/dashboard/frontend/coverage/src/routes/Login.svelte.html deleted file mode 100644 index 3cb03c8..0000000 --- a/dashboard/frontend/coverage/src/routes/Login.svelte.html +++ /dev/null @@ -1,775 +0,0 @@ - - - - - - Code coverage report for src/routes/Login.svelte - - - - - - - - - -
-
-

All files / src/routes Login.svelte

-
- -
- 41.02% - Statements - 48/117 -
- - -
- 30% - Branches - 3/10 -
- - -
- 33.33% - Functions - 2/6 -
- - -
- 41.02% - Lines - 48/117 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -178 -179 -180 -181 -182 -183 -184 -185 -186 -187 -188 -189 -190 -191 -192 -193 -194 -195 -196 -197 -198 -199 -200 -201 -202 -203 -204 -205 -206 -207 -208 -209 -210 -211 -212 -213 -214 -215 -216 -217 -218 -219 -220 -221 -222 -223 -224 -225 -226 -227 -228 -229 -230 -2313x -  -  -  -3x -3x -3x -3x -3x -3x -3x -  -3x -  -  -  -  -  -3x -  -  -  -  -  -  -3x -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -3x -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -3x -  -1x -1x -  -  -  -  -  -  -  -  -  -  -  -  -1x -1x -1x -  -1x -1x -  -  -1x -1x -  -  -  -  -  -  -  -  -  -  -  -  -1x -  -  -  -  -1x -1x -1x -1x -1x -  -1x -  -  -  -  -1x -  -  -  -  -1x -  -1x -  -  -  -  -1x -  -  -  -  -  -1x -  -1x -  -  -  -  -  -  -  -  -1x -  -  -  -  -1x -1x -  -  -  -  -  -  -  -1x -  -1x -  -  -1x -1x -  -  -  -  -1x -  -  -  -  -1x -1x -1x -  -  -  -  -  -1x -1x -  -  -  -  -  -  -  - 
<script>
-  import { login, setToken } from "../lib/api.js";
-  import { fade, fly } from "svelte/transition";
- 
-  let username = $state("");
-  let password = $state("");
-  let totp_code = $state("");
-  let error = $state("");
-  let loading = $state(false);
-  let require2FA = $state(false);
-  let showPasswordForm = $state(!window.isSecureContext);
- 
-  function base64urlToBuffer(b64) {
-    const s = b64.replace(/-/g, '+').replace(/_/g, '/');
-    const raw = atob(s);
-    return Uint8Array.from(raw, c => c.charCodeAt(0)).buffer;
-  }
- 
-  function bufferToBase64url(buf) {
-    const bytes = new Uint8Array(buf);
-    let s = '';
-    bytes.forEach(b => s += String.fromCharCode(b));
-    return btoa(s).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
-  }
- 
-  async function loginWithPasskey() {
-    loading = true;
-    error = "";
-    try {
-      const r = await fetch("/api/auth/passkey/login/options", { method: "POST" });
-      if (!r.ok) throw new Error("No passkeys registered");
-      const opts = await r.json();
-      opts.challenge = base64urlToBuffer(opts.challenge);
-      if (opts.allowCredentials) {
-        opts.allowCredentials = opts.allowCredentials.map(c => ({
-          ...c, id: base64urlToBuffer(c.id)
-        }));
-      }
-      const cred = await navigator.credentials.get({ publicKey: opts });
-      const body = {
-        id: cred.id,
-        rawId: bufferToBase64url(cred.rawId),
-        type: cred.type,
-        response: {
-          authenticatorData: bufferToBase64url(cred.response.authenticatorData),
-          clientDataJSON: bufferToBase64url(cred.response.clientDataJSON),
-          signature: bufferToBase64url(cred.response.signature),
-          userHandle: cred.response.userHandle ? bufferToBase64url(cred.response.userHandle) : null,
-        },
-      };
-      const res = await fetch("/api/auth/passkey/login/verify", {
-        method: "POST",
-        headers: { "Content-Type": "application/json" },
-        body: JSON.stringify(body),
-      });
-      if (!res.ok) { const e = await res.json(); throw new Error(e.detail || "Passkey verification failed"); }
-      const data = await res.json();
-      setToken(data.access_token);
-      window.location.reload();
-    } catch (e) {
-      if (e.name === "NotAllowedError") { error = "Passkey authentication cancelled"; }
-      else { error = e.message || "Passkey login failed"; }
-      showPasswordForm = true;
-    } finally {
-      loading = false;
-    }
-  }
- 
-  async function handleSubmit(e) {
-    e.preventDefault();
-    loading = true;
-    error = "";
-    
-    try {
-      const payload = { username, password };
-      if (require2FA) payload.totp_code = totp_code;
-      
-      const res = await login(payload); // api.js helper sends JSON
-      
-      setToken(res.access_token);
-      // Determine if we need to reload or just update state. 
-      // Reload is safest to clear any stale state.
-      window.location.reload(); 
-      
-    } catch (e) {
-      if (e.status === 403 && e.body?.detail === "2FA code required") {
-         require2FA = true;
-         // Clear error if any
-         error = "";
-      } else {
-         error = e.body?.detail || "Login failed. Please check your credentials.";
-         // Reset 2FA state on general failure (maybe password was wrong this time)
-         if (!require2FA) {
-            password = "";
-         }
-      }
-    } finally {
-      loading = false;
-    }
-  }
-</script>
- 
-<div class="flex min-h-screen items-center justify-center bg-surface-50 dark:bg-surface-950 px-4">
-  <div class="w-full max-w-sm space-y-8 bg-white dark:bg-surface-900 p-8 rounded-2xl shadow-xl border border-surface-200 dark:border-surface-800" in:fly={{ y: 20, duration: 400 }}>
-    <div class="text-center">
-      <div class="mx-auto h-12 w-12 bg-gradient-to-br from-primary-500 to-primary-600 rounded-xl flex items-center justify-center shadow-lg mb-4">
-        <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
-          <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
-        </svg>
-      </div>
-      <h2 class="text-2xl font-bold tracking-tight text-surface-900 dark:text-white">Sign in to NAS</h2>
-      <p class="mt-2 text-sm text-surface-600 dark:text-surface-400">
-        Enter your credentials to access the dashboard
-      </p>
-    </div>
- 
-    <div class="mt-8 space-y-6">
-      {#if !showPasswordForm && !require2FA}
-        <button
-          type="button"
-          disabled={loading}
-          onclick={loginWithPasskey}
-          class="flex w-full justify-center rounded-lg bg-surface-800 dark:bg-surface-700 px-3 py-2.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-surface-700 dark:hover:bg-surface-600 disabled:opacity-70 transition-all active:scale-[0.98]"
-        >
-          {#if loading}
-            <svg class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" fill="none" viewBox="0 0 24 24">
-              <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
-              <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
-            </svg>
-          {/if}
-          Sign in with Passkey
-        </button>
- 
-        <div class="relative">
-          <div class="absolute inset-0 flex items-center"><div class="w-full border-t border-surface-200 dark:border-surface-700"></div></div>
-          <div class="relative flex justify-center text-xs"><span class="bg-white dark:bg-surface-900 px-2 text-surface-400">or</span></div>
-        </div>
- 
-        <button type="button" onclick={() => showPasswordForm = true} class="flex w-full justify-center text-sm text-primary-500 hover:text-primary-400">
-          Use password instead
-        </button>
-      {/if}
- 
-      {#if showPasswordForm || require2FA}
-      <form class="space-y-6" onsubmit={handleSubmit}>
-      <div class="space-y-4">
-        {#if !require2FA}
-          <div>
-            <label for="username" class="sr-only">Username</label>
-            <input
-              id="username"
-              name="username"
-              type="text"
-              required
-              bind:value={username}
-              class="relative block w-full rounded-lg border-0 py-2.5 px-3 text-surface-900 dark:text-white ring-1 ring-inset ring-surface-300 dark:ring-surface-700 placeholder:text-surface-400 focus:z-10 focus:ring-2 focus:ring-inset focus:ring-primary-600 dark:bg-surface-800 sm:text-sm sm:leading-6"
-              placeholder="Username"
-            />
-          </div>
-          <div>
-            <label for="password" class="sr-only">Password</label>
-            <input
-              id="password"
-              name="password"
-              type="password"
-              required
-              bind:value={password}
-              class="relative block w-full rounded-lg border-0 py-2.5 px-3 text-surface-900 dark:text-white ring-1 ring-inset ring-surface-300 dark:ring-surface-700 placeholder:text-surface-400 focus:z-10 focus:ring-2 focus:ring-inset focus:ring-primary-600 dark:bg-surface-800 sm:text-sm sm:leading-6"
-              placeholder="Password"
-            />
-          </div>
-        {:else}
-          <div in:fade>
-            <label for="totp" class="block text-sm font-medium leading-6 text-surface-900 dark:text-white text-center mb-2">Authenticator Code</label>
-            <input
-              id="totp"
-              name="totp"
-              type="text"
-              inputmode="numeric"
-              pattern="[0-9]*"
-              autocomplete="one-time-code"
-              required
-              autofocus
-              bind:value={totp_code}
-              class="block w-full text-center tracking-[0.5em] text-2xl font-mono rounded-lg border-0 py-3 text-surface-900 dark:text-white ring-1 ring-inset ring-surface-300 dark:ring-surface-700 placeholder:text-surface-400 focus:ring-2 focus:ring-inset focus:ring-primary-600 dark:bg-surface-800"
-              placeholder="000000"
-              maxlength="6"
-            />
-            <p class="mt-4 text-center text-sm text-surface-500">
-              <button type="button" class="text-primary-500 hover:text-primary-400" onclick={() => require2FA = false}>
-                Back to credentials
-              </button>
-            </p>
-          </div>
-        {/if}
-      </div>
- 
-        <button
-          type="submit"
-          disabled={loading}
-          class="flex w-full justify-center rounded-lg bg-primary-600 px-3 py-2.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-primary-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600 disabled:opacity-70 transition-all active:scale-[0.98]"
-        >
-          {#if loading}
-            <svg class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" fill="none" viewBox="0 0 24 24">
-              <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
-              <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
-            </svg>
-          {/if}
-          {require2FA ? "Verify Code" : "Sign in"}
-        </button>
-      </form>
-      {/if}
- 
-      {#if error}
-        <div class="rounded-md bg-red-50 dark:bg-red-900/30 p-3" in:fade>
-          <div class="flex">
-            <div class="flex-shrink-0">
-              <svg class="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
-                <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
-              </svg>
-            </div>
-            <div class="ml-3">
-              <h3 class="text-sm font-medium text-red-800 dark:text-red-200">{error}</h3>
-            </div>
-          </div>
-        </div>
-      {/if}
-    </div>
-  </div>
-</div>
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/dashboard/frontend/coverage/src/routes/OPC.svelte.html b/dashboard/frontend/coverage/src/routes/OPC.svelte.html deleted file mode 100644 index b0a674a..0000000 --- a/dashboard/frontend/coverage/src/routes/OPC.svelte.html +++ /dev/null @@ -1,709 +0,0 @@ - - - - - - Code coverage report for src/routes/OPC.svelte - - - - - - - - - -
-
-

All files / src/routes OPC.svelte

-
- -
- 0% - Statements - 0/129 -
- - -
- 0% - Branches - 0/1 -
- - -
- 0% - Functions - 0/1 -
- - -
- 0% - Lines - 0/129 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -178 -179 -180 -181 -182 -183 -184 -185 -186 -187 -188 -189 -190 -191 -192 -193 -194 -195 -196 -197 -198 -199 -200 -201 -202 -203 -204 -205 -206 -207 -208 -209  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  - 
<script>
-  import { onMount, onDestroy } from "svelte";
-  import KanbanBoard from "../components/opc/KanbanBoard.svelte";
-  import TaskModal from "../components/opc/TaskModal.svelte";
-  import AgentPanel from "../components/opc/AgentPanel.svelte";
-  import { getTasks, getAgents } from "../lib/opc-api.js";
-  import * as opcWs from "../lib/opc-ws.js";
- 
-  let tasks = $state([]);
-  let agents = $state([]);
-  let loading = $state(true);
-  let showTaskModal = $state(false);
-  let editingTask = $state(null);
-  let showAgentPanel = $state(
-    typeof localStorage !== 'undefined'
-      ? localStorage.getItem('opc_showAgentPanel') !== 'false'
-      : true
-  );
-  let unsubscribe = null;
- 
-  // Persist agent panel visibility
-  $effect(() => {
-    if (typeof localStorage !== 'undefined') {
-      localStorage.setItem('opc_showAgentPanel', showAgentPanel);
-    }
-  });
- 
-  async function loadData() {
-    loading = true;
-    try {
-      const [tasksRes, agentsRes] = await Promise.all([
-        getTasks(),
-        getAgents()
-      ]);
-      tasks = tasksRes.items || [];
-      agents = agentsRes.items || [];
-    } catch (e) {
-      console.error("Failed to load data:", e);
-    } finally {
-      loading = false;
-    }
-  }
- 
-  function handleWebSocketMessage(message) {
-    if (message.type === "task_update") {
-      const { task_id, action, data } = message;
- 
-      if (action === "created") {
-        tasks = [...tasks, data];
-      } else if (action === "updated" || action === "moved") {
-        tasks = tasks.map(t => t.id === task_id ? data : t);
-      } else if (action === "deleted") {
-        tasks = tasks.filter(t => t.id !== task_id);
-      }
-    } else if (message.type === "agent_execution") {
-      // Could show agent status in UI
-      console.log("Agent execution update:", message);
-    }
-  }
- 
-  function handleCreateTask() {
-    editingTask = null;
-    showTaskModal = true;
-  }
- 
-  function handleEditTask(task) {
-    editingTask = task;
-    showTaskModal = true;
-  }
- 
-  function handleDeleteTask(taskId) {
-    tasks = tasks.filter(t => t.id !== taskId);
-  }
- 
-  function handleAssignTask(taskId, assignedTo, assignedType) {
-    const task = tasks.find(t => t.id === taskId);
-    if (task) {
-      task.assigned_to = assignedTo;
-      task.assigned_type = assignedType;
-      tasks = [...tasks];
-    }
-  }
- 
-  function handleTaskMoved(taskId, newStatus) {
-    const task = tasks.find(t => t.id === taskId);
-    if (task) {
-      task.status = newStatus;
-      tasks = [...tasks];
-    }
-  }
- 
-  function handleModalClose() {
-    showTaskModal = false;
-    editingTask = null;
-  }
- 
-  function handleModalSave() {
-    loadData();
-  }
- 
-  onMount(() => {
-    loadData();
- 
-    // Connect WebSocket
-    opcWs.connect();
-    unsubscribe = opcWs.subscribe(handleWebSocketMessage);
-  });
- 
-  onDestroy(() => {
-    if (unsubscribe) {
-      unsubscribe();
-    }
-    opcWs.disconnect();
-  });
-</script>
- 
-<div class="space-y-6 h-full flex flex-col">
-  <!-- Header -->
-  <div class="flex items-center justify-between">
-    <div>
-      <h1 class="text-2xl font-bold text-surface-900 dark:text-surface-100">
-        OPC Management
-      </h1>
-      <p class="text-surface-600 dark:text-surface-400 mt-1">
-        Manage your tasks with AI agents
-      </p>
-    </div>
-    <div class="flex gap-2">
-      <button
-        onclick={() => showAgentPanel = !showAgentPanel}
-        class="px-4 py-2 bg-surface-200 dark:bg-surface-700 text-surface-900 dark:text-surface-100 rounded-lg hover:bg-surface-300 dark:hover:bg-surface-600 flex items-center gap-2"
-      >
-        <span>🤖</span>
-        <span>{showAgentPanel ? "Hide" : "Show"} Agents</span>
-      </button>
-      <button
-        onclick={handleCreateTask}
-        class="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 flex items-center gap-2"
-      >
-        <span>➕</span>
-        <span>New Task</span>
-      </button>
-    </div>
-  </div>
- 
-  <!-- Stats -->
-  <div class="grid grid-cols-1 md:grid-cols-4 gap-4">
-    <div class="bg-white dark:bg-surface-800 rounded-lg p-4 border border-surface-200 dark:border-surface-700">
-      <div class="text-sm text-surface-600 dark:text-surface-400">Total Tasks</div>
-      <div class="text-2xl font-bold text-surface-900 dark:text-surface-100 mt-1">
-        {tasks.length}
-      </div>
-    </div>
-    <div class="bg-white dark:bg-surface-800 rounded-lg p-4 border border-surface-200 dark:border-surface-700">
-      <div class="text-sm text-surface-600 dark:text-surface-400">Parking Lot</div>
-      <div class="text-2xl font-bold text-slate-600 dark:text-slate-400 mt-1">
-        {tasks.filter(t => t.status === "parking_lot").length}
-      </div>
-    </div>
-    <div class="bg-white dark:bg-surface-800 rounded-lg p-4 border border-surface-200 dark:border-surface-700">
-      <div class="text-sm text-surface-600 dark:text-surface-400">In Progress</div>
-      <div class="text-2xl font-bold text-blue-600 dark:text-blue-400 mt-1">
-        {tasks.filter(t => t.status === "in_progress").length}
-      </div>
-    </div>
-    <div class="bg-white dark:bg-surface-800 rounded-lg p-4 border border-surface-200 dark:border-surface-700">
-      <div class="text-sm text-surface-600 dark:text-surface-400">Done</div>
-      <div class="text-2xl font-bold text-emerald-600 dark:text-emerald-400 mt-1">
-        {tasks.filter(t => t.status === "done").length}
-      </div>
-    </div>
-  </div>
- 
-  <!-- Kanban Board -->
-  {#if loading}
-    <div class="flex-1 flex items-center justify-center">
-      <div class="text-surface-600 dark:text-surface-400">Loading...</div>
-    </div>
-  {:else}
-    <div class="flex-1 overflow-hidden">
-      <KanbanBoard
-        {tasks}
-        {agents}
-        onEdit={handleEditTask}
-        onDelete={handleDeleteTask}
-        onAssign={handleAssignTask}
-        onTaskMoved={handleTaskMoved}
-      />
-    </div>
-  {/if}
- 
-  <!-- Agent Panel -->
-  {#if showAgentPanel}
-    <div class="mt-6">
-      <AgentPanel />
-    </div>
-  {/if}
-</div>
- 
-<!-- Task Modal -->
-{#if showTaskModal}
-  <TaskModal
-    task={editingTask}
-    {agents}
-    onClose={handleModalClose}
-    onSave={handleModalSave}
-  />
-{/if}
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/dashboard/frontend/coverage/src/routes/OpenClaw.svelte.html b/dashboard/frontend/coverage/src/routes/OpenClaw.svelte.html deleted file mode 100644 index 67cf897..0000000 --- a/dashboard/frontend/coverage/src/routes/OpenClaw.svelte.html +++ /dev/null @@ -1,283 +0,0 @@ - - - - - - Code coverage report for src/routes/OpenClaw.svelte - - - - - - - - - -
-
-

All files / src/routes OpenClaw.svelte

-
- -
- 0% - Statements - 0/22 -
- - -
- 0% - Branches - 0/1 -
- - -
- 0% - Functions - 0/1 -
- - -
- 0% - Lines - 0/22 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  - 
<script>
-  import { onMount } from "svelte";
-  import { get } from "../lib/api.js";
- 
-  let isTailscale = location.hostname.startsWith("100.");
-  let isLocal = location.hostname === "localhost" || location.hostname === "127.0.0.1" || location.hostname.startsWith("192.168.");
-  let canAccess = isTailscale || isLocal;
-  let token = $state("");
- 
-  onMount(async () => {
-    if (canAccess) {
-      const res = await get("/system/openclaw-token");
-      token = res?.token || "";
-    }
-  });
- 
-  function clawUrl() {
-    const base = `${location.protocol}//${location.hostname}:3100`;
-    return token ? `${base}/#token=${token}` : base;
-  }
-</script>
- 
-<div class="space-y-4">
-  <div>
-    <h1 class="text-2xl font-bold text-surface-900 tracking-tight">OpenClaw</h1>
-    <p class="text-sm text-surface-400 mt-1">AI Assistant</p>
-  </div>
- 
-  {#if canAccess}
-    <div class="flex flex-col items-center justify-center p-12 text-center bg-surface-50 dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 h-[calc(100vh-10rem)]">
-      <div class="h-16 w-16 bg-primary-100 dark:bg-primary-900/30 text-primary-600 dark:text-primary-400 rounded-2xl flex items-center justify-center mb-6">
-        <svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
-          <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" />
-        </svg>
-      </div>
-      <h2 class="text-xl font-semibold text-surface-900 dark:text-white mb-2">OpenClaw Ready</h2>
-      <p class="text-surface-600 dark:text-surface-400 max-w-md mb-8">
-        OpenClaw's security settings prevent it from being embedded inside the dashboard. Click below to launch the assistant securely in a new tab.
-      </p>
-      <a
-        href={clawUrl()}
-        target="_blank"
-        rel="noopener noreferrer"
-        class="inline-flex items-center justify-center px-6 py-3 border border-transparent text-base font-medium rounded-lg text-white bg-primary-600 hover:bg-primary-700 shadow-sm transition-colors"
-      >
-        Launch OpenClaw
-        <svg xmlns="http://www.w3.org/2000/svg" class="ml-2 -mr-1 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
-          <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
-        </svg>
-      </a>
-    </div>
-  {:else}
-    <div class="flex flex-col items-center justify-center p-12 text-center bg-surface-50 dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 h-[calc(100vh-10rem)]">
-      <svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16 text-surface-400 mb-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
-        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
-      </svg>
-      <h2 class="text-xl font-semibold text-surface-900 dark:text-white mb-2">Tailscale Network Required</h2>
-      <p class="text-surface-600 dark:text-surface-400 max-w-md">
-        For security reasons, OpenClaw is only accessible when connected directly to the NAS via the internal Tailscale network.
-      </p>
-      <p class="mt-4 text-sm text-surface-500">
-        Connect to Tailscale and visit <a href="http://100.78.131.124:4000" class="text-primary-500 hover:underline font-medium">http://100.78.131.124:4000</a> to use the AI Assistant.
-      </p>
-    </div>
-  {/if}
-</div>
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/dashboard/frontend/coverage/src/routes/Security.svelte.html b/dashboard/frontend/coverage/src/routes/Security.svelte.html deleted file mode 100644 index 2bfd797..0000000 --- a/dashboard/frontend/coverage/src/routes/Security.svelte.html +++ /dev/null @@ -1,934 +0,0 @@ - - - - - - Code coverage report for src/routes/Security.svelte - - - - - - - - - -
-
-

All files / src/routes Security.svelte

-
- -
- 0% - Statements - 0/171 -
- - -
- 0% - Branches - 0/1 -
- - -
- 0% - Functions - 0/1 -
- - -
- 0% - Lines - 0/171 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -178 -179 -180 -181 -182 -183 -184 -185 -186 -187 -188 -189 -190 -191 -192 -193 -194 -195 -196 -197 -198 -199 -200 -201 -202 -203 -204 -205 -206 -207 -208 -209 -210 -211 -212 -213 -214 -215 -216 -217 -218 -219 -220 -221 -222 -223 -224 -225 -226 -227 -228 -229 -230 -231 -232 -233 -234 -235 -236 -237 -238 -239 -240 -241 -242 -243 -244 -245 -246 -247 -248 -249 -250 -251 -252 -253 -254 -255 -256 -257 -258 -259 -260 -261 -262 -263 -264 -265 -266 -267 -268 -269 -270 -271 -272 -273 -274 -275 -276 -277 -278 -279 -280 -281 -282 -283 -284  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  - 
<script>
-  import { get } from "../lib/api.js";
-  import { onMount } from "svelte";
- 
-  let logs = $state([]);
-  let stats = $state(null);
-  let loading = $state(false);
-  let filter = $state("all");
-  let ipFilter = $state("");
-  let startDate = $state("");
-  let endDate = $state("");
-  let filterError = $state("");
- 
-  onMount(loadAll);
- 
-  async function loadAll() {
-    loading = true;
-    await Promise.all([loadStats(), loadLogs()]);
-    loading = false;
-  }
- 
-  async function loadStats() {
-    try {
-      stats = await get("/security/stats");
-    } catch (e) {
-      console.error("Failed to load security stats:", e);
-    }
-  }
- 
-  function buildLogsQuery() {
-    const params = new URLSearchParams({ limit: "300" });
-    if (filter !== "all") params.set("type", filter);
-    if (ipFilter.trim()) params.set("ip", ipFilter.trim());
-    if (startDate) params.set("start_date", startDate);
-    if (endDate) params.set("end_date", endDate);
-    return params.toString();
-  }
- 
-  function validateFilters() {
-    if (startDate && endDate && startDate > endDate) {
-      filterError = "From date must be on or before To date";
-      return false;
-    }
-    filterError = "";
-    return true;
-  }
- 
-  async function loadLogs() {
-    if (!validateFilters()) {
-      logs = [];
-      return;
-    }
- 
-    try {
-      const res = await get(`/security/logs?${buildLogsQuery()}`);
-      logs = res.logs;
-    } catch (e) {
-      console.error("Failed to load security logs:", e);
-    }
-  }
- 
-  async function applyTypeFilter(nextFilter) {
-    filter = nextFilter;
-    await loadLogs();
-  }
- 
-  async function applyIpFilter(ip) {
-    ipFilter = ip;
-    await loadLogs();
-  }
- 
-  async function clearFilters() {
-    filter = "all";
-    ipFilter = "";
-    startDate = "";
-    endDate = "";
-    filterError = "";
-    await loadLogs();
-  }
- 
-  let maxTimelineCount = $derived(
-    stats ? Math.max(1, ...stats.timeline.map(t => t.count)) : 1
-  );
- 
-  let hasActiveFilters = $derived(
-    filter !== "all" || !!ipFilter.trim() || !!startDate || !!endDate
-  );
- 
-  function isBlockedScannerEvent(entry) {
-    if (entry.type !== "unauthorized") return false;
- 
-    const message = (entry.message || "").toLowerCase();
-    return [
-      "/.git/",
-      "phpinfo",
-      "debug.log",
-      "laravel.log",
-      "stripe",
-      "swagger.json",
-      "wp-config",
-      "credentials",
-      "secrets",
-      "config",
-      "@vite",
-      "manifest.json",
-    ].some((needle) => message.includes(needle));
-  }
- 
-  const logTypeFilters = ["all", "auth_failure", "suspicious_request", "unauthorized"];
-  const logTypeButtonLabels = {
-    all: "All",
-    auth_failure: "Auth",
-    suspicious_request: "Requests",
-    unauthorized: "Blocked",
-  };
- 
-  const typeLabels = { auth_failure: "Auth", suspicious_request: "Request", unauthorized: "Blocked", error: "Error" };
-  const typeColors = {
-    auth_failure: "text-red-400",
-    suspicious_request: "text-amber-400",
-    unauthorized: "text-orange-400",
-    error: "text-surface-500"
-  };
-</script>
- 
-<div class="space-y-6">
-  <div class="flex items-center justify-between">
-    <h2 class="text-lg font-bold text-surface-800 dark:text-surface-100">Security</h2>
-    <button onclick={loadAll} disabled={loading} class="px-3 py-1.5 text-xs font-medium text-primary-600 bg-primary-50 hover:bg-primary-100 rounded-lg transition-colors disabled:opacity-50 dark:bg-primary-900/30 dark:text-primary-300">
-      {loading ? "Loading..." : "Refresh"}
-    </button>
-  </div>
- 
-  <!-- Stats cards -->
-  {#if stats}
-    <div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
-      <div class="bg-white dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 p-5 shadow-sm">
-        <p class="text-xs font-medium text-surface-400 uppercase tracking-wider">Failed Logins (24h)</p>
-        <p class="text-2xl font-bold text-red-600 dark:text-red-400 mt-1">{stats.failed_logins_24h}</p>
-      </div>
-      <div class="bg-white dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 p-5 shadow-sm">
-        <p class="text-xs font-medium text-surface-400 uppercase tracking-wider">Suspicious Requests</p>
-        <p class="text-2xl font-bold text-amber-600 dark:text-amber-400 mt-1">{stats.suspicious_requests}</p>
-      </div>
-      <div class="bg-white dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 p-5 shadow-sm">
-        <p class="text-xs font-medium text-surface-400 uppercase tracking-wider">Unique IPs</p>
-        <p class="text-2xl font-bold text-surface-700 dark:text-surface-200 mt-1">{stats.unique_ips}</p>
-      </div>
-    </div>
-  {/if}
- 
-  <!-- Timeline chart -->
-  {#if stats?.timeline}
-    <div class="bg-white dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 p-5 shadow-sm">
-      <h3 class="text-sm font-semibold text-surface-700 dark:text-surface-200 mb-3">Activity (Last 24h)</h3>
-      <div class="flex items-end gap-1 h-24">
-        {#each [...stats.timeline].reverse() as bar}
-          <div class="flex-1 flex flex-col items-center justify-end h-full">
-            <div
-              class="w-full rounded-t transition-all {bar.count > 0 ? 'bg-primary-500 dark:bg-primary-400' : 'bg-surface-100 dark:bg-surface-700'}"
-              style="height: {bar.count > 0 ? Math.max(8, (bar.count / maxTimelineCount) * 100) : 4}%"
-              title="{bar.hours_ago}h ago: {bar.count} events"
-            ></div>
-          </div>
-        {/each}
-      </div>
-      <div class="flex justify-between mt-1">
-        <span class="text-[10px] text-surface-400">24h ago</span>
-        <span class="text-[10px] text-surface-400">now</span>
-      </div>
-    </div>
-  {/if}
- 
-  <!-- Top IPs -->
-  {#if stats?.top_ips?.length}
-    <div class="bg-white dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 p-5 shadow-sm">
-      <h3 class="text-sm font-semibold text-surface-700 dark:text-surface-200 mb-3">Top IPs</h3>
-      <div class="space-y-2">
-        {#each stats.top_ips as entry}
-          <div class="flex items-center gap-3 text-sm">
-            <button onclick={() => applyIpFilter(entry.ip)} class="text-xs font-mono text-surface-600 hover:text-primary-600 dark:text-surface-300 dark:hover:text-primary-300 w-36 shrink-0 text-left transition-colors">{entry.ip}</button>
-            <div class="flex-1 bg-surface-100 dark:bg-surface-700 rounded-full h-2 overflow-hidden">
-              <div class="bg-red-500 dark:bg-red-400 h-full rounded-full" style="width: {(entry.count / stats.top_ips[0].count) * 100}%"></div>
-            </div>
-            <span class="text-xs text-surface-500 w-8 text-right">{entry.count}</span>
-          </div>
-        {/each}
-      </div>
-    </div>
-  {/if}
- 
-  <!-- Log table -->
-  <div class="bg-white dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 p-5 shadow-sm">
-    <div class="flex items-start justify-between gap-3 mb-3">
-      <div>
-        <h3 class="text-sm font-semibold text-surface-700 dark:text-surface-200">Event Log</h3>
-        <p class="text-xs text-surface-400 mt-1">Filter recent events by type, IP, or date range.</p>
-      </div>
-      {#if hasActiveFilters}
-        <button onclick={clearFilters} class="px-2.5 py-1 text-xs rounded-md transition-colors bg-surface-100 text-surface-500 hover:bg-surface-200 dark:bg-surface-700 dark:text-surface-300 dark:hover:bg-surface-600">
-          Clear filters
-        </button>
-      {/if}
-    </div>
- 
-    <div class="flex flex-wrap gap-1.5 mb-3">
-      {#each logTypeFilters as f}
-        <button onclick={() => applyTypeFilter(f)} class="px-2.5 py-1 text-xs rounded-md transition-colors {filter === f ? 'bg-surface-800 text-white dark:bg-surface-200 dark:text-surface-900' : 'bg-surface-100 text-surface-500 hover:bg-surface-200 dark:bg-surface-700 dark:text-surface-300 dark:hover:bg-surface-600'}">
-          {logTypeButtonLabels[f]}
-        </button>
-      {/each}
-    </div>
- 
-    <div class="grid grid-cols-1 sm:grid-cols-3 gap-3 mb-3">
-      <label class="block">
-        <span class="block text-xs font-medium text-surface-500 mb-1">Filter by IP</span>
-        <input
-          type="text"
-          bind:value={ipFilter}
-          placeholder="e.g. 1.2.3.4"
-          onkeydown={async (e) => { if (e.key === 'Enter') await loadLogs(); }}
-          class="w-full px-3 py-2 text-sm border border-surface-200 dark:border-surface-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 dark:bg-surface-700 dark:text-white"
-        />
-      </label>
-      <label class="block">
-        <span class="block text-xs font-medium text-surface-500 mb-1">From</span>
-        <input
-          type="date"
-          bind:value={startDate}
-          onchange={loadLogs}
-          class="w-full px-3 py-2 text-sm border border-surface-200 dark:border-surface-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 dark:bg-surface-700 dark:text-white"
-        />
-      </label>
-      <label class="block">
-        <span class="block text-xs font-medium text-surface-500 mb-1">To</span>
-        <input
-          type="date"
-          bind:value={endDate}
-          onchange={loadLogs}
-          class="w-full px-3 py-2 text-sm border border-surface-200 dark:border-surface-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 dark:bg-surface-700 dark:text-white"
-        />
-      </label>
-    </div>
- 
-    <div class="flex items-center gap-2 mb-3">
-      <button onclick={loadLogs} class="px-3 py-1.5 text-xs font-medium text-primary-600 bg-primary-50 hover:bg-primary-100 rounded-lg transition-colors dark:bg-primary-900/30 dark:text-primary-300">
-        Apply filters
-      </button>
-      {#if ipFilter.trim()}
-        <button onclick={() => applyIpFilter(ipFilter.trim())} class="px-2.5 py-1 text-xs rounded-md transition-colors bg-surface-100 text-surface-500 hover:bg-surface-200 dark:bg-surface-700 dark:text-surface-300 dark:hover:bg-surface-600">
-          Use typed IP
-        </button>
-      {/if}
-    </div>
- 
-    {#if filterError}
-      <p class="text-sm text-rose-500 mb-3">{filterError}</p>
-    {/if}
- 
-    {#if logs.length > 0}
-      <div class="max-h-96 overflow-auto rounded-lg bg-surface-950 p-3 space-y-0.5">
-        {#each logs as e}
-          <div class="text-xs font-mono flex gap-2 leading-5">
-            <span class="{typeColors[e.type] || 'text-surface-500'} w-12 shrink-0">{typeLabels[e.type] || e.type}</span>
-            <span class="text-surface-500 shrink-0">{e.timestamp}</span>
-            {#if e.ip}
-              <button onclick={() => applyIpFilter(e.ip)} class="text-surface-400 hover:text-primary-300 shrink-0 w-28 text-left transition-colors">{e.ip}</button>
-            {/if}
-            {#if e.username}
-              <span class="text-red-400 shrink-0">{e.username}</span>
-            {/if}
-            {#if isBlockedScannerEvent(e)}
-              <span class="shrink-0 rounded-md bg-orange-500/15 px-1.5 py-0.5 text-[10px] font-medium text-orange-300 border border-orange-500/30">scanner</span>
-            {/if}
-            <span class="text-surface-300 truncate">{e.message}</span>
-          </div>
-        {/each}
-      </div>
-    {:else if !loading}
-      <p class="text-sm text-surface-400">{hasActiveFilters ? "No security events match the current filters" : "No security events found"}</p>
-    {/if}
-  </div>
-</div>
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/dashboard/frontend/coverage/src/routes/Settings.svelte.html b/dashboard/frontend/coverage/src/routes/Settings.svelte.html deleted file mode 100644 index e2b81bf..0000000 --- a/dashboard/frontend/coverage/src/routes/Settings.svelte.html +++ /dev/null @@ -1,1597 +0,0 @@ - - - - - - Code coverage report for src/routes/Settings.svelte - - - - - - - - - -
-
-

All files / src/routes Settings.svelte

-
- -
- 0% - Statements - 0/362 -
- - -
- 0% - Branches - 0/1 -
- - -
- 0% - Functions - 0/1 -
- - -
- 0% - Lines - 0/362 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -178 -179 -180 -181 -182 -183 -184 -185 -186 -187 -188 -189 -190 -191 -192 -193 -194 -195 -196 -197 -198 -199 -200 -201 -202 -203 -204 -205 -206 -207 -208 -209 -210 -211 -212 -213 -214 -215 -216 -217 -218 -219 -220 -221 -222 -223 -224 -225 -226 -227 -228 -229 -230 -231 -232 -233 -234 -235 -236 -237 -238 -239 -240 -241 -242 -243 -244 -245 -246 -247 -248 -249 -250 -251 -252 -253 -254 -255 -256 -257 -258 -259 -260 -261 -262 -263 -264 -265 -266 -267 -268 -269 -270 -271 -272 -273 -274 -275 -276 -277 -278 -279 -280 -281 -282 -283 -284 -285 -286 -287 -288 -289 -290 -291 -292 -293 -294 -295 -296 -297 -298 -299 -300 -301 -302 -303 -304 -305 -306 -307 -308 -309 -310 -311 -312 -313 -314 -315 -316 -317 -318 -319 -320 -321 -322 -323 -324 -325 -326 -327 -328 -329 -330 -331 -332 -333 -334 -335 -336 -337 -338 -339 -340 -341 -342 -343 -344 -345 -346 -347 -348 -349 -350 -351 -352 -353 -354 -355 -356 -357 -358 -359 -360 -361 -362 -363 -364 -365 -366 -367 -368 -369 -370 -371 -372 -373 -374 -375 -376 -377 -378 -379 -380 -381 -382 -383 -384 -385 -386 -387 -388 -389 -390 -391 -392 -393 -394 -395 -396 -397 -398 -399 -400 -401 -402 -403 -404 -405 -406 -407 -408 -409 -410 -411 -412 -413 -414 -415 -416 -417 -418 -419 -420 -421 -422 -423 -424 -425 -426 -427 -428 -429 -430 -431 -432 -433 -434 -435 -436 -437 -438 -439 -440 -441 -442 -443 -444 -445 -446 -447 -448 -449 -450 -451 -452 -453 -454 -455 -456 -457 -458 -459 -460 -461 -462 -463 -464 -465 -466 -467 -468 -469 -470 -471 -472 -473 -474 -475 -476 -477 -478 -479 -480 -481 -482 -483 -484 -485 -486 -487 -488 -489 -490 -491 -492 -493 -494 -495 -496 -497 -498 -499 -500 -501 -502 -503 -504 -505  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  - 
<script>
-  import { post, get, del, put } from "../lib/api.js";
-  import { onMount } from "svelte";
-  import { currentUser } from "../lib/api.js";
- 
-  let currentPassword = $state("");
-  let newPassword = $state("");
-  let confirmPassword = $state("");
-  let message = $state("");
-  let error = $state("");
-  let saving = $state(false);
- 
-  let passkeys = $state([]);
-  let passkeyMsg = $state("");
-  let passkeyErr = $state("");
-  let passkeyLoading = $state(false);
- 
-  async function loadPasskeys() {
-    try {
-      const res = await get("/auth/passkey/list");
-      passkeys = res.passkeys;
-    } catch {}
-  }
- 
-  function base64urlToBuffer(b64) {
-    const s = b64.replace(/-/g, '+').replace(/_/g, '/');
-    const raw = atob(s);
-    return Uint8Array.from(raw, c => c.charCodeAt(0)).buffer;
-  }
- 
-  function bufferToBase64url(buf) {
-    const bytes = new Uint8Array(buf);
-    let s = '';
-    bytes.forEach(b => s += String.fromCharCode(b));
-    return btoa(s).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
-  }
- 
-  async function registerPasskey() {
-    alert("registerPasskey called!");
-    passkeyLoading = true;
-    passkeyMsg = ""; passkeyErr = "";
-    try {
-      alert("Checking PublicKeyCredential support...");
-      if (!window.PublicKeyCredential) {
-        alert("PublicKeyCredential NOT supported!");
-        throw new Error("Passkeys are not supported in this browser or context");
-      }
-      alert("Fetching registration options...");
-      const opts = await post("/auth/passkey/register/options");
-      alert("Got options: " + JSON.stringify(opts).substring(0, 100));
-      opts.challenge = base64urlToBuffer(opts.challenge);
-      opts.user.id = base64urlToBuffer(opts.user.id);
-      if (opts.excludeCredentials) {
-        opts.excludeCredentials = opts.excludeCredentials.map(c => ({ ...c, id: base64urlToBuffer(c.id) }));
-      }
-      const cred = await navigator.credentials.create({ publicKey: opts });
-      if (!cred) {
-        throw new Error("Passkey creation was cancelled");
-      }
-      const name = prompt("Name this passkey (e.g. MacBook, iPhone):", "Passkey") || "Passkey";
-      const body = {
-        id: cred.id,
-        rawId: bufferToBase64url(cred.rawId),
-        type: cred.type,
-        name,
-        response: {
-          attestationObject: bufferToBase64url(cred.response.attestationObject),
-          clientDataJSON: bufferToBase64url(cred.response.clientDataJSON),
-        },
-      };
-      await post("/auth/passkey/register/verify", body);
-      passkeyMsg = "Passkey registered successfully";
-      await loadPasskeys();
-    } catch (e) {
-      console.error("Passkey registration error:", e);
-      const errorMsg = e.body?.detail || e.message || "Failed to register passkey";
-      passkeyErr = errorMsg;
-      alert("Passkey Error: " + errorMsg + "\n\nFull error: " + JSON.stringify(e, null, 2));
-    } finally {
-      passkeyLoading = false;
-    }
-  }
- 
-  async function clearPasskeys() {
-    if (!confirm("Remove all passkeys?")) return;
-    passkeyLoading = true;
-    passkeyMsg = ""; passkeyErr = "";
-    try {
-      await post("/auth/passkey/clear");
-      passkeyMsg = "All passkeys removed";
-      passkeys = [];
-    } catch (e) {
-      passkeyErr = e.body?.detail || "Failed to remove passkeys";
-    }
-    passkeyLoading = false;
-  }
- 
-  async function deletePasskey(id, name) {
-    if (!confirm(`Remove passkey "${name}"?`)) return;
-    passkeyLoading = true;
-    passkeyMsg = ""; passkeyErr = "";
-    try {
-      await post("/auth/passkey/delete", { id });
-      passkeyMsg = `Passkey "${name}" removed`;
-      await loadPasskeys();
-    } catch (e) {
-      passkeyErr = e.body?.detail || "Failed to remove passkey";
-    }
-    passkeyLoading = false;
-  }
- 
-  async function changePassword() {
-    error = "";
-    message = "";
-    if (newPassword !== confirmPassword) { error = "Passwords do not match"; return; }
-    if (newPassword.length < 8) { error = "Password must be at least 8 characters"; return; }
-    saving = true;
-    try {
-      await post("/auth/change-password", { current_password: currentPassword, new_password: newPassword });
-      message = "Password changed successfully";
-      currentPassword = ""; newPassword = ""; confirmPassword = "";
-    } catch (e) {
-      error = e.body?.detail || "Failed to change password";
-    }
-    saving = false;
-  }
- 
-  let auditEntries = $state([]);
-  let auditLoading = $state(false);
-  let auditFilter = $state("all");
- 
-  const levelColors = { high: "text-rose-400", medium: "text-amber-400", low: "text-emerald-400", info: "text-surface-400" };
-  const levelLabels = { high: "HIGH", medium: "MED", low: "LOW", info: "INFO" };
- 
-  let filteredEntries = $derived(auditFilter === "all" ? auditEntries : auditEntries.filter(e => e.level === auditFilter));
- 
-  async function loadAuditLog() {
-    auditLoading = true;
-    try {
-      const res = await get("/system/audit-log?lines=200");
-      auditEntries = res.entries.reverse();
-    } catch {}
-    auditLoading = false;
-  }
- 
-  // Access Control
-  const allPages = ["dashboard", "docker", "files", "terminal", "security", "openclaw", "chat-digest", "gitea", "settings"];
-  const allSidebarLinks = ["Overview", "Docker", "Files", "Terminal", "Security", "Navidrome", "Jellyfin", "Audiobookshelf", "Immich", "OpenClaw", "Chat Digest", "Repos", "Gitea Web", "Stirling PDF", "Vaultwarden", "n8n", "Speedtest"];
-  let rbacConfig = $state(null);
-  let rbacLoading = $state(false);
-  let rbacMsg = $state("");
-  let rbacErr = $state("");
-  let editingUser = $state("");
-  let editPages = $state([]);
-  let newUsername = $state("");
-  let editingRole = $state("");
-  let editRolePages = $state([]);
-  let editRoleSidebarLinks = $state([]);
- 
-  async function loadRbac() {
-    rbacLoading = true;
-    try {
-      rbacConfig = await get("/auth/rbac/config");
-    } catch {}
-    rbacLoading = false;
-  }
- 
-  function startEdit(username, pages) {
-    editingUser = username;
-    editPages = [...pages];
-    newUsername = "";
-    editingRole = "";
-  }
- 
-  function startAdd() {
-    editingUser = "__new__";
-    editPages = ["dashboard"];
-    newUsername = "";
-    editingRole = "";
-  }
- 
-  function cancelEdit() {
-    editingUser = "";
-    editPages = [];
-    newUsername = "";
-    editingRole = "";
-    editRolePages = [];
-    editRoleSidebarLinks = [];
-  }
- 
-  function startEditRole(role, pages, sidebarLinks) {
-    editingRole = role;
-    editRolePages = pages === "*" ? ["*"] : [...pages];
-    editRoleSidebarLinks = sidebarLinks === "*" ? ["*"] : (sidebarLinks ? [...sidebarLinks] : ["*"]);
-    editingUser = "";
-  }
- 
-  function toggleRolePage(p) {
-    if (p === "*") {
-      editRolePages = ["*"];
-    } else {
-      if (editRolePages.includes("*")) editRolePages = [];
-      if (editRolePages.includes(p)) editRolePages = editRolePages.filter(x => x !== p);
-      else editRolePages = [...editRolePages, p];
-    }
-  }
- 
-  function toggleRoleSidebarLink(link) {
-    if (link === "*") {
-      editRoleSidebarLinks = ["*"];
-    } else {
-      if (editRoleSidebarLinks.includes("*")) editRoleSidebarLinks = [];
-      if (editRoleSidebarLinks.includes(link)) editRoleSidebarLinks = editRoleSidebarLinks.filter(x => x !== link);
-      else editRoleSidebarLinks = [...editRoleSidebarLinks, link];
-    }
-  }
- 
-  function togglePage(p) {
-    if (editPages.includes(p)) editPages = editPages.filter(x => x !== p);
-    else editPages = [...editPages, p];
-  }
- 
-  async function saveOverride() {
-    rbacMsg = ""; rbacErr = "";
-    const uname = editingUser === "__new__" ? newUsername.trim() : editingUser;
-    if (!uname) { rbacErr = "Username required"; return; }
-    if (!editPages.length) { rbacErr = "Select at least one page"; return; }
-    try {
-      await put(`/auth/rbac/overrides/${encodeURIComponent(uname)}`, { pages: editPages });
-      rbacMsg = `Saved override for ${uname}`;
-      cancelEdit();
-      await loadRbac();
-    } catch (e) {
-      rbacErr = e.body?.detail || "Failed to save";
-    }
-  }
- 
-  async function deleteOverride(username) {
-    if (!confirm(`Remove override for "${username}"?`)) return;
-    rbacMsg = ""; rbacErr = "";
-    try {
-      await del(`/auth/rbac/overrides/${encodeURIComponent(username)}`);
-      rbacMsg = `Removed override for ${username}`;
-      await loadRbac();
-    } catch (e) {
-      rbacErr = e.body?.detail || "Failed to delete";
-    }
-  }
- 
-  async function saveRoleDefault() {
-    rbacMsg = ""; rbacErr = "";
-    if (!editingRole) return;
-    if (!editRolePages.length) { rbacErr = "Select at least one page or '*' for all"; return; }
-    if (!editRoleSidebarLinks.length) { rbacErr = "Select at least one sidebar link or '*' for all"; return; }
-    try {
-      const pages = editRolePages.includes("*") ? "*" : editRolePages;
-      const sidebar_links = editRoleSidebarLinks.includes("*") ? "*" : editRoleSidebarLinks;
-      await put(`/auth/rbac/roles/${encodeURIComponent(editingRole)}`, { pages, sidebar_links });
-      rbacMsg = `Updated role default for ${editingRole}`;
-      cancelEdit();
-      await loadRbac();
-    } catch (e) {
-      rbacErr = e.body?.detail || "Failed to save role";
-    }
-  }
- 
-  onMount(() => {
-    loadPasskeys();
-    if (currentUser.role === "admin") loadRbac();
-  });
-</script>
- 
-<div class="space-y-6">
-  <div>
-    <h1 class="text-2xl font-bold text-surface-900 dark:text-white tracking-tight">Settings</h1>
-    <p class="text-sm text-surface-400 mt-1">Account and security settings</p>
-  </div>
- 
-  <div class="bg-white dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 p-6 shadow-sm max-w-md">
-    <h3 class="text-sm font-semibold text-surface-700 dark:text-surface-200 mb-4">Change Password</h3>
- 
-    {#if message}
-      <div class="text-sm text-emerald-600 bg-emerald-50 px-3 py-2 rounded-lg mb-4">{message}</div>
-    {/if}
-    {#if error}
-      <div class="text-sm text-rose-600 bg-rose-50 px-3 py-2 rounded-lg mb-4">{error}</div>
-    {/if}
- 
-    <form onsubmit={(e) => { e.preventDefault(); changePassword(); }} class="space-y-3">
-      <div>
-        <label class="block text-xs font-medium text-surface-500 mb-1">Current Password</label>
-        <input type="password" bind:value={currentPassword} required class="w-full px-3 py-2 text-sm border border-surface-200 dark:border-surface-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 dark:bg-surface-700 dark:text-white" />
-      </div>
-      <div>
-        <label class="block text-xs font-medium text-surface-500 mb-1">New Password</label>
-        <input type="password" bind:value={newPassword} required class="w-full px-3 py-2 text-sm border border-surface-200 dark:border-surface-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 dark:bg-surface-700 dark:text-white" />
-      </div>
-      <div>
-        <label class="block text-xs font-medium text-surface-500 mb-1">Confirm New Password</label>
-        <input type="password" bind:value={confirmPassword} required class="w-full px-3 py-2 text-sm border border-surface-200 dark:border-surface-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 dark:bg-surface-700 dark:text-white" />
-      </div>
-      <button type="submit" disabled={saving} class="w-full px-4 py-2 text-sm font-medium text-white bg-primary-600 hover:bg-primary-700 rounded-lg transition-colors disabled:opacity-50">
-        {saving ? "Saving..." : "Change Password"}
-      </button>
-    </form>
-  </div>
- 
-  <div class="bg-white dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 p-6 shadow-sm max-w-md">
-    <h3 class="text-sm font-semibold text-surface-700 dark:text-surface-200 mb-4">Passkeys</h3>
- 
-    {#if passkeyMsg}
-      <div class="text-sm text-emerald-600 bg-emerald-50 px-3 py-2 rounded-lg mb-4">{passkeyMsg}</div>
-    {/if}
-    {#if passkeyErr}
-      <div class="text-sm text-rose-600 bg-rose-50 px-3 py-2 rounded-lg mb-4">{passkeyErr}</div>
-    {/if}
- 
-    {#if passkeys.length > 0}
-      <div class="space-y-2 mb-4">
-        {#each passkeys as pk}
-          <div class="flex items-center justify-between px-3 py-2 bg-surface-50 dark:bg-surface-700 rounded-lg">
-            <div>
-              <span class="text-sm font-medium text-surface-700 dark:text-surface-200">{pk.name}</span>
-              {#if pk.created_at}
-                <span class="text-xs text-surface-400 ml-2">{new Date(pk.created_at).toLocaleDateString()}</span>
-              {/if}
-            </div>
-            <button onclick={() => deletePasskey(pk.id, pk.name)} disabled={passkeyLoading} class="text-xs text-rose-500 hover:text-rose-700 disabled:opacity-50">Remove</button>
-          </div>
-        {/each}
-      </div>
-    {:else}
-      <p class="text-sm text-surface-400 mb-4">No passkeys registered</p>
-    {/if}
- 
-    <div class="flex gap-2">
-      <button onclick={registerPasskey} disabled={passkeyLoading} class="px-4 py-2 text-sm font-medium text-white bg-primary-600 hover:bg-primary-700 rounded-lg transition-colors disabled:opacity-50">
-        Register new passkey
-      </button>
-      {#if passkeys.length > 1}
-        <button onclick={clearPasskeys} disabled={passkeyLoading} class="px-4 py-2 text-sm font-medium text-rose-600 bg-rose-50 hover:bg-rose-100 rounded-lg transition-colors disabled:opacity-50">
-          Remove all
-        </button>
-      {/if}
-    </div>
-  </div>
- 
-  {#if currentUser.role === "admin" && rbacConfig}
-  <div class="bg-white dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 p-6 shadow-sm">
-    <div class="flex items-center justify-between mb-4">
-      <h3 class="text-sm font-semibold text-surface-700 dark:text-surface-200">Access Control</h3>
-      <button onclick={loadRbac} disabled={rbacLoading} class="px-3 py-1.5 text-xs font-medium text-primary-600 bg-primary-50 hover:bg-primary-100 rounded-lg transition-colors disabled:opacity-50">
-        {rbacLoading ? "Loading..." : "Refresh"}
-      </button>
-    </div>
- 
-    {#if rbacMsg}
-      <div class="text-sm text-emerald-600 bg-emerald-50 px-3 py-2 rounded-lg mb-4">{rbacMsg}</div>
-    {/if}
-    {#if rbacErr}
-      <div class="text-sm text-rose-600 bg-rose-50 px-3 py-2 rounded-lg mb-4">{rbacErr}</div>
-    {/if}
- 
-    <!-- Role Defaults -->
-    <div class="mb-4">
-      <p class="text-xs font-medium text-surface-500 mb-2">Role Defaults</p>
-      <div class="space-y-2">
-        {#each Object.entries(rbacConfig.role_defaults || {}) as [role, cfg]}
-          {#if editingRole === role}
-            <div class="p-3 bg-surface-50 dark:bg-surface-700 rounded-lg space-y-2">
-              <p class="text-xs font-medium text-surface-700 dark:text-surface-200">{role}</p>
-              <p class="text-[10px] text-surface-500 mt-1">Pages (internal dashboard pages)</p>
-              <div class="flex flex-wrap gap-1.5">
-                <button onclick={() => toggleRolePage("*")} class="px-2 py-0.5 text-xs rounded-md transition-colors {editRolePages.includes('*') ? 'bg-primary-600 text-white' : 'bg-surface-200 dark:bg-surface-600 text-surface-500'}">* (all)</button>
-                {#each allPages as p}
-                  <button onclick={() => toggleRolePage(p)} class="px-2 py-0.5 text-xs rounded-md transition-colors {editRolePages.includes(p) ? 'bg-primary-600 text-white' : 'bg-surface-200 dark:bg-surface-600 text-surface-500'}">{p}</button>
-                {/each}
-              </div>
-              <p class="text-[10px] text-surface-500 mt-2">Sidebar Links (all services)</p>
-              <div class="flex flex-wrap gap-1.5">
-                <button onclick={() => toggleRoleSidebarLink("*")} class="px-2 py-0.5 text-xs rounded-md transition-colors {editRoleSidebarLinks.includes('*') ? 'bg-primary-600 text-white' : 'bg-surface-200 dark:bg-surface-600 text-surface-500'}">* (all)</button>
-                {#each allSidebarLinks as link}
-                  <button onclick={() => toggleRoleSidebarLink(link)} class="px-2 py-0.5 text-xs rounded-md transition-colors {editRoleSidebarLinks.includes(link) ? 'bg-primary-600 text-white' : 'bg-surface-200 dark:bg-surface-600 text-surface-500'}">{link}</button>
-                {/each}
-              </div>
-              <div class="flex gap-2">
-                <button onclick={saveRoleDefault} class="px-3 py-1 text-xs font-medium text-white bg-primary-600 hover:bg-primary-700 rounded-md transition-colors">Save</button>
-                <button onclick={cancelEdit} class="px-3 py-1 text-xs font-medium text-surface-600 hover:text-surface-800 dark:text-surface-400 dark:hover:text-surface-200">Cancel</button>
-              </div>
-            </div>
-          {:else}
-            <div class="flex items-center justify-between p-2 bg-surface-50 dark:bg-surface-700 rounded-lg">
-              <div class="flex flex-col gap-1 text-xs">
-                <div class="flex items-center gap-2">
-                  <span class="font-medium text-surface-700 dark:text-surface-300 w-16">{role}</span>
-                  <span class="text-surface-400">Pages: {cfg.pages === "*" ? "All" : (cfg.pages || []).join(", ")}</span>
-                </div>
-                <div class="flex items-center gap-2">
-                  <span class="w-16"></span>
-                  <span class="text-surface-400">Links: {cfg.sidebar_links === "*" ? "All" : (cfg.sidebar_links || ["*"]).join(", ")}</span>
-                </div>
-              </div>
-              <button onclick={() => startEditRole(role, cfg.pages, cfg.sidebar_links)} class="text-xs text-primary-500 hover:text-primary-700">Edit</button>
-            </div>
-          {/if}
-        {/each}
-      </div>
-    </div>
- 
-    <!-- User Overrides -->
-    <div class="mb-4">
-      <div class="flex items-center justify-between mb-2">
-        <p class="text-xs font-medium text-surface-500">User Overrides</p>
-        {#if editingUser !== "__new__"}
-          <button onclick={startAdd} class="px-2 py-1 text-xs font-medium text-primary-600 bg-primary-50 hover:bg-primary-100 rounded-md transition-colors">Add Override</button>
-        {/if}
-      </div>
- 
-      {#if Object.keys(rbacConfig.user_overrides || {}).length > 0}
-        <div class="space-y-2">
-          {#each Object.entries(rbacConfig.user_overrides) as [username, cfg]}
-            {#if editingUser === username}
-              <div class="p-3 bg-surface-50 dark:bg-surface-700 rounded-lg space-y-2">
-                <p class="text-xs font-medium text-surface-700 dark:text-surface-200">{username}</p>
-                <div class="flex flex-wrap gap-1.5">
-                  {#each allPages as p}
-                    <button onclick={() => togglePage(p)} class="px-2 py-0.5 text-xs rounded-md transition-colors {editPages.includes(p) ? 'bg-primary-600 text-white' : 'bg-surface-200 dark:bg-surface-600 text-surface-500'}">{p}</button>
-                  {/each}
-                </div>
-                <div class="flex gap-2">
-                  <button onclick={saveOverride} class="px-3 py-1 text-xs font-medium text-white bg-primary-600 hover:bg-primary-700 rounded-md">Save</button>
-                  <button onclick={cancelEdit} class="px-3 py-1 text-xs font-medium text-surface-500 bg-surface-200 hover:bg-surface-300 rounded-md">Cancel</button>
-                </div>
-              </div>
-            {:else}
-              <div class="flex items-center justify-between px-3 py-2 bg-surface-50 dark:bg-surface-700 rounded-lg">
-                <div>
-                  <span class="text-sm font-medium text-surface-700 dark:text-surface-200">{username}</span>
-                  <span class="text-xs text-surface-400 ml-2">{(cfg.pages || []).join(", ")}</span>
-                </div>
-                <div class="flex gap-1.5">
-                  <button onclick={() => startEdit(username, cfg.pages || [])} class="text-xs text-primary-500 hover:text-primary-700">Edit</button>
-                  <button onclick={() => deleteOverride(username)} class="text-xs text-rose-500 hover:text-rose-700">Delete</button>
-                </div>
-              </div>
-            {/if}
-          {/each}
-        </div>
-      {:else if editingUser !== "__new__"}
-        <p class="text-sm text-surface-400">No user overrides configured</p>
-      {/if}
- 
-      {#if editingUser === "__new__"}
-        <div class="p-3 bg-surface-50 dark:bg-surface-700 rounded-lg space-y-2 mt-2">
-          <input type="text" bind:value={newUsername} placeholder="Username" class="w-full px-3 py-1.5 text-sm border border-surface-200 dark:border-surface-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 dark:bg-surface-800 dark:text-white" />
-          <div class="flex flex-wrap gap-1.5">
-            {#each allPages as p}
-              <button onclick={() => togglePage(p)} class="px-2 py-0.5 text-xs rounded-md transition-colors {editPages.includes(p) ? 'bg-primary-600 text-white' : 'bg-surface-200 dark:bg-surface-600 text-surface-500'}">{p}</button>
-            {/each}
-          </div>
-          <div class="flex gap-2">
-            <button onclick={saveOverride} class="px-3 py-1 text-xs font-medium text-white bg-primary-600 hover:bg-primary-700 rounded-md">Save</button>
-            <button onclick={cancelEdit} class="px-3 py-1 text-xs font-medium text-surface-500 bg-surface-200 hover:bg-surface-300 rounded-md">Cancel</button>
-          </div>
-        </div>
-      {/if}
-    </div>
-  </div>
-  {/if}
- 
-  <div class="bg-white dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 p-6 shadow-sm">
-    <div class="flex items-center justify-between mb-4">
-      <h3 class="text-sm font-semibold text-surface-700 dark:text-surface-200">Audit Log</h3>
-      <button onclick={loadAuditLog} disabled={auditLoading} class="px-3 py-1.5 text-xs font-medium text-primary-600 bg-primary-50 hover:bg-primary-100 rounded-lg transition-colors disabled:opacity-50">
-        {auditLoading ? "Loading..." : auditEntries.length ? "Refresh" : "Load"}
-      </button>
-    </div>
-    {#if auditEntries.length > 0}
-      <div class="flex gap-1.5 mb-3">
-        {#each ["all", "high", "medium", "low", "info"] as lvl}
-          <button onclick={() => auditFilter = lvl} class="px-2.5 py-1 text-xs rounded-md transition-colors {auditFilter === lvl ? 'bg-surface-800 text-white' : 'bg-surface-100 text-surface-500 hover:bg-surface-200'}">
-            {lvl === "all" ? "All" : levelLabels[lvl] || lvl}
-            {#if lvl !== "all"}
-              <span class="ml-1 opacity-60">{auditEntries.filter(e => e.level === lvl).length}</span>
-            {/if}
-          </button>
-        {/each}
-      </div>
-      <div class="max-h-80 overflow-auto rounded-lg bg-surface-950 p-3 space-y-0.5">
-        {#each filteredEntries as e}
-          <div class="text-xs font-mono flex gap-2 leading-5">
-            <span class="{levelColors[e.level]} w-8 shrink-0">{levelLabels[e.level]}</span>
-            <span class="text-surface-500 shrink-0">{e.ts.replace('T', ' ')}</span>
-            <span class="text-surface-400 shrink-0 w-10">{e.status}</span>
-            <span class="text-surface-300 shrink-0">{e.user}</span>
-            <span class="text-surface-500">{e.method} {e.path}</span>
-          </div>
-        {/each}
-      </div>
-    {:else if !auditLoading}
-      <p class="text-sm text-surface-400">Click Load to view recent audit log entries</p>
-    {/if}
-  </div>
-</div>
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/dashboard/frontend/coverage/src/routes/Terminal.svelte.html b/dashboard/frontend/coverage/src/routes/Terminal.svelte.html deleted file mode 100644 index c3a9e5f..0000000 --- a/dashboard/frontend/coverage/src/routes/Terminal.svelte.html +++ /dev/null @@ -1,2494 +0,0 @@ - - - - - - Code coverage report for src/routes/Terminal.svelte - - - - - - - - - -
-
-

All files / src/routes Terminal.svelte

-
- -
- 0% - Statements - 0/545 -
- - -
- 0% - Branches - 0/1 -
- - -
- 0% - Functions - 0/1 -
- - -
- 0% - Lines - 0/545 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -178 -179 -180 -181 -182 -183 -184 -185 -186 -187 -188 -189 -190 -191 -192 -193 -194 -195 -196 -197 -198 -199 -200 -201 -202 -203 -204 -205 -206 -207 -208 -209 -210 -211 -212 -213 -214 -215 -216 -217 -218 -219 -220 -221 -222 -223 -224 -225 -226 -227 -228 -229 -230 -231 -232 -233 -234 -235 -236 -237 -238 -239 -240 -241 -242 -243 -244 -245 -246 -247 -248 -249 -250 -251 -252 -253 -254 -255 -256 -257 -258 -259 -260 -261 -262 -263 -264 -265 -266 -267 -268 -269 -270 -271 -272 -273 -274 -275 -276 -277 -278 -279 -280 -281 -282 -283 -284 -285 -286 -287 -288 -289 -290 -291 -292 -293 -294 -295 -296 -297 -298 -299 -300 -301 -302 -303 -304 -305 -306 -307 -308 -309 -310 -311 -312 -313 -314 -315 -316 -317 -318 -319 -320 -321 -322 -323 -324 -325 -326 -327 -328 -329 -330 -331 -332 -333 -334 -335 -336 -337 -338 -339 -340 -341 -342 -343 -344 -345 -346 -347 -348 -349 -350 -351 -352 -353 -354 -355 -356 -357 -358 -359 -360 -361 -362 -363 -364 -365 -366 -367 -368 -369 -370 -371 -372 -373 -374 -375 -376 -377 -378 -379 -380 -381 -382 -383 -384 -385 -386 -387 -388 -389 -390 -391 -392 -393 -394 -395 -396 -397 -398 -399 -400 -401 -402 -403 -404 -405 -406 -407 -408 -409 -410 -411 -412 -413 -414 -415 -416 -417 -418 -419 -420 -421 -422 -423 -424 -425 -426 -427 -428 -429 -430 -431 -432 -433 -434 -435 -436 -437 -438 -439 -440 -441 -442 -443 -444 -445 -446 -447 -448 -449 -450 -451 -452 -453 -454 -455 -456 -457 -458 -459 -460 -461 -462 -463 -464 -465 -466 -467 -468 -469 -470 -471 -472 -473 -474 -475 -476 -477 -478 -479 -480 -481 -482 -483 -484 -485 -486 -487 -488 -489 -490 -491 -492 -493 -494 -495 -496 -497 -498 -499 -500 -501 -502 -503 -504 -505 -506 -507 -508 -509 -510 -511 -512 -513 -514 -515 -516 -517 -518 -519 -520 -521 -522 -523 -524 -525 -526 -527 -528 -529 -530 -531 -532 -533 -534 -535 -536 -537 -538 -539 -540 -541 -542 -543 -544 -545 -546 -547 -548 -549 -550 -551 -552 -553 -554 -555 -556 -557 -558 -559 -560 -561 -562 -563 -564 -565 -566 -567 -568 -569 -570 -571 -572 -573 -574 -575 -576 -577 -578 -579 -580 -581 -582 -583 -584 -585 -586 -587 -588 -589 -590 -591 -592 -593 -594 -595 -596 -597 -598 -599 -600 -601 -602 -603 -604 -605 -606 -607 -608 -609 -610 -611 -612 -613 -614 -615 -616 -617 -618 -619 -620 -621 -622 -623 -624 -625 -626 -627 -628 -629 -630 -631 -632 -633 -634 -635 -636 -637 -638 -639 -640 -641 -642 -643 -644 -645 -646 -647 -648 -649 -650 -651 -652 -653 -654 -655 -656 -657 -658 -659 -660 -661 -662 -663 -664 -665 -666 -667 -668 -669 -670 -671 -672 -673 -674 -675 -676 -677 -678 -679 -680 -681 -682 -683 -684 -685 -686 -687 -688 -689 -690 -691 -692 -693 -694 -695 -696 -697 -698 -699 -700 -701 -702 -703 -704 -705 -706 -707 -708 -709 -710 -711 -712 -713 -714 -715 -716 -717 -718 -719 -720 -721 -722 -723 -724 -725 -726 -727 -728 -729 -730 -731 -732 -733 -734 -735 -736 -737 -738 -739 -740 -741 -742 -743 -744 -745 -746 -747 -748 -749 -750 -751 -752 -753 -754 -755 -756 -757 -758 -759 -760 -761 -762 -763 -764 -765 -766 -767 -768 -769 -770 -771 -772 -773 -774 -775 -776 -777 -778 -779 -780 -781 -782 -783 -784 -785 -786 -787 -788 -789 -790 -791 -792 -793 -794 -795 -796 -797 -798 -799 -800 -801 -802 -803 -804  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  - 
<script>
-  import { onMount, onDestroy } from "svelte";
-  import { VoiceSession } from "../lib/voice.js";
-  import { tryRefreshSession } from "../lib/api.js";
- 
-  const PERSISTENCE_STATUS_PREFIX = "__DASHBOARD_PERSISTENCE__:";
-  const PERSISTENCE_STATUS_PREFIX_BYTES = new TextEncoder().encode(PERSISTENCE_STATUS_PREFIX);
- 
-  const hosts = [
-    { id: "nas", label: "NAS" },
-    { id: "vps", label: "VPS" },
-    { id: "caddy-vps", label: "Caddy VPS" },
-    { id: "mac", label: "Mac" },
-    { id: "claude-dev", label: "Claude Dev", persistent: true },
-  ];
- 
-  const DARK_THEME = {
-    background: "#0f172a", foreground: "#e2e8f0", cursor: "#6366f1",
-    cursorAccent: "#0f172a", selectionBackground: "#334155",
-    black: "#0f172a", red: "#f43f5e", green: "#10b981",
-    yellow: "#f59e0b", blue: "#3b82f6", magenta: "#a855f7",
-    cyan: "#06b6d4", white: "#e2e8f0",
-  };
- 
-  const LIGHT_THEME = {
-    background: "#f8fafc", foreground: "#0f172a", cursor: "#4f46e5",
-    cursorAccent: "#f8fafc", selectionBackground: "#cbd5e1",
-    black: "#0f172a", red: "#e11d48", green: "#059669",
-    yellow: "#d97706", blue: "#2563eb", magenta: "#9333ea",
-    cyan: "#0891b2", white: "#334155",
-  };
- 
-  let tabs = $state([]);
-  let activeTab = $state(null);
-  let showMenu = $state(false);
-  let voiceTick = $state(0);
-  let isDark = $state(false);
-  let isFullscreen = $state(false);
-  let showActionButtons = $state(false);
-  let tabCounter = 0;
-  const tabData = new Map();
- 
-  function currentTheme() {
-    return isDark ? DARK_THEME : LIGHT_THEME;
-  }
- 
-  function hostById(hostId) {
-    return hosts.find((h) => h.id === hostId);
-  }
- 
-  function openFreshSession(tab) {
-    const url = new URL(window.location.href);
-    url.searchParams.set("page", "terminal");
-    url.searchParams.set("host", tab.hostId);
-    window.open(`${url.pathname}${url.search}${url.hash}`, "_blank", "noopener");
-  }
- 
-  function persistenceLabel(status, reconnecting) {
-    if (status === "created") return "created new persistent session";
-    if (reconnecting) return "reattached to persistent session";
-    return "attached to persistent session";
-  }
- 
-  function applyThemeToTerminals() {
-    const theme = currentTheme();
-    tabData.forEach((d) => {
-      d.term?.options && (d.term.options.theme = theme);
-    });
-  }
- 
-  function syncTheme() {
-    isDark = document.documentElement.classList.contains("dark");
-    applyThemeToTerminals();
-  }
- 
-  const themeObserver = new MutationObserver((mutations) => {
-    if (mutations.some((m) => m.attributeName === "class")) syncTheme();
-  });
-  onMount(() => {
-    syncTheme();
-    themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ["class"] });
-    const requestedHost = new URLSearchParams(window.location.search).get("host");
-    if (requestedHost && !tabs.length && hostById(requestedHost)) addTab(requestedHost);
-  });
- 
-  function cleanName(name) {
-    return (name || "image")
-      .replace(/[^a-zA-Z0-9._-]/g, "-")
-      .replace(/-+/g, "-")
-      .replace(/^-|-$/g, "") || "image";
-  }
- 
-  function extensionFor(type) {
-    if (type === "image/png") return "png";
-    if (type === "image/jpeg") return "jpg";
-    if (type === "image/webp") return "webp";
-    if (type === "image/gif") return "gif";
-    return "bin";
-  }
- 
-  async function fileToBase64(file) {
-    const buf = await file.arrayBuffer();
-    let binary = "";
-    const bytes = new Uint8Array(buf);
-    const chunkSize = 0x8000;
-    for (let i = 0; i < bytes.length; i += chunkSize) {
-      binary += String.fromCharCode(...bytes.subarray(i, i + chunkSize));
-    }
-    return btoa(binary);
-  }
- 
-  async function sendDroppedImage(term, ws, file) {
-    if (!file?.type?.startsWith("image/")) return;
-    if (ws.readyState !== 1) return;
- 
-    const ext = extensionFor(file.type);
-    const baseName = cleanName(file.name).replace(/\.[a-zA-Z0-9]+$/, "");
-    const stamp = Date.now();
-    const imagePath = `/tmp/claude-images/${baseName || "image"}-${stamp}.${ext}`;
-    const b64 = await fileToBase64(file);
- 
-    const cmd = [
-      "python3 - <<'PY'",
-      "import base64, pathlib",
-      `p = pathlib.Path(${JSON.stringify(imagePath)})`,
-      "p.parent.mkdir(parents=True, exist_ok=True)",
-      `p.write_bytes(base64.b64decode(${JSON.stringify(b64)}))`,
-      `print(f'saved image -> {p}')`,
-      "PY",
-      "",
-    ].join("\n");
- 
-    ws.send(new TextEncoder().encode(cmd));
-    term.write(`\r\n\x1b[90m[Uploaded image to ${imagePath}]\x1b[0m\r\n`);
-  }
- 
-  function addTab(hostId) {
-    const id = ++tabCounter;
-    const host = hostById(hostId);
-    tabs = [...tabs, {
-      id,
-      hostId,
-      label: host.label,
-      connected: false,
-      error: "",
-      statusBanner: host.persistent ? "Connecting to persistent session..." : "",
-      persistent: !!host.persistent,
-    }];
-    activeTab = id;
-    showMenu = false;
-  }
- 
-  function closeTab(id) {
-    const d = tabData.get(id);
-    if (d) {
-      d.closedManually = true;
-      if (d.reconnectTimer) clearTimeout(d.reconnectTimer);
-      d.voice?.destroy();
-      d.ro?.disconnect();
-      if (d.heartbeat) clearInterval(d.heartbeat);
-      d.term?.element?.removeEventListener("dragover", d.handleDragOver);
-      d.term?.element?.removeEventListener("drop", d.handleDrop);
-      d.term?.textarea?.removeEventListener("paste", d.handlePaste);
-      d.ws?.close();
-      d.term?.dispose();
-      tabData.delete(id);
-    }
-    tabs = tabs.filter((t) => t.id !== id);
-    if (activeTab === id) activeTab = tabs.length ? tabs[tabs.length - 1].id : null;
-  }
- 
-  function updateTab(tabId, props) {
-    tabs = tabs.map((t) => (t.id === tabId ? { ...t, ...props } : t));
-  }
- 
-  function initTerminal(el, tab) {
-    const tabId = tab.id;
-    (async () => {
-      const { Terminal } = await import("@xterm/xterm");
-      const { FitAddon } = await import("@xterm/addon-fit");
-      await import("@xterm/xterm/css/xterm.css");
-      const term = new Terminal({
-        cursorBlink: true, fontSize: 13,
-        fontFamily: "'JetBrains Mono', 'Fira Code', 'SF Mono', 'Menlo', monospace",
-        lineHeight: 1.4,
-        theme: currentTheme(),
-      });
-      const fit = new FitAddon();
-      term.loadAddon(fit);
-      term.open(el);
-      fit.fit();
- 
-      const proto = location.protocol === "https:" ? "wss:" : "ws:";
-      let heartbeat = null;
-      let pongTimeout = null;
-      let reconnectTimer = null;
-      let closedManually = false;
-      let reconnecting = false;
-      let authRecoveryInFlight = false;
-      let reconnectAttempts = 0;
-      let consecutiveAuthFailures = 0;
-      const MAX_RECONNECT_DELAY = 30000;
-      const MAX_AUTH_FAILURES = 3;
-      const PING_INTERVAL = 30000; // 30s between pings
-      const PONG_TIMEOUT = 10000; // 10s to receive pong
- 
-      function clearHeartbeat() {
-        if (heartbeat) {
-          clearInterval(heartbeat);
-          heartbeat = null;
-        }
-        if (pongTimeout) {
-          clearTimeout(pongTimeout);
-          pongTimeout = null;
-        }
-      }
- 
-      function handleAuthExpired(reason = "Session expired — please log in again") {
-        clearHeartbeat();
-        if (reconnectTimer) {
-          clearTimeout(reconnectTimer);
-          reconnectTimer = null;
-        }
-        reconnecting = false;
-        updateTab(tabId, {
-          connected: false,
-          error: reason,
-          statusBanner: "",
-        });
-        term.write(`\r\n\x1b[90m[${reason}]\x1b[0m`);
-        const d = tabData.get(tabId);
-        if (d) {
-          d.ws = null;
-          d.heartbeat = heartbeat;
-          d.reconnectTimer = reconnectTimer;
-          d.closedManually = closedManually;
-          d.reconnecting = reconnecting;
-        }
-      }
- 
-      function scheduleReconnect(reason) {
-        clearHeartbeat();
-        if (closedManually || reconnectTimer) return;
-        reconnecting = true;
-        reconnectAttempts++;
-        const delay = Math.min(1000 * Math.pow(2, reconnectAttempts - 1), MAX_RECONNECT_DELAY);
-        updateTab(tabId, {
-          connected: false,
-          error: `${reason} — reconnecting in ${Math.round(delay / 1000)}s (attempt ${reconnectAttempts})...`,
-          statusBanner: tab.persistent ? "Reconnecting to persistent session..." : "",
-        });
-        term.write(`\r\n\x1b[90m[${reason} — reconnecting in ${Math.round(delay / 1000)}s...]\x1b[0m`);
-        reconnectTimer = setTimeout(() => {
-          reconnectTimer = null;
-          if (!tabData.has(tabId) || closedManually) return;
-          connect(true);
-        }, delay);
-      }
- 
-      function sendSize(ws) {
-        if (ws.readyState === 1) {
-          const buf = new Uint8Array(5);
-          buf[0] = 0x01;
-          new DataView(buf.buffer).setUint16(1, term.cols);
-          new DataView(buf.buffer).setUint16(3, term.rows);
-          ws.send(buf);
-        }
-      }
- 
-      async function connect(forceRefresh = false) {
-        let d = tabData.get(tabId);
- 
-        if (forceRefresh) {
-          const refreshed = await tryRefreshSession();
-          if (!refreshed) {
-            handleAuthExpired();
-            return null;
-          }
-        }
- 
-        const ws = new WebSocket(
-          `${proto}//${location.host}/ws/terminal?host=${encodeURIComponent(tab.hostId)}`
-        );
-        ws.binaryType = "arraybuffer";
- 
-        ws.onopen = () => {
-          reconnectAttempts = 0;
-          consecutiveAuthFailures = 0; // Reset auth failure counter on successful connection
-          updateTab(tabId, {
-            connected: true,
-            error: "",
-            statusBanner: tab.persistent ? "Connected. Waiting for persistent session status..." : "",
-          });
-          sendSize(ws);
-          clearHeartbeat();
-          heartbeat = setInterval(() => {
-            if (ws.readyState === 1) {
-              ws.send("__ping__");
-              // Set timeout to detect missing pong
-              pongTimeout = setTimeout(() => {
-                // Check if still the same WebSocket instance and still open
-                const currentTab = tabData.get(tabId);
-                if (currentTab?.ws === ws && ws.readyState === 1) {
-                  ws.close(1000, "keepalive ping timeout");
-                }
-              }, PONG_TIMEOUT);
-            }
-          }, PING_INTERVAL);
-          const d = tabData.get(tabId);
-          if (d) {
-            d.ws = ws;
-            d.heartbeat = heartbeat;
-            d.reconnectTimer = reconnectTimer;
-            d.closedManually = closedManually;
-            d.reconnecting = reconnecting;
-            d.voice?.attach(term, ws);
-          }
-        };
-        ws.onmessage = (e) => {
-          // Check for text pong response
-          if (typeof e.data === 'string' && e.data === '__pong__') {
-            // Clear the pong timeout - connection is alive
-            if (pongTimeout) {
-              clearTimeout(pongTimeout);
-              pongTimeout = null;
-            }
-            return;
-          }
- 
-          const data = new Uint8Array(e.data);
-          const isPersistenceMessage = tab.persistent
-            && data.length >= PERSISTENCE_STATUS_PREFIX_BYTES.length
-            && PERSISTENCE_STATUS_PREFIX_BYTES.every((byte, index) => data[index] === byte);
-          if (isPersistenceMessage) {
-            const status = new TextDecoder().decode(data.slice(PERSISTENCE_STATUS_PREFIX_BYTES.length)).trim();
-            updateTab(tabId, { statusBanner: persistenceLabel(status, reconnecting) });
-            reconnecting = false;
-            const d = tabData.get(tabId);
-            if (d) d.reconnecting = reconnecting;
-            return;
-          }
-          term.write(data);
-        };
-        ws.onclose = async (e) => {
-          const reason = e.reason || `Connection closed (code ${e.code})`;
-          const tabState = tabData.get(tabId);
-          if (tabState) tabState.ws = null;
- 
-          // Handle auth failures (code 1008)
-          if (e.code === 1008) {
-            consecutiveAuthFailures++;
- 
-            // Stop after MAX_AUTH_FAILURES consecutive auth failures
-            if (consecutiveAuthFailures >= MAX_AUTH_FAILURES) {
-              handleAuthExpired(`Session expired after ${MAX_AUTH_FAILURES} attempts — please refresh page`);
-              return;
-            }
- 
-            // Try to refresh session
-            if (!closedManually && !authRecoveryInFlight) {
-              authRecoveryInFlight = true;
-              try {
-                const refreshed = await tryRefreshSession();
-                if (refreshed && !closedManually) {
-                  reconnecting = true;
-                  updateTab(tabId, {
-                    connected: false,
-                    error: `Refreshing session (attempt ${consecutiveAuthFailures}/${MAX_AUTH_FAILURES})...`,
-                    statusBanner: tab.persistent ? "Reconnecting to persistent session..." : "",
-                  });
-                  term.write(`\r\n\x1b[90m[Refreshing session...]\x1b[0m`);
-                  void connect(false);
-                  return;
-                }
-              } catch (err) {
-                console.error("Session refresh failed:", err);
-              } finally {
-                authRecoveryInFlight = false;
-              }
-            }
-            handleAuthExpired("Session expired — please log in again");
-            return;
-          }
- 
-          // Non-auth failures: continue with normal reconnection
-          if (!closedManually) scheduleReconnect(reason);
-          else {
-            clearHeartbeat();
-            updateTab(tabId, { connected: false, error: reason });
-            term.write(`\r\n\x1b[90m[${reason}]\x1b[0m`);
-          }
-        };
-        ws.onerror = () => {
-          const tabState = tabData.get(tabId);
-          if (tabState) tabState.ws = null;
-          if (!closedManually) scheduleReconnect("Connection failed");
-          else updateTab(tabId, { connected: false, error: "Connection failed" });
-        };
- 
-        d = tabData.get(tabId);
-        if (d) {
-          d.ws = ws;
-          d.heartbeat = heartbeat;
-          d.reconnectTimer = reconnectTimer;
-          d.closedManually = closedManually;
-          d.reconnecting = reconnecting;
-        }
-        return ws;
-      }
- 
-      let ws = null;
-      void connect().then((connectedWs) => {
-        ws = connectedWs;
-        if (connectedWs) voice.attach(term, connectedWs);
-      });
-      term.onData((data) => {
-        const currentWs = tabData.get(tabId)?.ws;
-        if (currentWs?.readyState === 1) currentWs.send(new TextEncoder().encode(data));
-      });
- 
-      const handleDragOver = (e) => {
-        e.preventDefault();
-      };
- 
-      const handleDrop = async (e) => {
-        e.preventDefault();
-        if (activeTab !== tabId) return;
-        const currentWs = tabData.get(tabId)?.ws;
-        const files = [...(e.dataTransfer?.files || [])].filter((f) => f.type?.startsWith("image/"));
-        for (const file of files) {
-          await sendDroppedImage(term, currentWs, file);
-        }
-      };
- 
-      const handlePaste = async (e) => {
-        if (activeTab !== tabId) return;
-        const currentWs = tabData.get(tabId)?.ws;
-        const items = [...(e.clipboardData?.items || [])]
-          .filter((it) => it.kind === "file" && it.type?.startsWith("image/"));
-        for (const item of items) {
-          const file = item.getAsFile();
-          if (file) await sendDroppedImage(term, currentWs, file);
-        }
-      };
- 
-      term.element?.addEventListener("dragover", handleDragOver);
-      term.element?.addEventListener("drop", handleDrop);
-      term.textarea?.addEventListener("paste", handlePaste);
- 
-      const ro = new ResizeObserver(() => {
-        fit.fit();
-        const currentWs = tabData.get(tabId)?.ws;
-        if (currentWs) sendSize(currentWs);
-      });
-      ro.observe(el);
-      const voice = new VoiceSession(() => { voiceTick++; });
-      voice.attach(term, ws);
-      tabData.set(tabId, { ws, term, ro, el, voice, heartbeat, reconnectTimer, closedManually, reconnecting, handleDragOver, handleDrop, handlePaste });
-    })();
-  }
- 
-  function activeVoice() { return voiceTick >= 0 && tabData.get(activeTab)?.voice; }
- 
-  function toggleFullscreen() {
-    const elem = document.documentElement;
-    if (!document.fullscreenElement) {
-      elem.requestFullscreen().then(() => {
-        isFullscreen = true;
-        showActionButtons = true;
-      }).catch(err => {
-        console.error('Failed to enter fullscreen:', err);
-      });
-    } else {
-      document.exitFullscreen().then(() => {
-        isFullscreen = false;
-        showActionButtons = false;
-      }).catch(err => {
-        console.error('Failed to exit fullscreen:', err);
-      });
-    }
-  }
- 
-  function sendKey(key) {
-    const currentWs = tabData.get(activeTab)?.ws;
-    if (currentWs?.readyState === 1) {
-      currentWs.send(new TextEncoder().encode(key));
-    }
-  }
- 
-  onMount(() => {
-    syncTheme();
-    themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ["class"] });
-    const requestedHost = new URLSearchParams(window.location.search).get("host");
-    if (requestedHost && !tabs.length && hostById(requestedHost)) addTab(requestedHost);
- 
-    // Listen for fullscreen changes
-    document.addEventListener('fullscreenchange', () => {
-      isFullscreen = !!document.fullscreenElement;
-      showActionButtons = !!document.fullscreenElement;
-    });
-  });
- 
-  onDestroy(() => {
-    themeObserver.disconnect();
-    tabData.forEach((d) => {
-      d.closedManually = true;
-      if (d.reconnectTimer) clearTimeout(d.reconnectTimer);
-      d.voice?.destroy();
-      d.ro?.disconnect();
-      if (d.heartbeat) clearInterval(d.heartbeat);
-      d.term?.element?.removeEventListener("dragover", d.handleDragOver);
-      d.term?.element?.removeEventListener("drop", d.handleDrop);
-      d.term?.textarea?.removeEventListener("paste", d.handlePaste);
-      d.ws?.close();
-      d.term?.dispose();
-    });
-  });
-</script>
-<div class={activeVoice()?.voiceMode ? 'flex flex-col h-[calc(100vh-4rem)] overflow-hidden gap-1' : 'space-y-2'}>
-  {#if !activeVoice()?.voiceMode}
-    <h1 class="text-2xl font-bold text-surface-900 dark:text-surface-100 tracking-tight">Terminal</h1>
-  {/if}
- 
-  <!-- iPhone Landscape Action Buttons (only in fullscreen) -->
-  {#if showActionButtons && activeTab}
-    <!-- Top Left Pocket -->
-    <div class="actions-top-left">
-      <button class="action-btn" onclick={() => sendKey('\x1b')} title="ESC">ESC</button>
-      <button class="action-btn" onclick={() => sendKey('\t')} title="TAB">TAB</button>
-    </div>
- 
-    <!-- Bottom Left Pocket -->
-    <div class="actions-bottom-left">
-      <button class="action-btn" onclick={() => sendKey('\x01')} title="CTRL+A">^A</button>
-      <button class="action-btn" onclick={() => sendKey('\x03')} title="CTRL+C">^C</button>
-    </div>
- 
-    <!-- Top Right Pocket -->
-    <div class="actions-top-right">
-      <button class="action-btn" onclick={() => sendKey('\x0c')} title="CTRL+L">^L</button>
-      <button class="action-btn" onclick={() => sendKey('\x1a')} title="CTRL+Z">^Z</button>
-    </div>
- 
-    <!-- Bottom Right Pocket -->
-    <div class="actions-bottom-right">
-      <button class="action-btn" onclick={() => sendKey('\x04')} title="CTRL+D">^D</button>
-      <button class="action-btn" onclick={() => sendKey('\x12')} title="CTRL+R">^R</button>
-    </div>
-  {/if}
- 
-  <div class="flex items-center gap-1 border-b border-surface-300 dark:border-surface-700 pb-1 relative">
-    {#each tabs as tab (tab.id)}
-      <button
-        class="flex items-center gap-1.5 px-3 py-1.5 text-sm rounded-t-lg transition-colors {activeTab === tab.id ? 'bg-surface-200 dark:bg-surface-800 text-surface-900 dark:text-surface-100' : 'text-surface-600 dark:text-surface-400 hover:text-surface-800 dark:hover:text-surface-200 hover:bg-surface-100 dark:hover:bg-surface-800/50'}"
-        onclick={() => activeTab = tab.id}
-      >
-        {#if tab.connected}
-          <span class="w-1.5 h-1.5 rounded-full bg-emerald-500"></span>
-        {:else if tab.error}
-          <span class="w-1.5 h-1.5 rounded-full bg-rose-500"></span>
-        {:else}
-          <span class="w-1.5 h-1.5 rounded-full bg-amber-400 animate-pulse"></span>
-        {/if}
-        {tab.label}
-        {#if tab.persistent}
-          <span class="rounded bg-indigo-100 px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-wide text-indigo-700 dark:bg-indigo-500/20 dark:text-indigo-300">tmux</span>
-        {/if}
-        <span
-          class="ml-1 text-surface-500 hover:text-sky-500 cursor-pointer"
-          role="button"
-          tabindex="0"
-          title="Open fresh session in new browser tab"
-          aria-label={`Open fresh ${tab.label} session in new browser tab`}
-          onclick={(e) => { e.stopPropagation(); openFreshSession(tab); }}
-          onkeydown={(e) => e.key === 'Enter' && openFreshSession(tab)}
-        >↗</span>
-        <span
-          class="ml-1 text-surface-500 hover:text-rose-400 cursor-pointer"
-          role="button"
-          tabindex="0"
-          onclick={(e) => { e.stopPropagation(); closeTab(tab.id); }}
-          onkeydown={(e) => e.key === 'Enter' && closeTab(tab.id)}
-        >&times;</span>
-      </button>
-    {/each}
-    <div class="relative">
-      <button
-        class="px-2 py-1 text-surface-600 dark:text-surface-400 hover:text-surface-900 dark:hover:text-surface-100 hover:bg-surface-200 dark:hover:bg-surface-800 rounded text-lg leading-none"
-        onclick={() => showMenu = !showMenu}
-      >+</button>
-      {#if showMenu}
-        <div class="absolute top-full left-0 mt-1 bg-surface-100 dark:bg-surface-800 border border-surface-300 dark:border-surface-600 rounded-lg shadow-xl z-10 py-1 min-w-[140px]">
-          {#each hosts as h}
-            <button
-              class="flex w-full items-center justify-between gap-2 px-3 py-1.5 text-left text-sm text-surface-800 dark:text-surface-200 hover:bg-surface-200 dark:hover:bg-surface-700"
-              onclick={() => addTab(h.id)}
-            >
-              <span>{h.label}</span>
-              {#if h.persistent}
-                <span class="rounded bg-indigo-100 px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-wide text-indigo-700 dark:bg-indigo-500/20 dark:text-indigo-300">persistent</span>
-              {/if}
-            </button>
-          {/each}
-        </div>
-      {/if}
-    </div>
-    {#if activeTab && tabs.find(t => t.id === activeTab)?.connected}
-      <div class="ml-auto flex items-center gap-1">
-        <button
-          class="px-2 py-1 rounded text-sm transition-colors text-surface-600 dark:text-surface-500 hover:text-surface-800 dark:hover:text-surface-300"
-          title={isFullscreen ? 'Exit fullscreen' : 'Enter fullscreen'}
-          onclick={toggleFullscreen}
-        >
-          {#if isFullscreen}
-            <svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M8 3v3a2 2 0 01-2 2H3m18 0h-3a2 2 0 01-2-2V3m0 18v-3a2 2 0 012-2h3M3 16h3a2 2 0 012 2v3"/></svg>
-          {:else}
-            <svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M8 3H5a2 2 0 00-2 2v3m18 0V5a2 2 0 00-2-2h-3m0 18h3a2 2 0 002-2v-3M3 16v3a2 2 0 002 2h3"/></svg>
-          {/if}
-        </button>
-        {#if activeVoice()}
-          {@const v = activeVoice()}
-          <button
-            class="px-2 py-1 rounded text-sm transition-colors {v.voiceMode ? 'text-rose-400' : v.sttAvailable ? 'text-surface-600 dark:text-surface-500 hover:text-surface-800 dark:hover:text-surface-300' : 'text-surface-300 dark:text-surface-700 cursor-not-allowed'}"
-            title={!v.sttAvailable ? 'STT not supported' : v.voiceMode ? 'Voice mode on' : 'Enter voice mode'}
-            disabled={!v.sttAvailable}
-            onclick={() => { if (!v.voiceMode) v.enterVoiceMode(); else v.exitVoiceMode(); voiceTick++; }}
-          >
-            <svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 1a3 3 0 00-3 3v8a3 3 0 006 0V4a3 3 0 00-3-3z"/><path d="M19 10v2a7 7 0 01-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg>
-          </button>
-        {/if}
-      </div>
-    {/if}
-  </div>
- 
-  <p class="text-xs text-surface-600 dark:text-surface-500">
-    Tip: drag & drop or paste images into the active terminal tab to upload them to <code class="text-surface-700 dark:text-surface-400">/tmp/claude-images</code>.
-  </p>
- 
-  {#if activeTab && tabs.find(t => t.id === activeTab)?.statusBanner}
-    <div class="rounded-lg border border-indigo-200 bg-indigo-50 px-3 py-2 text-sm text-indigo-800 dark:border-indigo-500/30 dark:bg-indigo-500/10 dark:text-indigo-200">
-      {tabs.find(t => t.id === activeTab)?.statusBanner}
-    </div>
-  {/if}
- 
-  {#if !tabs.length}
-    <div class="flex items-center justify-center h-[calc(100vh-12rem)] text-surface-600 dark:text-surface-500 text-sm">
-      Click + to open a terminal
-    </div>
-  {/if}
- 
-  {#each tabs as tab (tab.id)}
-    <div class="{activeTab === tab.id ? '' : 'hidden'} {activeVoice()?.voiceMode ? 'flex-1 min-h-0' : ''}">
-      <div
-        use:initTerminal={tab}
-        class="h-full rounded-xl overflow-hidden shadow-lg border border-surface-300 dark:border-surface-700 bg-surface-50 dark:bg-[#0f172a] transition-all duration-300"
-        style="{activeVoice()?.voiceMode ? '' : 'height: calc(100vh - 10rem);'}"
-      ></div>
-    </div>
-  {/each}
- 
-  {#if activeVoice()?.voiceMode}
-    {@const v = activeVoice()}
-    <div class="mt-1 shrink-0 rounded-xl bg-surface-100/95 dark:bg-surface-800/95 border border-surface-300 dark:border-surface-700 shadow-xl overflow-hidden animate-slide-up">
-      <!-- Header -->
-      <div class="flex items-center justify-between px-3 py-1.5 border-b border-surface-300/50 dark:border-surface-700/50">
-        <span class="text-xs font-medium text-surface-700 dark:text-surface-300">Voice Mode</span>
-        <div class="flex items-center gap-2">
-          <select
-            class="bg-surface-200 dark:bg-surface-700 text-surface-800 dark:text-surface-300 text-xs rounded px-1 py-0.5 outline-none max-w-[120px]"
-            onchange={(e) => { v.setVoice(+e.target.value); voiceTick++; }}
-          >
-            {#each v.voices as voice, i}
-              <option value={i} selected={i === v.voiceIdx}>{voice.name}</option>
-            {/each}
-          </select>
-          <button
-            class="px-1.5 py-0.5 rounded text-xs font-medium text-surface-600 dark:text-surface-400 hover:text-surface-800 dark:hover:text-surface-200 hover:bg-surface-200 dark:hover:bg-surface-700"
-            onclick={() => { v.cycleLang(); voiceTick++; }}
-          >{v.langLabel}</button>
-          <button
-            class="px-1.5 py-0.5 rounded text-xs font-medium text-surface-600 dark:text-surface-400 hover:text-surface-800 dark:hover:text-surface-200 hover:bg-surface-200 dark:hover:bg-surface-700"
-            onclick={() => { v.cycleTalkMode(); voiceTick++; }}
-          >{v.modeLabel}</button>
-          <button
-            class="px-1.5 py-0.5 rounded text-xs text-surface-500 hover:text-rose-400"
-            onclick={() => { v.exitVoiceMode(); voiceTick++; }}
-          >&times;</button>
-        </div>
-      </div>
-      <!-- Transcript + Mic -->
-      <div class="flex items-center gap-3 px-3 py-2">
-        {#if v.talkMode === "hold"}
-          <button
-            aria-label="Hold to talk"
-            class="w-10 h-10 shrink-0 rounded-full flex items-center justify-center transition-colors {v.listening ? 'bg-rose-500/30 text-rose-400' : 'bg-surface-200 dark:bg-surface-700 text-surface-700 dark:text-surface-300 hover:bg-surface-300 dark:hover:bg-surface-600'}"
-            onpointerdown={() => { v.startHold(); voiceTick++; }}
-            onpointerup={() => { v.releaseHold(); voiceTick++; }}
-            onpointerleave={() => { if (v.listening) { v.releaseHold(); voiceTick++; } }}
-          >
-            <svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 1a3 3 0 00-3 3v8a3 3 0 006 0V4a3 3 0 00-3-3z"/><path d="M19 10v2a7 7 0 01-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg>
-          </button>
-        {:else if v.talkMode === "tap"}
-          <button
-            aria-label="Tap to talk"
-            class="w-10 h-10 shrink-0 rounded-full flex items-center justify-center transition-colors {v.listening ? 'bg-rose-500/30 text-rose-400' : 'bg-surface-200 dark:bg-surface-700 text-surface-700 dark:text-surface-300 hover:bg-surface-300 dark:hover:bg-surface-600'}"
-            onclick={() => { v.tapMic(); voiceTick++; }}
-          >
-            <svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 1a3 3 0 00-3 3v8a3 3 0 006 0V4a3 3 0 00-3-3z"/><path d="M19 10v2a7 7 0 01-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg>
-          </button>
-        {:else}
-          <div class="w-10 h-10 shrink-0 rounded-full flex items-center justify-center {v.listening ? 'bg-emerald-500/20 text-emerald-400 animate-pulse' : 'bg-surface-200 dark:bg-surface-700 text-surface-500'}">
-            <svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 1a3 3 0 00-3 3v8a3 3 0 006 0V4a3 3 0 00-3-3z"/><path d="M19 10v2a7 7 0 01-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg>
-          </div>
-        {/if}
-        <div class="flex-1 min-w-0">
-          {#if v.transcript}
-            <p class="text-sm text-surface-600 dark:text-surface-400 italic truncate">"{v.transcript}"</p>
-          {:else}
-            <p class="text-xs text-surface-500 dark:text-surface-600">{v.speaking ? 'Speaking...' : v.listening ? 'Listening...' : 'Ready'}</p>
-          {/if}
-        </div>
-      </div>
-    </div>
-  {/if}
-</div>
- 
-<style>
-  @keyframes slide-up {
-    from { transform: translateY(100%); opacity: 0; }
-    to { transform: translateY(0); opacity: 1; }
-  }
-  .animate-slide-up { animation: slide-up 0.25s ease-out; }
- 
-  /* iPhone 17 Pro Landscape Action Button Zones */
-  .actions-top-left,
-  .actions-bottom-left,
-  .actions-top-right,
-  .actions-bottom-right {
-    position: fixed;
-    display: flex;
-    flex-direction: column;
-    gap: 5px;
-    z-index: 9999;
-  }
- 
-  .actions-top-left {
-    top: 5px;
-    left: 5px;
-    width: 52px;
-    height: 125px;
-  }
- 
-  .actions-bottom-left {
-    bottom: 26px;
-    left: 5px;
-    width: 52px;
-    height: 110px;
-  }
- 
-  .actions-top-right {
-    top: 5px;
-    right: 5px;
-    width: 52px;
-    height: 125px;
-  }
- 
-  .actions-bottom-right {
-    bottom: 26px;
-    right: 5px;
-    width: 52px;
-    height: 110px;
-  }
- 
-  .action-btn {
-    flex: 1;
-    background: rgba(99, 102, 241, 0.9);
-    color: white;
-    border: none;
-    border-radius: 8px;
-    font-size: 11px;
-    font-weight: 600;
-    cursor: pointer;
-    transition: all 0.2s;
-    backdrop-filter: blur(10px);
-    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
-    display: flex;
-    align-items: center;
-    justify-content: center;
-  }
- 
-  .action-btn:active {
-    background: rgba(79, 70, 229, 1);
-    transform: scale(0.95);
-  }
- 
-  /* Terminal container respects safe areas */
-  .terminal-safe-container {
-    padding-left: env(safe-area-inset-left);
-    padding-right: env(safe-area-inset-right);
-    padding-bottom: env(safe-area-inset-bottom);
-    box-sizing: border-box;
-  }
-</style>
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/dashboard/frontend/coverage/src/routes/Transmission.svelte.html b/dashboard/frontend/coverage/src/routes/Transmission.svelte.html deleted file mode 100644 index be1401e..0000000 --- a/dashboard/frontend/coverage/src/routes/Transmission.svelte.html +++ /dev/null @@ -1,931 +0,0 @@ - - - - - - Code coverage report for src/routes/Transmission.svelte - - - - - - - - - -
-
-

All files / src/routes Transmission.svelte

-
- -
- 0% - Statements - 0/145 -
- - -
- 0% - Branches - 0/1 -
- - -
- 0% - Functions - 0/1 -
- - -
- 0% - Lines - 0/145 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -178 -179 -180 -181 -182 -183 -184 -185 -186 -187 -188 -189 -190 -191 -192 -193 -194 -195 -196 -197 -198 -199 -200 -201 -202 -203 -204 -205 -206 -207 -208 -209 -210 -211 -212 -213 -214 -215 -216 -217 -218 -219 -220 -221 -222 -223 -224 -225 -226 -227 -228 -229 -230 -231 -232 -233 -234 -235 -236 -237 -238 -239 -240 -241 -242 -243 -244 -245 -246 -247 -248 -249 -250 -251 -252 -253 -254 -255 -256 -257 -258 -259 -260 -261 -262 -263 -264 -265 -266 -267 -268 -269 -270 -271 -272 -273 -274 -275 -276 -277 -278 -279 -280 -281 -282 -283  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  - 
<script>
-  import { onMount, onDestroy } from "svelte";
-  import { get, post } from "../lib/api.js";
- 
-  let status = $state(null);
-  let torrents = $state([]);
-  let trackers = $state([]);
-  let stats = $state(null);
-  let loading = $state(true);
-  let selectedTab = $state("torrents");
- 
-  async function load() {
-    try {
-      const [statusData, torrentsData, trackersData, statsData] = await Promise.all([
-        get("/transmission/status").catch(() => null),
-        get("/transmission/torrents").catch(() => ({ torrents: [] })),
-        get("/transmission/trackers").catch(() => ({ trackers: [] })),
-        get("/transmission/stats").catch(() => null),
-      ]);
- 
-      status = statusData;
-      torrents = torrentsData?.torrents || [];
-      trackers = trackersData?.trackers || [];
-      stats = statsData;
-      loading = false;
-    } catch (e) {
-      console.error("Failed to load transmission data:", e);
-      loading = false;
-    }
-  }
- 
-  async function reannounce(torrentId) {
-    try {
-      await post(`/transmission/reannounce/${torrentId}`);
-      await load();
-    } catch (e) {
-      alert("Failed to reannounce: " + e.message);
-    }
-  }
- 
-  async function startTorrent(torrentId) {
-    try {
-      await post(`/transmission/start/${torrentId}`);
-      await load();
-    } catch (e) {
-      alert("Failed to start: " + e.message);
-    }
-  }
- 
-  async function stopTorrent(torrentId) {
-    try {
-      await post(`/transmission/stop/${torrentId}`);
-      await load();
-    } catch (e) {
-      alert("Failed to stop: " + e.message);
-    }
-  }
- 
-  function fmtBytes(b) {
-    if (!b) return "0 B";
-    const u = ["B", "KB", "MB", "GB", "TB"];
-    const i = Math.floor(Math.log(b) / Math.log(1024));
-    return (b / Math.pow(1024, i)).toFixed(1) + " " + u[i];
-  }
- 
-  function fmtSpeed(bytesPerSec) {
-    return fmtBytes(bytesPerSec) + "/s";
-  }
- 
-  function statusText(status) {
-    const statusMap = {
-      0: "Stopped",
-      1: "Check waiting",
-      2: "Checking",
-      3: "Download waiting",
-      4: "Downloading",
-      5: "Seed waiting",
-      6: "Seeding"
-    };
-    return statusMap[status] || "Unknown";
-  }
- 
-  let interval;
-  onMount(() => { load(); interval = setInterval(load, 10000); });
-  onDestroy(() => clearInterval(interval));
-</script>
- 
-<div class="space-y-6">
-  <div>
-    <h1 class="text-2xl font-bold text-surface-900 dark:text-white tracking-tight">Transmission</h1>
-    <p class="text-sm text-surface-400 mt-1">BitTorrent client status and management</p>
-  </div>
- 
-  {#if loading}
-    <div class="h-[400px] rounded-xl bg-surface-100 dark:bg-surface-800 animate-pulse"></div>
-  {:else}
-    <!-- Status Cards -->
-    <div class="grid grid-cols-1 md:grid-cols-4 gap-4">
-      <!-- Daemon Status -->
-      <div class="rounded-xl bg-white dark:bg-surface-800 p-6 border border-surface-200 dark:border-surface-700">
-        <div class="flex items-center justify-between">
-          <div>
-            <p class="text-sm text-surface-500 dark:text-surface-400">Daemon</p>
-            <p class="text-2xl font-bold mt-1 {status?.running ? 'text-emerald-500' : 'text-rose-500'}">
-              {status?.running ? 'Running' : 'Stopped'}
-            </p>
-          </div>
-          <div class="w-12 h-12 rounded-full {status?.running ? 'bg-emerald-100 dark:bg-emerald-900/30' : 'bg-rose-100 dark:bg-rose-900/30'} flex items-center justify-center">
-            <svg class="w-6 h-6 {status?.running ? 'text-emerald-500' : 'text-rose-500'}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
-              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path>
-            </svg>
-          </div>
-        </div>
-      </div>
- 
-      <!-- Active Torrents -->
-      <div class="rounded-xl bg-white dark:bg-surface-800 p-6 border border-surface-200 dark:border-surface-700">
-        <div class="flex items-center justify-between">
-          <div>
-            <p class="text-sm text-surface-500 dark:text-surface-400">Active Torrents</p>
-            <p class="text-2xl font-bold mt-1 text-surface-900 dark:text-white">
-              {torrents.filter(t => t.status === 4 || t.status === 6).length}
-            </p>
-          </div>
-          <div class="w-12 h-12 rounded-full bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center">
-            <svg class="w-6 h-6 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
-              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M9 19l3 3m0 0l3-3m-3 3V10"></path>
-            </svg>
-          </div>
-        </div>
-      </div>
- 
-      <!-- Download Speed -->
-      <div class="rounded-xl bg-white dark:bg-surface-800 p-6 border border-surface-200 dark:border-surface-700">
-        <div class="flex items-center justify-between">
-          <div>
-            <p class="text-sm text-surface-500 dark:text-surface-400">Download</p>
-            <p class="text-2xl font-bold mt-1 text-surface-900 dark:text-white">
-              {fmtSpeed(torrents.reduce((sum, t) => sum + (t.rateDownload || 0), 0))}
-            </p>
-          </div>
-          <div class="w-12 h-12 rounded-full bg-purple-100 dark:bg-purple-900/30 flex items-center justify-center">
-            <svg class="w-6 h-6 text-purple-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
-              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 14l-7 7m0 0l-7-7m7 7V3"></path>
-            </svg>
-          </div>
-        </div>
-      </div>
- 
-      <!-- Upload Speed -->
-      <div class="rounded-xl bg-white dark:bg-surface-800 p-6 border border-surface-200 dark:border-surface-700">
-        <div class="flex items-center justify-between">
-          <div>
-            <p class="text-sm text-surface-500 dark:text-surface-400">Upload</p>
-            <p class="text-2xl font-bold mt-1 text-surface-900 dark:text-white">
-              {fmtSpeed(torrents.reduce((sum, t) => sum + (t.rateUpload || 0), 0))}
-            </p>
-          </div>
-          <div class="w-12 h-12 rounded-full bg-amber-100 dark:bg-amber-900/30 flex items-center justify-center">
-            <svg class="w-6 h-6 text-amber-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
-              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 10l7-7m0 0l7 7m-7-7v18"></path>
-            </svg>
-          </div>
-        </div>
-      </div>
-    </div>
- 
-    <!-- Tabs -->
-    <div class="border-b border-surface-200 dark:border-surface-700">
-      <nav class="-mb-px flex space-x-8">
-        <button
-          onclick={() => selectedTab = "torrents"}
-          class="py-4 px-1 border-b-2 font-medium text-sm {selectedTab === 'torrents' ? 'border-blue-500 text-blue-600 dark:text-blue-400' : 'border-transparent text-surface-500 hover:text-surface-700 hover:border-surface-300 dark:text-surface-400 dark:hover:text-surface-300'}"
-        >
-          Torrents ({torrents.length})
-        </button>
-        <button
-          onclick={() => selectedTab = "trackers"}
-          class="py-4 px-1 border-b-2 font-medium text-sm {selectedTab === 'trackers' ? 'border-blue-500 text-blue-600 dark:text-blue-400' : 'border-transparent text-surface-500 hover:text-surface-700 hover:border-surface-300 dark:text-surface-400 dark:hover:text-surface-300'}"
-        >
-          Trackers ({trackers.length})
-        </button>
-      </nav>
-    </div>
- 
-    <!-- Content -->
-    {#if selectedTab === "torrents"}
-      <div class="rounded-xl bg-white dark:bg-surface-800 border border-surface-200 dark:border-surface-700 overflow-hidden">
-        <div class="overflow-x-auto">
-          <table class="w-full">
-            <thead class="bg-surface-50 dark:bg-surface-900/50">
-              <tr>
-                <th class="px-6 py-3 text-left text-xs font-medium text-surface-500 dark:text-surface-400 uppercase tracking-wider">Name</th>
-                <th class="px-6 py-3 text-left text-xs font-medium text-surface-500 dark:text-surface-400 uppercase tracking-wider">Status</th>
-                <th class="px-6 py-3 text-left text-xs font-medium text-surface-500 dark:text-surface-400 uppercase tracking-wider">Progress</th>
-                <th class="px-6 py-3 text-left text-xs font-medium text-surface-500 dark:text-surface-400 uppercase tracking-wider">Down</th>
-                <th class="px-6 py-3 text-left text-xs font-medium text-surface-500 dark:text-surface-400 uppercase tracking-wider">Up</th>
-                <th class="px-6 py-3 text-left text-xs font-medium text-surface-500 dark:text-surface-400 uppercase tracking-wider">Ratio</th>
-                <th class="px-6 py-3 text-left text-xs font-medium text-surface-500 dark:text-surface-400 uppercase tracking-wider">Tracker</th>
-                <th class="px-6 py-3 text-left text-xs font-medium text-surface-500 dark:text-surface-400 uppercase tracking-wider">Actions</th>
-              </tr>
-            </thead>
-            <tbody class="divide-y divide-surface-200 dark:divide-surface-700">
-              {#each torrents as torrent}
-                <tr class="hover:bg-surface-50 dark:hover:bg-surface-900/30">
-                  <td class="px-6 py-4 text-sm text-surface-900 dark:text-white max-w-md truncate">{torrent.name}</td>
-                  <td class="px-6 py-4 text-sm">
-                    <span class="px-2 py-1 rounded-full text-xs font-medium {torrent.status === 4 || torrent.status === 6 ? 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400' : 'bg-surface-100 text-surface-700 dark:bg-surface-700 dark:text-surface-300'}">
-                      {statusText(torrent.status)}
-                    </span>
-                  </td>
-                  <td class="px-6 py-4 text-sm text-surface-500 dark:text-surface-400">{(torrent.percentDone * 100).toFixed(1)}%</td>
-                  <td class="px-6 py-4 text-sm text-surface-500 dark:text-surface-400">{fmtSpeed(torrent.rateDownload)}</td>
-                  <td class="px-6 py-4 text-sm text-surface-500 dark:text-surface-400">{fmtSpeed(torrent.rateUpload)}</td>
-                  <td class="px-6 py-4 text-sm text-surface-500 dark:text-surface-400">{torrent.uploadRatio?.toFixed(2) || '0.00'}</td>
-                  <td class="px-6 py-4 text-sm">
-                    {#if torrent.hasTrackerErrors}
-                      <span class="text-rose-500 flex items-center gap-1">
-                        <svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
-                          <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"></path>
-                        </svg>
-                        Error
-                      </span>
-                    {:else}
-                      <span class="text-emerald-500">OK</span>
-                    {/if}
-                  </td>
-                  <td class="px-6 py-4 text-sm">
-                    <button
-                      onclick={() => reannounce(torrent.id)}
-                      class="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300"
-                      title="Reannounce to tracker"
-                    >
-                      Reannounce
-                    </button>
-                  </td>
-                </tr>
-              {/each}
-            </tbody>
-          </table>
-        </div>
-      </div>
-    {:else if selectedTab === "trackers"}
-      <div class="rounded-xl bg-white dark:bg-surface-800 border border-surface-200 dark:border-surface-700 overflow-hidden">
-        <div class="overflow-x-auto">
-          <table class="w-full">
-            <thead class="bg-surface-50 dark:bg-surface-900/50">
-              <tr>
-                <th class="px-6 py-3 text-left text-xs font-medium text-surface-500 dark:text-surface-400 uppercase tracking-wider">Tracker</th>
-                <th class="px-6 py-3 text-left text-xs font-medium text-surface-500 dark:text-surface-400 uppercase tracking-wider">Total Torrents</th>
-                <th class="px-6 py-3 text-left text-xs font-medium text-surface-500 dark:text-surface-400 uppercase tracking-wider">Errors</th>
-                <th class="px-6 py-3 text-left text-xs font-medium text-surface-500 dark:text-surface-400 uppercase tracking-wider">Status</th>
-                <th class="px-6 py-3 text-left text-xs font-medium text-surface-500 dark:text-surface-400 uppercase tracking-wider">Last Error</th>
-              </tr>
-            </thead>
-            <tbody class="divide-y divide-surface-200 dark:divide-surface-700">
-              {#each trackers as tracker}
-                <tr class="hover:bg-surface-50 dark:hover:bg-surface-900/30">
-                  <td class="px-6 py-4 text-sm font-medium text-surface-900 dark:text-white">{tracker.host}</td>
-                  <td class="px-6 py-4 text-sm text-surface-500 dark:text-surface-400">{tracker.total}</td>
-                  <td class="px-6 py-4 text-sm text-surface-500 dark:text-surface-400">{tracker.errors}</td>
-                  <td class="px-6 py-4 text-sm">
-                    {#if tracker.errors > 0}
-                      <span class="px-2 py-1 rounded-full text-xs font-medium bg-rose-100 text-rose-700 dark:bg-rose-900/30 dark:text-rose-400">
-                        Error
-                      </span>
-                    {:else}
-                      <span class="px-2 py-1 rounded-full text-xs font-medium bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400">
-                        OK
-                      </span>
-                    {/if}
-                  </td>
-                  <td class="px-6 py-4 text-sm text-surface-500 dark:text-surface-400">{tracker.lastError || '-'}</td>
-                </tr>
-              {/each}
-            </tbody>
-          </table>
-        </div>
-      </div>
-    {/if}
-  {/if}
-</div>
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/dashboard/frontend/coverage/src/routes/index.html b/dashboard/frontend/coverage/src/routes/index.html deleted file mode 100644 index eb1ef62..0000000 --- a/dashboard/frontend/coverage/src/routes/index.html +++ /dev/null @@ -1,341 +0,0 @@ - - - - - - Code coverage report for src/routes - - - - - - - - - -
-
-

All files src/routes

-
- -
- 2.01% - Statements - 48/2380 -
- - -
- 12% - Branches - 3/25 -
- - -
- 9.52% - Functions - 2/21 -
- - -
- 2.01% - Lines - 48/2380 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FileStatementsBranchesFunctionsLines
CcConnect.svelte -
-
0%0/870%0/10%0/10%0/87
ChatSummary.svelte -
-
0%0/910%0/10%0/10%0/91
Conversations.svelte -
-
0%0/2010%0/10%0/10%0/201
Dashboard.svelte -
-
0%0/870%0/10%0/10%0/87
Docker.svelte -
-
0%0/940%0/10%0/10%0/94
Files.svelte -
-
0%0/1050%0/10%0/10%0/105
Gitea.svelte -
-
0%0/740%0/10%0/10%0/74
InfoEngine.svelte -
-
0%0/920%0/10%0/10%0/92
LiteLLM.svelte -
-
0%0/580%0/10%0/10%0/58
Login.svelte -
-
41.02%48/11730%3/1033.33%2/641.02%48/117
OPC.svelte -
-
0%0/1290%0/10%0/10%0/129
OpenClaw.svelte -
-
0%0/220%0/10%0/10%0/22
Security.svelte -
-
0%0/1710%0/10%0/10%0/171
Settings.svelte -
-
0%0/3620%0/10%0/10%0/362
Terminal.svelte -
-
0%0/5450%0/10%0/10%0/545
Transmission.svelte -
-
0%0/1450%0/10%0/10%0/145
-
-
-
- - - - - - - - \ No newline at end of file