create-walle 0.9.21 → 0.9.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -5
- package/package.json +2 -2
- package/template/CLAUDE.md +2 -2
- package/template/LICENSE +1 -1
- package/template/bin/ctm-dev-cleanup.js +24 -3
- package/template/bin/ctm-launch.sh +13 -0
- package/template/bin/dev.sh +156 -18
- package/template/bin/node-bin.sh +84 -0
- package/template/bin/pin-node.sh +51 -0
- package/template/claude-task-manager/api-prompts.js +1203 -182
- package/template/claude-task-manager/api-reviews.js +109 -15
- package/template/claude-task-manager/approval-agent.js +1360 -280
- package/template/claude-task-manager/bin/restart-ctm.sh +64 -23
- package/template/claude-task-manager/bin/storage-migration-supervisor.js +338 -0
- package/template/claude-task-manager/db.js +4417 -295
- package/template/claude-task-manager/docs/app-update-refresh-protocol.md +69 -0
- package/template/claude-task-manager/docs/approval-ai-refinement.md +138 -0
- package/template/claude-task-manager/docs/approval-rescue-loop.md +74 -0
- package/template/claude-task-manager/docs/codex-operational-warning-health.md +107 -0
- package/template/claude-task-manager/docs/codex-resume-state-guard-design.md +17 -12
- package/template/claude-task-manager/docs/codex-terminal-render-controller-handoff.md +311 -0
- package/template/claude-task-manager/docs/coding-agent-hooks-architecture.md +418 -0
- package/template/claude-task-manager/docs/conversation-import-freshness.md +20 -0
- package/template/claude-task-manager/docs/google-workspace-auth-health.md +77 -0
- package/template/claude-task-manager/docs/image-paste-ux.md +13 -0
- package/template/claude-task-manager/docs/ipad-web-preview.md +88 -0
- package/template/claude-task-manager/docs/main-loop-offload-architecture.md +66 -0
- package/template/claude-task-manager/docs/microsoft-dev-tunnel-phone-access-design.md +274 -519
- package/template/claude-task-manager/docs/mobile-live-streaming.md +27 -5
- package/template/claude-task-manager/docs/mobile-remote-submission-lifecycle.md +69 -0
- package/template/claude-task-manager/docs/phone-access-design.md +53 -15
- package/template/claude-task-manager/docs/phone-passkey-identity.md +122 -0
- package/template/claude-task-manager/docs/phone-setup.md +3 -0
- package/template/claude-task-manager/docs/prompt-editing-tree-design.md +25 -1
- package/template/claude-task-manager/docs/remote-desktop-access-design.md +268 -0
- package/template/claude-task-manager/docs/restart-lifecycle-architecture.md +95 -0
- package/template/claude-task-manager/docs/runtime-work-control-plane.md +53 -0
- package/template/claude-task-manager/docs/session-interactive-wait-surfaces.md +38 -0
- package/template/claude-task-manager/docs/session-needs-you-dismissal.md +84 -0
- package/template/claude-task-manager/docs/session-render-state-management-design.md +91 -3
- package/template/claude-task-manager/docs/session-standup-command-center-design.md +25 -1
- package/template/claude-task-manager/docs/session-title-authority.md +32 -0
- package/template/claude-task-manager/docs/session-workspace-binding.md +33 -0
- package/template/claude-task-manager/docs/skill-intent-resolution-design.md +72 -0
- package/template/claude-task-manager/docs/walle-mcp-supervisor-health.md +86 -0
- package/template/claude-task-manager/docs/walle-relay-phone-access-design.md +24 -15
- package/template/claude-task-manager/docs/walle-session-history-hydration.md +114 -0
- package/template/claude-task-manager/docs/walle-session-input-queue.md +104 -0
- package/template/claude-task-manager/docs/walle-session-model-catalog.md +90 -0
- package/template/claude-task-manager/docs/walle-session-model-preferences.md +15 -6
- package/template/claude-task-manager/git-utils.js +897 -27
- package/template/claude-task-manager/lib/agent-capabilities.js +33 -0
- package/template/claude-task-manager/lib/agent-cli-cache.js +37 -7
- package/template/claude-task-manager/lib/agent-hooks-installer.js +26 -2
- package/template/claude-task-manager/lib/agent-presets.js +17 -1
- package/template/claude-task-manager/lib/all-sessions-query.js +108 -0
- package/template/claude-task-manager/lib/approval-ai-refinement.js +488 -0
- package/template/claude-task-manager/lib/approval-self-adapt.js +168 -0
- package/template/claude-task-manager/lib/async-semaphore.js +44 -0
- package/template/claude-task-manager/lib/auth-context.js +5 -0
- package/template/claude-task-manager/lib/auth-rate-limit.js +47 -4
- package/template/claude-task-manager/lib/auth-rules.js +29 -2
- package/template/claude-task-manager/lib/auto-approval-verifier.js +129 -16
- package/template/claude-task-manager/lib/background-llm.js +144 -17
- package/template/claude-task-manager/lib/branch-inventory.js +212 -0
- package/template/claude-task-manager/lib/claude-desktop-sessions.js +15 -3
- package/template/claude-task-manager/lib/coalesce-sync-frames.js +151 -0
- package/template/claude-task-manager/lib/codex-launch-health.js +762 -0
- package/template/claude-task-manager/lib/codex-transcript-pager.js +51 -0
- package/template/claude-task-manager/lib/codex-zst.js +124 -0
- package/template/claude-task-manager/lib/coding-agent-models.js +233 -30
- package/template/claude-task-manager/lib/connection-health.js +232 -0
- package/template/claude-task-manager/lib/conversation-blob-parser.js +42 -0
- package/template/claude-task-manager/lib/conversation-tail-merge.js +89 -26
- package/template/claude-task-manager/lib/ctm-session-context-api.js +39 -10
- package/template/claude-task-manager/lib/cursor-conversation-store.js +354 -0
- package/template/claude-task-manager/lib/db-owner-worker-client.js +315 -0
- package/template/claude-task-manager/lib/document-review.js +141 -6
- package/template/claude-task-manager/lib/escalation-review.js +152 -0
- package/template/claude-task-manager/lib/graceful-shutdown.js +159 -0
- package/template/claude-task-manager/lib/headless-term-service.js +678 -0
- package/template/claude-task-manager/lib/heavy-worker-fallback.js +38 -0
- package/template/claude-task-manager/lib/jsonl-conversation-parser.js +542 -0
- package/template/claude-task-manager/lib/jsonl-range-reader.js +112 -0
- package/template/claude-task-manager/lib/main-db-census.js +216 -0
- package/template/claude-task-manager/lib/message-pagination.js +106 -4
- package/template/claude-task-manager/lib/microsoft-dev-tunnel-setup.js +750 -26
- package/template/claude-task-manager/lib/mobile-auth-api.js +274 -7
- package/template/claude-task-manager/lib/mobile-auth-store.js +592 -10
- package/template/claude-task-manager/lib/mobile-notification-dispatcher.js +15 -0
- package/template/claude-task-manager/lib/model-overview-brain-fallback.js +311 -0
- package/template/claude-task-manager/lib/model-overview-cache.js +141 -0
- package/template/claude-task-manager/lib/models-health-routing-notice.js +126 -0
- package/template/claude-task-manager/lib/node-pin-guard.js +93 -0
- package/template/claude-task-manager/lib/perf-tracker.js +242 -6
- package/template/claude-task-manager/lib/permission-match.js +76 -0
- package/template/claude-task-manager/lib/permission-sync.js +133 -20
- package/template/claude-task-manager/lib/process-title.js +35 -0
- package/template/claude-task-manager/lib/prompt-executions-query.js +25 -0
- package/template/claude-task-manager/lib/prompt-index-disk-cache.js +44 -0
- package/template/claude-task-manager/lib/prompt-intent.js +132 -0
- package/template/claude-task-manager/lib/provider-user-context.js +34 -0
- package/template/claude-task-manager/lib/read-pool-client.js +313 -0
- package/template/claude-task-manager/lib/readpool-breaker.js +31 -0
- package/template/claude-task-manager/lib/recent-sessions-breaker.js +12 -0
- package/template/claude-task-manager/lib/remote-feedback-client.js +72 -0
- package/template/claude-task-manager/lib/remote-relay-protocol.js +37 -4
- package/template/claude-task-manager/lib/remote-relay-store.js +159 -0
- package/template/claude-task-manager/lib/remote-submission-observer.js +278 -0
- package/template/claude-task-manager/lib/restart-guard.js +109 -0
- package/template/claude-task-manager/lib/restore-interruption-detector.js +439 -0
- package/template/claude-task-manager/lib/restore-policy.js +13 -0
- package/template/claude-task-manager/lib/restore-resume-batch.js +74 -0
- package/template/claude-task-manager/lib/restore-runtime.js +68 -0
- package/template/claude-task-manager/lib/restore-storm.js +34 -0
- package/template/claude-task-manager/lib/resume-cwd.js +36 -0
- package/template/claude-task-manager/lib/resume-preflight.js +313 -0
- package/template/claude-task-manager/lib/runtime-work-registry.js +444 -0
- package/template/claude-task-manager/lib/sanitize-openai-auth.js +31 -0
- package/template/claude-task-manager/lib/scheduler.js +21 -1
- package/template/claude-task-manager/lib/scrollback-snapshot-store.js +159 -0
- package/template/claude-task-manager/lib/serial-task-queue.js +64 -0
- package/template/claude-task-manager/lib/server-listeners.js +239 -0
- package/template/claude-task-manager/lib/session-capture.js +42 -7
- package/template/claude-task-manager/lib/session-content-backfill.js +131 -0
- package/template/claude-task-manager/lib/session-history.js +388 -43
- package/template/claude-task-manager/lib/session-host-manager.js +287 -0
- package/template/claude-task-manager/lib/session-image-refs.js +209 -0
- package/template/claude-task-manager/lib/session-jobs.js +399 -59
- package/template/claude-task-manager/lib/session-prompt-index.js +137 -0
- package/template/claude-task-manager/lib/session-restore.js +53 -0
- package/template/claude-task-manager/lib/session-standup.js +123 -23
- package/template/claude-task-manager/lib/session-state-bus.js +14 -0
- package/template/claude-task-manager/lib/session-stream.js +64 -16
- package/template/claude-task-manager/lib/session-timeline-summary.js +260 -0
- package/template/claude-task-manager/lib/session-token-usage.js +494 -0
- package/template/claude-task-manager/lib/session-workspace-binding.js +356 -0
- package/template/claude-task-manager/lib/setup-network-config.js +9 -0
- package/template/claude-task-manager/lib/size-cap.js +45 -0
- package/template/claude-task-manager/lib/size-cap.test.js +62 -0
- package/template/claude-task-manager/lib/skill-autocomplete.js +180 -1
- package/template/claude-task-manager/lib/skill-intent-resolver.js +304 -0
- package/template/claude-task-manager/lib/sqlite-driver.js +19 -3
- package/template/claude-task-manager/lib/standup-attention.js +7 -3
- package/template/claude-task-manager/lib/status-authority.js +39 -0
- package/template/claude-task-manager/lib/status-hooks.js +4 -0
- package/template/claude-task-manager/lib/storage-migration.js +235 -0
- package/template/claude-task-manager/lib/structured-capture.js +298 -0
- package/template/claude-task-manager/lib/sync-io-census.js +163 -0
- package/template/claude-task-manager/lib/tailscale-setup.js +6 -0
- package/template/claude-task-manager/lib/terminal-activity-evidence.js +33 -0
- package/template/claude-task-manager/lib/terminal-choice.js +364 -0
- package/template/claude-task-manager/lib/terminal-control-sanitize.js +17 -0
- package/template/claude-task-manager/lib/terminal-fingerprint.js +48 -0
- package/template/claude-task-manager/lib/terminal-output-flush.js +84 -0
- package/template/claude-task-manager/lib/timeline-order.js +122 -0
- package/template/claude-task-manager/lib/transcript-store.js +348 -43
- package/template/claude-task-manager/lib/transport-security.js +84 -1
- package/template/claude-task-manager/lib/wait-state.js +184 -0
- package/template/claude-task-manager/lib/walle-client.js +47 -5
- package/template/claude-task-manager/lib/walle-ctm-history.js +564 -4
- package/template/claude-task-manager/lib/walle-external-actions.js +135 -16
- package/template/claude-task-manager/lib/walle-history-hydration.js +46 -0
- package/template/claude-task-manager/lib/walle-native-health.js +403 -0
- package/template/claude-task-manager/lib/walle-repair.js +701 -0
- package/template/claude-task-manager/lib/walle-session-cache.js +109 -0
- package/template/claude-task-manager/lib/walle-session-context.js +57 -21
- package/template/claude-task-manager/lib/walle-session-model-catalog.js +34 -0
- package/template/claude-task-manager/lib/walle-supervisor.js +539 -63
- package/template/claude-task-manager/lib/walle-transcript.js +52 -0
- package/template/claude-task-manager/lib/worktree-active-sync.js +11 -7
- package/template/claude-task-manager/lib/worktree-cwd.js +32 -1
- package/template/claude-task-manager/package.json +1 -1
- package/template/claude-task-manager/prompt-harvest.js +89 -66
- package/template/claude-task-manager/providers/claude-code.js +51 -3
- package/template/claude-task-manager/providers/cursor.js +140 -45
- package/template/claude-task-manager/public/css/reviews.css +551 -61
- package/template/claude-task-manager/public/css/setup.css +191 -0
- package/template/claude-task-manager/public/css/walle-session.css +865 -10
- package/template/claude-task-manager/public/css/walle.css +154 -0
- package/template/claude-task-manager/public/designs/ai-providers-consolidation-v2.html +830 -0
- package/template/claude-task-manager/public/index.html +18516 -2058
- package/template/claude-task-manager/public/ipad.html +363 -0
- package/template/claude-task-manager/public/js/document-review-links.js +301 -0
- package/template/claude-task-manager/public/js/image-normalize.js +69 -36
- package/template/claude-task-manager/public/js/message-renderer.js +1265 -77
- package/template/claude-task-manager/public/js/prompts.js +66 -29
- package/template/claude-task-manager/public/js/reviews.js +901 -133
- package/template/claude-task-manager/public/js/session-activity-utils.js +11 -1
- package/template/claude-task-manager/public/js/session-search-utils.js +94 -10
- package/template/claude-task-manager/public/js/session-status-precedence.js +23 -5
- package/template/claude-task-manager/public/js/setup.js +1273 -176
- package/template/claude-task-manager/public/js/stream-view.js +691 -73
- package/template/claude-task-manager/public/js/terminal-reconciler.js +210 -0
- package/template/claude-task-manager/public/js/walle-session.js +2455 -158
- package/template/claude-task-manager/public/js/walle.js +455 -28
- package/template/claude-task-manager/public/m/app.css +2909 -262
- package/template/claude-task-manager/public/m/app.js +6601 -398
- package/template/claude-task-manager/public/m/claim.html +224 -17
- package/template/claude-task-manager/public/m/index.html +117 -21
- package/template/claude-task-manager/public/m/sw.js +3 -1
- package/template/claude-task-manager/public/manifest.json +2 -2
- package/template/claude-task-manager/public/prompts.html +30 -14
- package/template/claude-task-manager/queue-engine.js +507 -28
- package/template/claude-task-manager/scripts/repair-claude-session-images.js +27 -8
- package/template/claude-task-manager/server.js +14341 -2197
- package/template/claude-task-manager/session-integrity.js +160 -18
- package/template/claude-task-manager/session-search-ranking.js +1 -0
- package/template/claude-task-manager/session-utils.js +25 -5
- package/template/claude-task-manager/workers/approval-blocklist.js +96 -6
- package/template/claude-task-manager/workers/approval-widget-validator.js +14 -8
- package/template/claude-task-manager/workers/conversation-import-worker.js +11 -50
- package/template/claude-task-manager/workers/db-owner-worker.js +386 -0
- package/template/claude-task-manager/workers/harvest-worker.js +9 -55
- package/template/claude-task-manager/workers/headless-term-worker.js +9 -530
- package/template/claude-task-manager/workers/read-pool-worker.js +387 -0
- package/template/claude-task-manager/workers/scrollback-worker.js +11 -72
- package/template/claude-task-manager/workers/session-host-process.js +146 -0
- package/template/claude-task-manager/workers/session-integrity-worker.js +10 -54
- package/template/claude-task-manager/workers/state-detectors/base.js +18 -1
- package/template/claude-task-manager/workers/state-detectors/claude-code.js +182 -9
- package/template/claude-task-manager/workers/state-detectors/codex.js +150 -2
- package/template/claude-task-manager/workers/state-detectors/cursor.js +127 -0
- package/template/claude-task-manager/workers/state-detectors/gemini.js +21 -0
- package/template/claude-task-manager/workers/state-detectors/index.js +29 -0
- package/template/claude-task-manager/workers/state-detectors/opencode.js +103 -0
- package/template/docs/design/markdown-review-pane.md +206 -0
- package/template/docs/designs/2026-05-17-portkey-gateway-provider-ux.md +129 -38
- package/template/docs/designs/2026-05-20-mobile-worktree-finish-command.md +27 -0
- package/template/docs/designs/2026-05-22-ai-configuration-consolidation.md +248 -0
- package/template/docs/designs/ai-configuration-consolidation-mock.html +812 -0
- package/template/docs/private-memory-and-pii-policy.md +69 -0
- package/template/package.json +2 -1
- package/template/scripts/check-private-data.js +201 -0
- package/template/shared/sqlite-owner-guard.js +30 -0
- package/template/shared/sqlite-owner-write-queue.js +225 -0
- package/template/shared/sqlite-storage-policy.js +111 -0
- package/template/shared/sqlite-write-lock.js +428 -0
- package/template/wall-e/agent-runners/claude-code.js +5 -0
- package/template/wall-e/agent.js +166 -22
- package/template/wall-e/api-walle.js +524 -70
- package/template/wall-e/auth/provider-flows.js +11 -1
- package/template/wall-e/bin/walle-mcp-stdio.js +341 -17
- package/template/wall-e/brain.js +1614 -141
- package/template/wall-e/chat/attachment-blocks.js +96 -0
- package/template/wall-e/chat/attachments.js +2 -1
- package/template/wall-e/chat/capability-resolver.js +7 -7
- package/template/wall-e/chat/context-messages.js +28 -0
- package/template/wall-e/chat/conversation-frame.js +630 -0
- package/template/wall-e/chat/provider-messages.js +125 -0
- package/template/wall-e/chat.js +1002 -233
- package/template/wall-e/coding/acceptance-contract.js +170 -0
- package/template/wall-e/coding/acp-adapter.js +1 -1
- package/template/wall-e/coding/agent-catalog.js +3 -0
- package/template/wall-e/coding/artifact-store.js +93 -0
- package/template/wall-e/coding/capability-router.js +120 -0
- package/template/wall-e/coding/coding-run-controller.js +423 -0
- package/template/wall-e/coding/compaction-service.js +157 -12
- package/template/wall-e/coding/frontend-verification.js +258 -0
- package/template/wall-e/coding/lifecycle-hooks.js +75 -0
- package/template/wall-e/coding/local-preview-contract.js +157 -0
- package/template/wall-e/coding/permission-service.js +57 -13
- package/template/wall-e/coding/prompt-bundle.js +19 -1
- package/template/wall-e/coding/prompt-section-registry.js +227 -0
- package/template/wall-e/coding/provider-compat.js +15 -0
- package/template/wall-e/coding/runtime-events.js +224 -0
- package/template/wall-e/coding/runtime-mode.js +3 -0
- package/template/wall-e/coding/side-git-snapshot.js +160 -4
- package/template/wall-e/coding/snapshot-service.js +143 -1
- package/template/wall-e/coding/stream-processor.js +388 -34
- package/template/wall-e/coding/task-tool.js +141 -4
- package/template/wall-e/coding/tool-execution-controller.js +365 -0
- package/template/wall-e/coding/tool-registry.js +43 -5
- package/template/wall-e/coding/user-hooks.js +217 -0
- package/template/wall-e/coding-orchestrator.js +1330 -221
- package/template/wall-e/coding-prompts.js +20 -4
- package/template/wall-e/context/context-builder.js +15 -2
- package/template/wall-e/decision/confidence.js +1 -1
- package/template/wall-e/docs/coding-acceptance-contract.md +41 -0
- package/template/wall-e/docs/external-action-controller.md +26 -6
- package/template/wall-e/docs/telemetry-lifecycle.md +8 -2
- package/template/wall-e/embeddings.js +591 -53
- package/template/wall-e/external-action-controller.js +12 -0
- package/template/wall-e/http/auth.js +1 -0
- package/template/wall-e/http/chat-api.js +46 -11
- package/template/wall-e/http/model-admin.js +836 -34
- package/template/wall-e/lib/boot-profile.js +88 -0
- package/template/wall-e/lib/event-loop-monitor.js +93 -0
- package/template/wall-e/lib/service-health.js +194 -0
- package/template/wall-e/llm/anthropic.js +130 -5
- package/template/wall-e/llm/client.js +266 -63
- package/template/wall-e/llm/default-fallback.js +382 -0
- package/template/wall-e/llm/health.js +19 -0
- package/template/wall-e/llm/message-guard.js +78 -0
- package/template/wall-e/llm/model-catalog.js +252 -1
- package/template/wall-e/llm/openai.js +26 -4
- package/template/wall-e/llm/portkey-sync.js +654 -0
- package/template/wall-e/llm/provider-error.js +30 -2
- package/template/wall-e/llm/registry.js +5 -1
- package/template/wall-e/llm/request-compat.js +67 -0
- package/template/wall-e/loops/backfill.js +79 -23
- package/template/wall-e/loops/brain-optimize.js +67 -0
- package/template/wall-e/loops/ingest.js +25 -10
- package/template/wall-e/loops/question-digest.js +160 -0
- package/template/wall-e/loops/reflect.js +6 -4
- package/template/wall-e/loops/think.js +39 -12
- package/template/wall-e/mcp-server.js +318 -36
- package/template/wall-e/memory/ctm-context-client.js +52 -14
- package/template/wall-e/memory/ctm-operational-context.js +237 -0
- package/template/wall-e/memory/ctm-prompt-executions-client.js +128 -0
- package/template/wall-e/memory/ctm-session-context.js +111 -63
- package/template/wall-e/prompts/coding/deepseek.txt +3 -0
- package/template/wall-e/prompts/coding/gemini.txt +6 -0
- package/template/wall-e/prompts/coding/gpt.txt +6 -0
- package/template/wall-e/prompts/coding/local.txt +7 -0
- package/template/wall-e/runtime/decision-hooks.js +115 -0
- package/template/wall-e/runtime/devbox-gateway.js +82 -8
- package/template/wall-e/runtime/prompt-manifest.js +86 -0
- package/template/wall-e/runtime/tool-executor.js +269 -0
- package/template/wall-e/runtime/tool-result-envelope.js +138 -0
- package/template/wall-e/runtime/transcript-projection.js +60 -0
- package/template/wall-e/runtime/walle-runtime.js +224 -0
- package/template/wall-e/scripts/db-optimize/migrate.js +162 -0
- package/template/wall-e/scripts/db-optimize/recall-eval.js +117 -0
- package/template/wall-e/server.js +15 -0
- package/template/wall-e/session-files.js +9 -0
- package/template/wall-e/skills/_bundled/google-calendar/run.js +1 -1
- package/template/wall-e/skills/_bundled/gws-workspace/run.js +1 -1
- package/template/wall-e/skills/_bundled/slack-mentions/run.js +76 -6
- package/template/wall-e/skills/claude-code-reader.js +7 -3
- package/template/wall-e/skills/script-skill-runner.js +10 -0
- package/template/wall-e/skills/skill-planner.js +38 -0
- package/template/wall-e/tools/builtin-middleware.js +19 -9
- package/template/wall-e/tools/local-tools.js +1428 -16
- package/template/wall-e/tools/permission-checker.js +73 -5
- package/template/wall-e/tools/question-manager.js +117 -7
- package/template/wall-e/training/harvester.js +12 -28
- package/template/wall-e/training/replay.js +25 -80
- package/template/website/index.html +10 -10
- package/template/wall-e/eval/ab-test.js +0 -203
- package/template/wall-e/eval/agent-runner.js +0 -772
- package/template/wall-e/eval/agent-scorer.js +0 -461
- package/template/wall-e/eval/aggregator.js +0 -414
- package/template/wall-e/eval/allowed-test-commands.js +0 -34
- package/template/wall-e/eval/benchmark-generator.js +0 -113
- package/template/wall-e/eval/benchmarks/chat-eval.json +0 -1662
- package/template/wall-e/eval/benchmarks/chat.json +0 -82
- package/template/wall-e/eval/benchmarks/coding-agent-real.json +0 -1
- package/template/wall-e/eval/benchmarks/coding-agent.json +0 -1581
- package/template/wall-e/eval/benchmarks/coding.json +0 -122
- package/template/wall-e/eval/benchmarks/memory-retrieval.json +0 -234
- package/template/wall-e/eval/benchmarks/reasoning.json +0 -82
- package/template/wall-e/eval/benchmarks/swebench-lite-30.json +0 -212
- package/template/wall-e/eval/benchmarks.js +0 -669
- package/template/wall-e/eval/cc-replay.js +0 -719
- package/template/wall-e/eval/chat-eval.js +0 -525
- package/template/wall-e/eval/check-keys.js +0 -15
- package/template/wall-e/eval/check-providers.js +0 -42
- package/template/wall-e/eval/codex-cli-baseline.js +0 -669
- package/template/wall-e/eval/coding-agent-real.js +0 -570
- package/template/wall-e/eval/context-compactor.js +0 -251
- package/template/wall-e/eval/debug-agent003.js +0 -68
- package/template/wall-e/eval/diagnostics.js +0 -216
- package/template/wall-e/eval/eval-orchestrator.js +0 -642
- package/template/wall-e/eval/evaluate.js +0 -202
- package/template/wall-e/eval/evaluator.js +0 -373
- package/template/wall-e/eval/exporter.js +0 -212
- package/template/wall-e/eval/fixtures/express-basic/package.json +0 -9
- package/template/wall-e/eval/fixtures/express-basic/server.js +0 -115
- package/template/wall-e/eval/fixtures/express-basic/test.js +0 -83
- package/template/wall-e/eval/fixtures/express-buggy/package.json +0 -9
- package/template/wall-e/eval/fixtures/express-buggy/server.js +0 -113
- package/template/wall-e/eval/fixtures/express-buggy/test.js +0 -83
- package/template/wall-e/eval/fixtures/express-buggy-items/package.json +0 -9
- package/template/wall-e/eval/fixtures/express-buggy-items/server.js +0 -112
- package/template/wall-e/eval/fixtures/express-buggy-items/test.js +0 -83
- package/template/wall-e/eval/fixtures/express-buggy-search/package.json +0 -9
- package/template/wall-e/eval/fixtures/express-buggy-search/server.js +0 -121
- package/template/wall-e/eval/fixtures/express-buggy-search/test.js +0 -83
- package/template/wall-e/eval/fixtures/express-rename-data/data.js +0 -34
- package/template/wall-e/eval/fixtures/express-rename-data/package.json +0 -9
- package/template/wall-e/eval/fixtures/express-rename-data/server.js +0 -97
- package/template/wall-e/eval/fixtures/express-rename-data/test.js +0 -88
- package/template/wall-e/eval/fixtures/express-xss/package.json +0 -12
- package/template/wall-e/eval/fixtures/express-xss/server.js +0 -90
- package/template/wall-e/eval/fixtures/express-xss/test.js +0 -67
- package/template/wall-e/eval/fixtures/express-xss/views/profile.ejs +0 -9
- package/template/wall-e/eval/fixtures/fullstack-app/config/default.js +0 -9
- package/template/wall-e/eval/fixtures/fullstack-app/config/test.js +0 -13
- package/template/wall-e/eval/fixtures/fullstack-app/package.json +0 -11
- package/template/wall-e/eval/fixtures/fullstack-app/public/css/style.css +0 -137
- package/template/wall-e/eval/fixtures/fullstack-app/public/index.html +0 -46
- package/template/wall-e/eval/fixtures/fullstack-app/public/js/app.js +0 -121
- package/template/wall-e/eval/fixtures/fullstack-app/public/js/auth.js +0 -71
- package/template/wall-e/eval/fixtures/fullstack-app/public/js/items.js +0 -80
- package/template/wall-e/eval/fixtures/fullstack-app/public/js/users.js +0 -46
- package/template/wall-e/eval/fixtures/fullstack-app/public/login.html +0 -45
- package/template/wall-e/eval/fixtures/fullstack-app/public/register.html +0 -38
- package/template/wall-e/eval/fixtures/fullstack-app/scripts/migrate.js +0 -23
- package/template/wall-e/eval/fixtures/fullstack-app/scripts/seed.js +0 -46
- package/template/wall-e/eval/fixtures/fullstack-app/server/db.js +0 -99
- package/template/wall-e/eval/fixtures/fullstack-app/server/index.js +0 -94
- package/template/wall-e/eval/fixtures/fullstack-app/server/middleware/auth.js +0 -19
- package/template/wall-e/eval/fixtures/fullstack-app/server/middleware/logger.js +0 -19
- package/template/wall-e/eval/fixtures/fullstack-app/server/router.js +0 -50
- package/template/wall-e/eval/fixtures/fullstack-app/server/routes/auth.js +0 -69
- package/template/wall-e/eval/fixtures/fullstack-app/server/routes/health.js +0 -23
- package/template/wall-e/eval/fixtures/fullstack-app/server/routes/items.js +0 -88
- package/template/wall-e/eval/fixtures/fullstack-app/server/routes/users.js +0 -75
- package/template/wall-e/eval/fixtures/fullstack-app/server/test.js +0 -198
- package/template/wall-e/eval/fixtures/fullstack-app/server/utils/response.js +0 -34
- package/template/wall-e/eval/fixtures/fullstack-app/server/utils/validate.js +0 -26
- package/template/wall-e/eval/fixtures/fullstack-app/server.js +0 -8
- package/template/wall-e/eval/fixtures/fullstack-app/test.js +0 -12
- package/template/wall-e/eval/fixtures/monorepo-basic/package.json +0 -8
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/data.js +0 -58
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/middleware.js +0 -46
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/package.json +0 -8
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/routes.js +0 -64
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/server.js +0 -56
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/test.js +0 -116
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/cli/commands.js +0 -61
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/cli/index.js +0 -62
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/cli/output.js +0 -43
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/cli/package.json +0 -11
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/cli/test.js +0 -44
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/shared/formatters.js +0 -43
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/shared/index.js +0 -12
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/shared/package.json +0 -5
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/shared/test.js +0 -55
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/shared/validators.js +0 -29
- package/template/wall-e/eval/fixtures/monorepo-basic/test.js +0 -46
- package/template/wall-e/eval/fixtures/node-cli/index.js +0 -78
- package/template/wall-e/eval/fixtures/node-cli/package.json +0 -10
- package/template/wall-e/eval/fixtures/node-cli/test.js +0 -57
- package/template/wall-e/eval/fixtures/node-typed/package.json +0 -8
- package/template/wall-e/eval/fixtures/node-typed/src/handlers.js +0 -31
- package/template/wall-e/eval/fixtures/node-typed/src/utils.js +0 -33
- package/template/wall-e/eval/fixtures/node-typed/test.js +0 -36
- package/template/wall-e/eval/fixtures/python-flask/app.py +0 -14
- package/template/wall-e/eval/fixtures/python-flask/requirements.txt +0 -2
- package/template/wall-e/eval/fixtures/python-flask/test_app.py +0 -25
- package/template/wall-e/eval/fixtures/wall-e-subset/brain.js +0 -105
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/aggregator.js +0 -101
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/benchmarks/chat.json +0 -20
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/benchmarks/coding.json +0 -32
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/benchmarks.js +0 -64
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/fixtures/simple-project/package.json +0 -6
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/fixtures/simple-project/server.js +0 -31
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/fixtures/simple-project/test.js +0 -18
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/fixtures/simple-project/utils.js +0 -34
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/runner.js +0 -104
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/scorer.js +0 -73
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/test.js +0 -134
- package/template/wall-e/eval/fixtures/wall-e-subset/llm/client.js +0 -99
- package/template/wall-e/eval/fixtures/wall-e-subset/llm/providers.js +0 -63
- package/template/wall-e/eval/fixtures/wall-e-subset/llm/test.js +0 -70
- package/template/wall-e/eval/fixtures/wall-e-subset/package.json +0 -10
- package/template/wall-e/eval/fixtures/wall-e-subset/test.js +0 -86
- package/template/wall-e/eval/harvester.js +0 -685
- package/template/wall-e/eval/head-to-head.js +0 -388
- package/template/wall-e/eval/humaneval-adapter.js +0 -321
- package/template/wall-e/eval/list-models.js +0 -31
- package/template/wall-e/eval/livecodebench-adapter.js +0 -291
- package/template/wall-e/eval/mail-integration.js +0 -443
- package/template/wall-e/eval/manifest.js +0 -186
- package/template/wall-e/eval/meta-harness/adapters/coding-agent.js +0 -57
- package/template/wall-e/eval/meta-harness/bootstrap-snapshot.js +0 -149
- package/template/wall-e/eval/meta-harness/candidate-store.js +0 -117
- package/template/wall-e/eval/meta-harness/cli.js +0 -86
- package/template/wall-e/eval/meta-harness/domain-spec.js +0 -154
- package/template/wall-e/eval/meta-harness/domains/coding-agent.domain.json +0 -84
- package/template/wall-e/eval/meta-harness/examples/env-bootstrap-candidate.js +0 -29
- package/template/wall-e/eval/meta-harness/experience-store.js +0 -174
- package/template/wall-e/eval/meta-harness/frontier.js +0 -96
- package/template/wall-e/eval/meta-harness/harness-interface.js +0 -90
- package/template/wall-e/eval/meta-harness/leakage-guard.js +0 -80
- package/template/wall-e/eval/meta-harness/optimizer.js +0 -207
- package/template/wall-e/eval/meta-harness/proposer-runner.js +0 -110
- package/template/wall-e/eval/meta-harness/reporting.js +0 -58
- package/template/wall-e/eval/meta-harness/telemetry.js +0 -27
- package/template/wall-e/eval/meta-harness/validation.js +0 -81
- package/template/wall-e/eval/promoter.js +0 -228
- package/template/wall-e/eval/provider-normalizer.js +0 -33
- package/template/wall-e/eval/replay.js +0 -395
- package/template/wall-e/eval/run-agent-benchmarks.js +0 -386
- package/template/wall-e/eval/run-codex-cli-baseline.js +0 -177
- package/template/wall-e/eval/run-coding-agent-real.js +0 -187
- package/template/wall-e/eval/run-eval.js +0 -435
- package/template/wall-e/eval/run-model-comparison.js +0 -142
- package/template/wall-e/eval/session-evaluator.js +0 -187
- package/template/wall-e/eval/session-miner.js +0 -207
- package/template/wall-e/eval/session-retrieval-benchmark.js +0 -150
- package/template/wall-e/eval/session-transcripts.js +0 -509
- package/template/wall-e/eval/shadow.js +0 -161
- package/template/wall-e/eval/swebench-adapter.js +0 -345
- package/template/wall-e/eval/swebench-docker.js +0 -192
- package/template/wall-e/eval/train.py +0 -320
- package/template/wall-e/eval/trainer.js +0 -232
- package/template/wall-e/eval/weekly-eval-loop.js +0 -241
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
(function() {
|
|
3
3
|
const SETUP = {};
|
|
4
4
|
|
|
5
|
-
var _activeSetupTab = '
|
|
5
|
+
var _activeSetupTab = 'integrations';
|
|
6
6
|
var _selectedProvider = 'anthropic';
|
|
7
7
|
var _mcpPort = 3457;
|
|
8
8
|
var _embedProvidersCache = null;
|
|
@@ -11,10 +11,14 @@ var _isConfigured = false;
|
|
|
11
11
|
var _setupTabsBound = false;
|
|
12
12
|
var _providerPickerBound = false;
|
|
13
13
|
var _pcardAccordionBound = false;
|
|
14
|
+
var _connectedServicesLifecycleBound = false;
|
|
15
|
+
var _connectedServicesRefreshTimer = null;
|
|
16
|
+
var _connectedServicesLoadSeq = 0;
|
|
14
17
|
var _setupMemoryCaptureBound = false;
|
|
15
18
|
var _setupMemoryCaptureTimer = null;
|
|
19
|
+
var _retiredSetupTabs = new Set(['provider']);
|
|
16
20
|
var _setupTempMemory = {
|
|
17
|
-
activeTab: '
|
|
21
|
+
activeTab: 'integrations',
|
|
18
22
|
values: null,
|
|
19
23
|
details: null,
|
|
20
24
|
scroll: null,
|
|
@@ -27,7 +31,96 @@ var _setupTempMemory = {
|
|
|
27
31
|
var _microsoftSetupInFlight = false;
|
|
28
32
|
var _microsoftProgressPollTimer = null;
|
|
29
33
|
var _microsoftProbeInFlight = false;
|
|
34
|
+
var _microsoftLoginCheckInFlight = false;
|
|
30
35
|
var _microsoftTunnelProbe = null;
|
|
36
|
+
// Dev Tunnels sign-in account type. GitHub sign-ins expire on an hours-to-days
|
|
37
|
+
// scale and break the tunnel until re-login; Microsoft accounts refresh silently.
|
|
38
|
+
// Resolution: explicit user choice (persisted) > server's last-used provider >
|
|
39
|
+
// 'microsoft' (recommended default).
|
|
40
|
+
var _microsoftLoginProviderChoice = '';
|
|
41
|
+
try { _microsoftLoginProviderChoice = localStorage.getItem('ctm-ms-login-provider') || ''; } catch {}
|
|
42
|
+
|
|
43
|
+
function _msLoginProvider() {
|
|
44
|
+
if (_microsoftLoginProviderChoice === 'microsoft' || _microsoftLoginProviderChoice === 'github') {
|
|
45
|
+
return _microsoftLoginProviderChoice;
|
|
46
|
+
}
|
|
47
|
+
var ms = (_lastNetworkSettings || {}).microsoft_dev_tunnel || {};
|
|
48
|
+
if (ms.login_provider === 'github' || ms.login_provider === 'microsoft') return ms.login_provider;
|
|
49
|
+
return 'microsoft';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function _msProviderLabel(provider) {
|
|
53
|
+
return (provider || _msLoginProvider()) === 'github' ? 'GitHub' : 'Microsoft';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function _renderMicrosoftLoginProvider() {
|
|
57
|
+
var provider = _msLoginProvider();
|
|
58
|
+
var msBtn = document.getElementById('setup-ms-provider-microsoft');
|
|
59
|
+
var ghBtn = document.getElementById('setup-ms-provider-github');
|
|
60
|
+
if (msBtn) {
|
|
61
|
+
msBtn.setAttribute('aria-pressed', provider === 'microsoft' ? 'true' : 'false');
|
|
62
|
+
msBtn.disabled = _microsoftSetupInFlight;
|
|
63
|
+
}
|
|
64
|
+
if (ghBtn) {
|
|
65
|
+
ghBtn.setAttribute('aria-pressed', provider === 'github' ? 'true' : 'false');
|
|
66
|
+
ghBtn.disabled = _microsoftSetupInFlight;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function setMicrosoftLoginProvider(provider) {
|
|
71
|
+
if (provider !== 'microsoft' && provider !== 'github') return;
|
|
72
|
+
var previous = _msLoginProvider();
|
|
73
|
+
_microsoftLoginProviderChoice = provider;
|
|
74
|
+
try { localStorage.setItem('ctm-ms-login-provider', provider); } catch {}
|
|
75
|
+
_renderMicrosoftLoginProvider();
|
|
76
|
+
var ms = (_lastNetworkSettings || {}).microsoft_dev_tunnel || {};
|
|
77
|
+
if (provider !== previous && ms.signed_in) {
|
|
78
|
+
_setMicrosoftActionStatus('Next sign-in will use a ' + _msProviderLabel(provider) + ' account. Click "Use Different Account" to switch now.', '');
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Human-readable "what happened last" line from the managed host_events ring
|
|
83
|
+
// buffer (drop diagnosis: credential expiry vs relay disconnects).
|
|
84
|
+
function _msLastHostEventText(events) {
|
|
85
|
+
if (!Array.isArray(events) || !events.length) return '';
|
|
86
|
+
var ev = events[events.length - 1];
|
|
87
|
+
if (!ev || !ev.type) return '';
|
|
88
|
+
var labels = {
|
|
89
|
+
credential_expired: 'Dev Tunnels sign-in expired',
|
|
90
|
+
auth_failure: 'tunnel authorization failed',
|
|
91
|
+
connection_lost: 'tunnel relay connection lost',
|
|
92
|
+
reconnected: 'tunnel relay reconnected',
|
|
93
|
+
signed_in: 'Dev Tunnels signed in',
|
|
94
|
+
host_started: 'tunnel started',
|
|
95
|
+
tunnel_offline: 'tunnel went offline',
|
|
96
|
+
tunnel_error: 'tunnel reported an error',
|
|
97
|
+
};
|
|
98
|
+
var what = labels[ev.type] || String(ev.type).replace(/_/g, ' ');
|
|
99
|
+
var when = ev.at ? new Date(ev.at).toLocaleString() : '';
|
|
100
|
+
return 'Last event: ' + what + (when ? ' (' + when + ').' : '.');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function tryOtherMicrosoftLoginProvider() {
|
|
104
|
+
var other = _msLoginProvider() === 'github' ? 'microsoft' : 'github';
|
|
105
|
+
setMicrosoftLoginProvider(other);
|
|
106
|
+
var btn = document.getElementById('setup-ms-login-try-other');
|
|
107
|
+
if (btn) { btn.disabled = true; btn.textContent = 'Starting...'; }
|
|
108
|
+
try {
|
|
109
|
+
await _postMicrosoftTunnelLogin(true, { forceNew: true });
|
|
110
|
+
_setMicrosoftActionStatus(_msProviderLabel(other) + ' sign-in started. Enter the displayed code on the ' + _msProviderLabel(other) + ' page.', 'ok');
|
|
111
|
+
} catch (e) {
|
|
112
|
+
_setMicrosoftActionStatus(e.message || 'Could not start ' + _msProviderLabel(other) + ' sign-in', 'error');
|
|
113
|
+
} finally {
|
|
114
|
+
if (btn) btn.disabled = false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// Dev Tunnel default is app-gated ("ctm_authenticated"): the URL is reachable and
|
|
118
|
+
// CTM's own passkey + device-token auth is the gate. This is the only mode the
|
|
119
|
+
// phone PWA/WebSocket can use — Dev Tunnels "private" mode uses an interactive
|
|
120
|
+
// edge sign-in that a PWA's fetch/WebSocket can't follow. Private is an explicit
|
|
121
|
+
// opt-in (and is migrated back to app-gated on headless restart by the backend).
|
|
122
|
+
var _microsoftTunnelAccessMode = 'ctm_authenticated';
|
|
123
|
+
var _microsoftTunnelAccessModeTouched = false;
|
|
31
124
|
var _activeDeviceClaim = null;
|
|
32
125
|
var _deviceClaimScopeUpdateTimer = null;
|
|
33
126
|
var _deviceClaimScopeUpdateSeq = 0;
|
|
@@ -38,7 +131,15 @@ var _lastPersistedDeviceLabel = '';
|
|
|
38
131
|
var _deviceLabelSaveInFlight = null;
|
|
39
132
|
var _deviceLabelQueuedValue = null;
|
|
40
133
|
var _deviceLabelLifecycleBound = false;
|
|
134
|
+
var _deviceScopePersistTimer = null;
|
|
135
|
+
var _deviceScopeUserTouched = false;
|
|
136
|
+
var _lastPersistedDeviceScopesKey = '';
|
|
137
|
+
var _deviceScopeLifecycleBound = false;
|
|
138
|
+
var _pairingRequestsPollTimer = null;
|
|
41
139
|
var DEVICE_LABEL_SETTING_KEY = 'ui_setup_device_label';
|
|
140
|
+
var DEVICE_SCOPE_SETTING_KEY = 'ui_setup_device_scopes';
|
|
141
|
+
var DEVICE_SCOPE_VALUES = ['read', 'respond', 'create', 'admin'];
|
|
142
|
+
var DEFAULT_DEVICE_SCOPES = ['read', 'respond'];
|
|
42
143
|
var DEFAULT_DEVICE_LABEL = 'Owner iPhone';
|
|
43
144
|
|
|
44
145
|
// ── Toast (delegates to dashboard's showToast) ──────────────────────
|
|
@@ -168,10 +269,109 @@ async function loadDeviceLabelSetting() {
|
|
|
168
269
|
} catch { /* setup status also carries this value */ }
|
|
169
270
|
}
|
|
170
271
|
|
|
272
|
+
function _normalizeDeviceScopes(scopes) {
|
|
273
|
+
var raw = Array.isArray(scopes) ? scopes : String(scopes || '').split(',');
|
|
274
|
+
var seen = {};
|
|
275
|
+
raw.forEach(function(item) {
|
|
276
|
+
var scope = String(item || '').trim().toLowerCase();
|
|
277
|
+
if (DEVICE_SCOPE_VALUES.indexOf(scope) >= 0) seen[scope] = true;
|
|
278
|
+
});
|
|
279
|
+
if (seen.admin) {
|
|
280
|
+
DEVICE_SCOPE_VALUES.forEach(function(scope) { seen[scope] = true; });
|
|
281
|
+
}
|
|
282
|
+
var out = DEVICE_SCOPE_VALUES.filter(function(scope) { return !!seen[scope]; });
|
|
283
|
+
return out.length ? out : DEFAULT_DEVICE_SCOPES.slice();
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function _applyPersistedDeviceScopes(scopes, options) {
|
|
287
|
+
var normalized = _normalizeDeviceScopes(scopes);
|
|
288
|
+
var opts = options || {};
|
|
289
|
+
if (opts.force || !_deviceScopeUserTouched) {
|
|
290
|
+
var selected = {};
|
|
291
|
+
normalized.forEach(function(scope) { selected[scope] = true; });
|
|
292
|
+
document.querySelectorAll('#setup-device-card [data-device-scope]').forEach(function(cb) {
|
|
293
|
+
cb.checked = !!selected[cb.getAttribute('data-device-scope')];
|
|
294
|
+
});
|
|
295
|
+
_normalizeDeviceScopeCheckboxes();
|
|
296
|
+
}
|
|
297
|
+
_lastPersistedDeviceScopesKey = _scopeKey(normalized);
|
|
298
|
+
if (!_deviceClaimVisible()) {
|
|
299
|
+
_setDeviceScopeStatus('Selected permissions for the next QR: ' + _scopeListText(_selectedDeviceScopes()) + '.', '');
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async function _writeDeviceScopeSetting(scopes) {
|
|
304
|
+
var normalized = _normalizeDeviceScopes(scopes);
|
|
305
|
+
var key = _scopeKey(normalized);
|
|
306
|
+
if (key === _lastPersistedDeviceScopesKey) return normalized;
|
|
307
|
+
var body = {};
|
|
308
|
+
body[DEVICE_SCOPE_SETTING_KEY] = normalized;
|
|
309
|
+
var headers = { 'Content-Type': 'application/json' };
|
|
310
|
+
if (typeof state !== 'undefined' && state && state.clientId) headers['X-CTM-Client-Id'] = state.clientId;
|
|
311
|
+
var r = await fetch('/api/settings', {
|
|
312
|
+
method: 'PUT',
|
|
313
|
+
headers: headers,
|
|
314
|
+
body: JSON.stringify(body),
|
|
315
|
+
});
|
|
316
|
+
var d = await r.json().catch(function() { return {}; });
|
|
317
|
+
if (!r.ok || d.busy || d.error) throw new Error(d.error || 'Phone permission save failed');
|
|
318
|
+
_lastPersistedDeviceScopesKey = key;
|
|
319
|
+
return normalized;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function _persistDeviceScopes(scopes, options) {
|
|
323
|
+
var opts = options || {};
|
|
324
|
+
var normalized = _normalizeDeviceScopes(scopes);
|
|
325
|
+
if (opts.immediate) {
|
|
326
|
+
clearTimeout(_deviceScopePersistTimer);
|
|
327
|
+
_deviceScopePersistTimer = null;
|
|
328
|
+
return _writeDeviceScopeSetting(normalized);
|
|
329
|
+
}
|
|
330
|
+
clearTimeout(_deviceScopePersistTimer);
|
|
331
|
+
_deviceScopePersistTimer = setTimeout(function() {
|
|
332
|
+
_writeDeviceScopeSetting(normalized).catch(function(e) {
|
|
333
|
+
_setDeviceError(e.message || 'Phone permission save failed');
|
|
334
|
+
});
|
|
335
|
+
}, 150);
|
|
336
|
+
return Promise.resolve(normalized);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function _flushDeviceScopesOnPageHide() {
|
|
340
|
+
if (!_deviceScopeUserTouched) return;
|
|
341
|
+
var body = {};
|
|
342
|
+
body[DEVICE_SCOPE_SETTING_KEY] = _selectedDeviceScopes();
|
|
343
|
+
try {
|
|
344
|
+
fetch('/api/settings', {
|
|
345
|
+
method: 'PUT',
|
|
346
|
+
headers: { 'Content-Type': 'application/json' },
|
|
347
|
+
body: JSON.stringify(body),
|
|
348
|
+
keepalive: true,
|
|
349
|
+
}).catch(function() {});
|
|
350
|
+
} catch { /* best effort during unload */ }
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function initDeviceScopeLifecycle() {
|
|
354
|
+
if (_deviceScopeLifecycleBound) return;
|
|
355
|
+
_deviceScopeLifecycleBound = true;
|
|
356
|
+
window.addEventListener('pagehide', _flushDeviceScopesOnPageHide);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
async function loadDeviceScopeSetting() {
|
|
360
|
+
try {
|
|
361
|
+
var r = await fetch('/api/settings?key=' + encodeURIComponent(DEVICE_SCOPE_SETTING_KEY));
|
|
362
|
+
var d = await r.json();
|
|
363
|
+
if (!r.ok || d.busy) return;
|
|
364
|
+
if (d && d.value) _applyPersistedDeviceScopes(d.value);
|
|
365
|
+
else _lastPersistedDeviceScopesKey = _scopeKey(_selectedDeviceScopes());
|
|
366
|
+
} catch {
|
|
367
|
+
_lastPersistedDeviceScopesKey = _scopeKey(_selectedDeviceScopes());
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
171
371
|
// ── Tab navigation ──────────────────────────────────────────────────
|
|
172
372
|
function _setSetupActiveTab(target) {
|
|
173
|
-
if (!target) target = '
|
|
174
|
-
if (!document.getElementById('setup-section-' + target)) target = '
|
|
373
|
+
if (!target || _retiredSetupTabs.has(target)) target = 'integrations';
|
|
374
|
+
if (!document.getElementById('setup-section-' + target)) target = 'integrations';
|
|
175
375
|
_activeSetupTab = target;
|
|
176
376
|
_setupTempMemory.activeTab = target;
|
|
177
377
|
var tabs = document.querySelectorAll('#setup-panel .setup-tab');
|
|
@@ -184,11 +384,15 @@ function _setSetupActiveTab(target) {
|
|
|
184
384
|
document.querySelectorAll('#setup-panel .setup-section').forEach(function(p) {
|
|
185
385
|
p.style.display = p.id === 'setup-section-' + target ? '' : 'none';
|
|
186
386
|
});
|
|
387
|
+
if (target === 'backups' && typeof window.loadBackupsData === 'function') {
|
|
388
|
+
window.loadBackupsData();
|
|
389
|
+
}
|
|
187
390
|
}
|
|
188
391
|
|
|
189
392
|
function _setupTabFromLocation() {
|
|
190
393
|
var hash = (window.location.hash || '').replace(/^#/, '');
|
|
191
394
|
if (hash === 'devices' || hash === 'setup-access' || hash === 'setup-devices') return 'access';
|
|
395
|
+
if (hash === 'backups') return 'backups';
|
|
192
396
|
if (hash.indexOf('setup&') === 0) {
|
|
193
397
|
var parts = hash.split('&');
|
|
194
398
|
for (var i = 1; i < parts.length; i++) {
|
|
@@ -200,7 +404,8 @@ function _setupTabFromLocation() {
|
|
|
200
404
|
}
|
|
201
405
|
|
|
202
406
|
function _initialSetupTab() {
|
|
203
|
-
|
|
407
|
+
var target = _setupTabFromLocation() || _setupTempMemory.activeTab || _activeSetupTab || 'integrations';
|
|
408
|
+
return _retiredSetupTabs.has(target) ? 'integrations' : target;
|
|
204
409
|
}
|
|
205
410
|
|
|
206
411
|
function _setupFieldKey(el) {
|
|
@@ -253,7 +458,7 @@ function captureSetupTempState(options) {
|
|
|
253
458
|
if (el.id) scroll[el.id] = Number(el.scrollTop || 0);
|
|
254
459
|
});
|
|
255
460
|
_setupTempMemory = {
|
|
256
|
-
activeTab: _activeSetupTab || _setupTempMemory.activeTab || '
|
|
461
|
+
activeTab: _activeSetupTab || _setupTempMemory.activeTab || 'integrations',
|
|
257
462
|
values: values,
|
|
258
463
|
details: details,
|
|
259
464
|
scroll: scroll,
|
|
@@ -1569,7 +1774,34 @@ function _googleSourceLabel(accounts, serviceKey) {
|
|
|
1569
1774
|
return 'Google (' + count + ')';
|
|
1570
1775
|
}
|
|
1571
1776
|
|
|
1777
|
+
function _isSetupVisibleForRefresh() {
|
|
1778
|
+
var panel = document.getElementById('setup-panel');
|
|
1779
|
+
if (panel && panel.classList.contains('active')) return true;
|
|
1780
|
+
var hash = window.location && window.location.hash ? window.location.hash : '';
|
|
1781
|
+
return /^#(?:setup(?:&|$)|setup-access$|setup-devices$|devices$)/.test(hash);
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
function _scheduleConnectedServicesRefresh() {
|
|
1785
|
+
if (!_isSetupVisibleForRefresh()) return;
|
|
1786
|
+
clearTimeout(_connectedServicesRefreshTimer);
|
|
1787
|
+
_connectedServicesRefreshTimer = setTimeout(function() {
|
|
1788
|
+
loadConnectedServices().catch(function() {});
|
|
1789
|
+
}, 80);
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
function initConnectedServicesLifecycle() {
|
|
1793
|
+
if (_connectedServicesLifecycleBound) return;
|
|
1794
|
+
_connectedServicesLifecycleBound = true;
|
|
1795
|
+
window.addEventListener('online', function() { _scheduleConnectedServicesRefresh(); _refreshAccessHealthOnReturn(); });
|
|
1796
|
+
window.addEventListener('focus', function() { _scheduleConnectedServicesRefresh(); _refreshAccessHealthOnReturn(); });
|
|
1797
|
+
window.addEventListener('pageshow', function() { _scheduleConnectedServicesRefresh(); _refreshAccessHealthOnReturn(); });
|
|
1798
|
+
document.addEventListener('visibilitychange', function() {
|
|
1799
|
+
if (!document.hidden) { _scheduleConnectedServicesRefresh(); _refreshAccessHealthOnReturn(); }
|
|
1800
|
+
});
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1572
1803
|
async function loadConnectedServices() {
|
|
1804
|
+
var seq = ++_connectedServicesLoadSeq;
|
|
1573
1805
|
var container = document.getElementById('setup-svc-accounts-list');
|
|
1574
1806
|
var matrixEl = document.getElementById('setup-svc-matrix');
|
|
1575
1807
|
var dotEl = document.getElementById('setup-svc-dot');
|
|
@@ -1577,8 +1809,12 @@ async function loadConnectedServices() {
|
|
|
1577
1809
|
var addBtn = document.getElementById('setup-svc-add-google-btn');
|
|
1578
1810
|
|
|
1579
1811
|
try {
|
|
1580
|
-
var r = await fetch('/api/wall-e/gws/accounts'
|
|
1812
|
+
var r = await fetch('/api/wall-e/gws/accounts', {
|
|
1813
|
+
cache: 'no-store',
|
|
1814
|
+
headers: { 'Cache-Control': 'no-cache' },
|
|
1815
|
+
});
|
|
1581
1816
|
var d = await r.json();
|
|
1817
|
+
if (seq !== _connectedServicesLoadSeq) return _gwsAccountsCache || d;
|
|
1582
1818
|
_gwsAccountsCache = d;
|
|
1583
1819
|
_syncGoogleReauthAlert(d);
|
|
1584
1820
|
|
|
@@ -1655,11 +1891,12 @@ async function loadConnectedServices() {
|
|
|
1655
1891
|
}
|
|
1656
1892
|
return d;
|
|
1657
1893
|
} catch (e) {
|
|
1894
|
+
if (seq !== _connectedServicesLoadSeq) return _gwsAccountsCache;
|
|
1658
1895
|
_syncGoogleReauthAlert(null);
|
|
1659
1896
|
var errDiv = document.createElement('div');
|
|
1660
1897
|
errDiv.style.cssText = 'color:var(--fg-dim);font-size:12px;padding:8px 0;';
|
|
1661
1898
|
errDiv.textContent = 'Could not load services.';
|
|
1662
|
-
container.replaceChildren(errDiv);
|
|
1899
|
+
if (container) container.replaceChildren(errDiv);
|
|
1663
1900
|
return null;
|
|
1664
1901
|
}
|
|
1665
1902
|
}
|
|
@@ -2065,6 +2302,47 @@ function _microsoftTunnelCanStart(d) {
|
|
|
2065
2302
|
return !!(ms.installed && ms.signed_in);
|
|
2066
2303
|
}
|
|
2067
2304
|
|
|
2305
|
+
function _microsoftTunnelAccess(ms) {
|
|
2306
|
+
ms = ms || {};
|
|
2307
|
+
var managed = ms.managed_tunnel || {};
|
|
2308
|
+
return managed.access || ms.access || {};
|
|
2309
|
+
}
|
|
2310
|
+
|
|
2311
|
+
function _normalizeMicrosoftTunnelAccessMode(mode) {
|
|
2312
|
+
var raw = String(mode || '').trim().toLowerCase().replace(/[-\s]+/g, '_');
|
|
2313
|
+
if (raw === 'private' || raw === 'microsoft' || raw === 'private_microsoft') return 'private_microsoft';
|
|
2314
|
+
if (raw === 'anonymous' || raw === 'public' || raw === 'ctm_auth' || raw === 'ctm_authenticated') return 'ctm_authenticated';
|
|
2315
|
+
return '';
|
|
2316
|
+
}
|
|
2317
|
+
|
|
2318
|
+
function _microsoftTunnelCurrentAccessMode(ms) {
|
|
2319
|
+
ms = ms || {};
|
|
2320
|
+
var managed = ms.managed_tunnel || {};
|
|
2321
|
+
var access = _microsoftTunnelAccess(ms);
|
|
2322
|
+
return _normalizeMicrosoftTunnelAccessMode(access.mode || managed.access_mode || ms.access_mode) || 'ctm_authenticated';
|
|
2323
|
+
}
|
|
2324
|
+
|
|
2325
|
+
function _microsoftTunnelSelectedAccessMode(ms) {
|
|
2326
|
+
if (!_microsoftTunnelAccessModeTouched) {
|
|
2327
|
+
var current = _microsoftTunnelCurrentAccessMode(ms);
|
|
2328
|
+
_microsoftTunnelAccessMode = current === 'ctm_authenticated' ? current : 'ctm_authenticated';
|
|
2329
|
+
}
|
|
2330
|
+
return _normalizeMicrosoftTunnelAccessMode(_microsoftTunnelAccessMode) || 'ctm_authenticated';
|
|
2331
|
+
}
|
|
2332
|
+
|
|
2333
|
+
function _microsoftTunnelUsesCtmAuth(ms) {
|
|
2334
|
+
return _microsoftTunnelSelectedAccessMode(ms) === 'ctm_authenticated';
|
|
2335
|
+
}
|
|
2336
|
+
|
|
2337
|
+
function _microsoftAccessModeMatchesSelection(ms) {
|
|
2338
|
+
return _microsoftTunnelCurrentAccessMode(ms) === _microsoftTunnelSelectedAccessMode(ms);
|
|
2339
|
+
}
|
|
2340
|
+
|
|
2341
|
+
function _microsoftNeedsPrivateAccessReset(ms) {
|
|
2342
|
+
var access = _microsoftTunnelAccess(ms);
|
|
2343
|
+
return !!(access.needs_private_reset || access.stale_anonymous_access || (access.mode === 'private_microsoft' && access.anonymous_connect));
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2068
2346
|
function _microsoftTunnelAccountText(ms) {
|
|
2069
2347
|
ms = ms || {};
|
|
2070
2348
|
return ms.account && (ms.account.display || ms.account.email || ms.account.name || ms.account.id) || '';
|
|
@@ -2094,10 +2372,13 @@ function _microsoftAvailabilityCopy(ms, ready) {
|
|
|
2094
2372
|
}
|
|
2095
2373
|
|
|
2096
2374
|
function _microsoftTunnelPhoneAuthText(ms) {
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2375
|
+
if (_microsoftNeedsPrivateAccessReset(ms)) {
|
|
2376
|
+
return 'Anonymous tunnel access is still enabled. Reset to private before relying on the phone URL.';
|
|
2377
|
+
}
|
|
2378
|
+
if (_microsoftTunnelUsesCtmAuth(ms)) {
|
|
2379
|
+
return 'The phone URL reaches CTM directly. CTM still requires pairing, device token, route permissions, and passkey step-up.';
|
|
2380
|
+
}
|
|
2381
|
+
return 'Remote browsers pass Microsoft/GitHub tunnel auth first; CTM still requires pairing, device token, route permissions, and passkey step-up.';
|
|
2101
2382
|
}
|
|
2102
2383
|
|
|
2103
2384
|
function _microsoftTunnelOrigin(d) {
|
|
@@ -2116,6 +2397,9 @@ function _tailscaleOrigin(d) {
|
|
|
2116
2397
|
}
|
|
2117
2398
|
|
|
2118
2399
|
function _defaultPhoneAccessMethod(d) {
|
|
2400
|
+
// Prefer the live recommended method (the one that's actually working); else Microsoft
|
|
2401
|
+
// tunnel (simplest — no VPN app, no custom domain).
|
|
2402
|
+
if (_lastConnectionHealth && _lastConnectionHealth.recommended) return _lastConnectionHealth.recommended;
|
|
2119
2403
|
return 'microsoft';
|
|
2120
2404
|
}
|
|
2121
2405
|
|
|
@@ -2168,16 +2452,33 @@ function _clearPhoneOriginIfAuto() {
|
|
|
2168
2452
|
}
|
|
2169
2453
|
}
|
|
2170
2454
|
|
|
2455
|
+
function _phoneOriginEmptyHint(method) {
|
|
2456
|
+
if (method === 'walle') return 'Connect Walle Remote to generate a phone URL';
|
|
2457
|
+
if (method === 'cloudflare') return 'Finish Cloudflare setup to generate a phone URL';
|
|
2458
|
+
if (method === 'microsoft') return 'Start the Microsoft tunnel to generate a phone URL';
|
|
2459
|
+
return 'Sign in to Tailscale to generate a phone URL';
|
|
2460
|
+
}
|
|
2461
|
+
|
|
2171
2462
|
function _renderRecommendedPhoneOrigin(origin, method) {
|
|
2463
|
+
// Until /api/setup/network resolves, _lastNetworkSettings is null and we
|
|
2464
|
+
// cannot know the real origin yet — show an explicit loading state instead
|
|
2465
|
+
// of a fake placeholder URL that reads as a real (wrong) value.
|
|
2466
|
+
var loaded = _lastNetworkSettings !== null;
|
|
2172
2467
|
var label = document.getElementById('setup-phone-best-label');
|
|
2173
2468
|
var walleReady = method === 'walle' && _walleRemoteHostedPairingReady(_lastNetworkSettings || {});
|
|
2174
2469
|
if (label) label.textContent = method === 'walle'
|
|
2175
2470
|
? (walleReady ? 'Walle Remote URL' : 'Walle Remote status')
|
|
2176
2471
|
: (method === 'cloudflare' ? 'Recommended Cloudflare URL' : (method === 'microsoft' ? 'Microsoft tunnel URL' : 'Recommended Tailscale URL'));
|
|
2177
2472
|
var best = document.getElementById('setup-phone-best-url');
|
|
2178
|
-
if (best) best.textContent =
|
|
2179
|
-
?
|
|
2180
|
-
: (
|
|
2473
|
+
if (best) best.textContent = !loaded
|
|
2474
|
+
? 'Resolving…'
|
|
2475
|
+
: (method === 'walle'
|
|
2476
|
+
? (walleReady ? _walleRemoteUsableMobileUrl(_lastNetworkSettings || {}) : 'Hosted relay unavailable')
|
|
2477
|
+
: (origin ? origin.replace(/\/+$/, '') + '/m/' : (method === 'microsoft' ? 'Start tunnel first' : '')));
|
|
2478
|
+
var phoneInput = document.getElementById('setup-device-origin');
|
|
2479
|
+
if (phoneInput) phoneInput.placeholder = origin
|
|
2480
|
+
? ''
|
|
2481
|
+
: (!loaded ? 'Resolving phone URL…' : (walleReady ? '' : _phoneOriginEmptyHint(method)));
|
|
2181
2482
|
}
|
|
2182
2483
|
|
|
2183
2484
|
function _renderPhoneSetupGuidance(method, d) {
|
|
@@ -2196,7 +2497,7 @@ function _renderPhoneSetupGuidance(method, d) {
|
|
|
2196
2497
|
title.textContent = 'Phone setup: Walle Remote';
|
|
2197
2498
|
body.textContent = _walleRemoteHostedPairingReady(d)
|
|
2198
2499
|
? 'Open Walle on your phone, sign in to the same Walle account, then scan the pairing QR from this Mac. CTM keeps running locally and the phone sends typed remote-control actions through Walle Remote.'
|
|
2199
|
-
: 'Hosted Walle Relay pairing is not connected yet, and m.walle.sh is not a working CTM phone URL for this install. Choose
|
|
2500
|
+
: 'Hosted Walle Relay pairing is not connected yet, and m.walle.sh is not a working CTM phone URL for this install. Choose Microsoft tunnel or Tailscale for phone access.';
|
|
2200
2501
|
return;
|
|
2201
2502
|
}
|
|
2202
2503
|
if (method === 'cloudflare') {
|
|
@@ -2225,7 +2526,7 @@ function _hideDeviceClaim() {
|
|
|
2225
2526
|
if (url) url.textContent = '';
|
|
2226
2527
|
_activeDeviceClaim = null;
|
|
2227
2528
|
clearTimeout(_deviceClaimScopeUpdateTimer);
|
|
2228
|
-
_setDeviceScopeStatus('Choose permissions before Pair Phone.
|
|
2529
|
+
_setDeviceScopeStatus('Choose permissions before Pair Phone. CTM saves them and writes them into the QR claim when it is created.', '');
|
|
2229
2530
|
}
|
|
2230
2531
|
|
|
2231
2532
|
function _renderDeviceClaimAction(d) {
|
|
@@ -2370,11 +2671,14 @@ function _renderMicrosoftProgress(progress) {
|
|
|
2370
2671
|
var codeWrap = document.getElementById('setup-ms-login-code-wrap');
|
|
2371
2672
|
var codeEl = document.getElementById('setup-ms-login-code');
|
|
2372
2673
|
var copyCode = document.getElementById('setup-ms-login-copy');
|
|
2674
|
+
var checkLogin = document.getElementById('setup-ms-login-check');
|
|
2675
|
+
var regenerateCode = document.getElementById('setup-ms-login-regenerate');
|
|
2373
2676
|
var install = progress && progress.install || {};
|
|
2374
2677
|
var login = progress && progress.login || {};
|
|
2375
2678
|
var installTail = _processTail(install);
|
|
2376
2679
|
var loginTail = _processTail(login);
|
|
2377
|
-
var
|
|
2680
|
+
var signedInProgress = !!login.signed_in;
|
|
2681
|
+
var hasLoginFallback = !signedInProgress && !!(login.login_url || login.device_code);
|
|
2378
2682
|
var show = !!(install.running || login.running || installTail || loginTail || hasLoginFallback || install.error || login.error);
|
|
2379
2683
|
panel.style.display = show ? '' : 'none';
|
|
2380
2684
|
if (!show) return;
|
|
@@ -2385,6 +2689,7 @@ function _renderMicrosoftProgress(progress) {
|
|
|
2385
2689
|
else if (typeof install.exit_code === 'number' && install.exit_code !== 0) state = 'Install exited with code ' + install.exit_code;
|
|
2386
2690
|
else if (login.error) state = 'Sign-in failed';
|
|
2387
2691
|
else if (typeof login.exit_code === 'number' && login.exit_code !== 0) state = 'Sign-in exited with code ' + login.exit_code;
|
|
2692
|
+
else if (login.signed_in) state = 'Signed in';
|
|
2388
2693
|
else if (login.running) state = 'Waiting for sign-in...';
|
|
2389
2694
|
else if (login.finished_at) state = 'Sign-in process finished';
|
|
2390
2695
|
else if (install.finished_at) state = 'Install finished';
|
|
@@ -2405,6 +2710,24 @@ function _renderMicrosoftProgress(progress) {
|
|
|
2405
2710
|
if (codeEl) codeEl.textContent = login.device_code || '';
|
|
2406
2711
|
if (codeWrap) codeWrap.style.display = login.device_code ? '' : 'none';
|
|
2407
2712
|
if (copyCode) copyCode.style.display = login.device_code ? '' : 'none';
|
|
2713
|
+
if (checkLogin) {
|
|
2714
|
+
checkLogin.style.display = hasLoginFallback || login.finished_at ? '' : 'none';
|
|
2715
|
+
checkLogin.disabled = !!_microsoftLoginCheckInFlight;
|
|
2716
|
+
checkLogin.textContent = _microsoftLoginCheckInFlight ? 'Checking...' : 'Check Sign-In';
|
|
2717
|
+
}
|
|
2718
|
+
if (regenerateCode) {
|
|
2719
|
+
regenerateCode.style.display = login.device_code ? '' : 'none';
|
|
2720
|
+
regenerateCode.disabled = !!_microsoftSetupInFlight;
|
|
2721
|
+
regenerateCode.title = 'Start a fresh ' + _msProviderLabel() + ' device-code sign-in if this code expired.';
|
|
2722
|
+
}
|
|
2723
|
+
var tryOther = document.getElementById('setup-ms-login-try-other');
|
|
2724
|
+
if (tryOther) {
|
|
2725
|
+
// Sign-in failed or is dragging: offer the other account type so a broken
|
|
2726
|
+
// Microsoft login is never a dead end (and vice versa).
|
|
2727
|
+
var otherLabel = _msLoginProvider() === 'github' ? 'Microsoft' : 'GitHub';
|
|
2728
|
+
tryOther.textContent = 'Try ' + otherLabel + ' instead';
|
|
2729
|
+
tryOther.style.display = (login.error || login.error_code || hasLoginFallback) && !signedInProgress ? '' : 'none';
|
|
2730
|
+
}
|
|
2408
2731
|
if (fallback) fallback.style.display = hasLoginFallback ? '' : 'none';
|
|
2409
2732
|
|
|
2410
2733
|
if (logEl) {
|
|
@@ -2430,12 +2753,88 @@ async function copyMicrosoftLoginCode() {
|
|
|
2430
2753
|
}
|
|
2431
2754
|
}
|
|
2432
2755
|
|
|
2756
|
+
async function checkMicrosoftTunnelLogin(options) {
|
|
2757
|
+
var opts = options || {};
|
|
2758
|
+
if (_microsoftLoginCheckInFlight) return null;
|
|
2759
|
+
_microsoftLoginCheckInFlight = true;
|
|
2760
|
+
var err = document.getElementById('setup-network-err');
|
|
2761
|
+
var btn = document.getElementById('setup-ms-login-check');
|
|
2762
|
+
if (err && !opts.silent) err.style.display = 'none';
|
|
2763
|
+
if (btn) { btn.disabled = true; btn.textContent = 'Checking...'; }
|
|
2764
|
+
try {
|
|
2765
|
+
var r = await fetch('/api/setup/network/microsoft-dev-tunnel/login/check', {
|
|
2766
|
+
method: 'POST',
|
|
2767
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2768
|
+
body: '{}',
|
|
2769
|
+
});
|
|
2770
|
+
var d = await r.json();
|
|
2771
|
+
var setup = d.setup || d.microsoft_dev_tunnel || null;
|
|
2772
|
+
if (setup) {
|
|
2773
|
+
var settings = _lastNetworkSettings || {};
|
|
2774
|
+
settings.microsoft_dev_tunnel = setup;
|
|
2775
|
+
_lastNetworkSettings = settings;
|
|
2776
|
+
_renderMicrosoftDevTunnelSetup(setup, settings);
|
|
2777
|
+
_renderAccessMethodSelection(settings);
|
|
2778
|
+
_renderPhoneSetupGuidance(_selectedPhoneAccessMethod || 'microsoft', settings);
|
|
2779
|
+
}
|
|
2780
|
+
if (d.progress) _renderMicrosoftProgress(d.progress);
|
|
2781
|
+
if (d.ok) {
|
|
2782
|
+
var display = d.account && d.account.display ? d.account.display : '';
|
|
2783
|
+
if (!opts.silent) _setMicrosoftActionStatus(display ? 'Dev Tunnels signed in as ' + display + '.' : 'Dev Tunnels sign-in verified.', 'ok');
|
|
2784
|
+
return d;
|
|
2785
|
+
}
|
|
2786
|
+
if (!opts.silent) _setMicrosoftActionStatus(d.error || 'Dev Tunnels is not signed in yet. Finish the browser sign-in, then check again.', 'warning');
|
|
2787
|
+
return d;
|
|
2788
|
+
} catch (e) {
|
|
2789
|
+
if (!opts.silent) {
|
|
2790
|
+
if (err) { err.textContent = e.message; err.style.display = 'block'; }
|
|
2791
|
+
_setMicrosoftActionStatus(e.message || 'Could not check Dev Tunnels sign-in', 'error');
|
|
2792
|
+
}
|
|
2793
|
+
return null;
|
|
2794
|
+
} finally {
|
|
2795
|
+
_microsoftLoginCheckInFlight = false;
|
|
2796
|
+
if (btn) { btn.disabled = false; btn.textContent = 'Check Sign-In'; }
|
|
2797
|
+
}
|
|
2798
|
+
}
|
|
2799
|
+
|
|
2800
|
+
async function regenerateMicrosoftLoginCode() {
|
|
2801
|
+
var err = document.getElementById('setup-network-err');
|
|
2802
|
+
var btn = document.getElementById('setup-ms-login-regenerate');
|
|
2803
|
+
if (err) err.style.display = 'none';
|
|
2804
|
+
var original = btn ? btn.textContent : 'New Code';
|
|
2805
|
+
if (btn) { btn.disabled = true; btn.textContent = 'Starting...'; }
|
|
2806
|
+
try {
|
|
2807
|
+
await _postMicrosoftTunnelLogin(true, { forceNew: true });
|
|
2808
|
+
_setMicrosoftActionStatus('New GitHub sign-in code generated. Use the newest code shown below.', 'ok');
|
|
2809
|
+
setupToast('New Microsoft tunnel sign-in code generated');
|
|
2810
|
+
} catch (e) {
|
|
2811
|
+
if (err) { err.textContent = e.message; err.style.display = 'block'; }
|
|
2812
|
+
_setMicrosoftActionStatus(e.message || 'Could not generate a new sign-in code', 'error');
|
|
2813
|
+
} finally {
|
|
2814
|
+
if (btn) { btn.disabled = false; btn.textContent = original; }
|
|
2815
|
+
}
|
|
2816
|
+
}
|
|
2817
|
+
|
|
2433
2818
|
async function _loadMicrosoftProgress() {
|
|
2434
2819
|
try {
|
|
2435
2820
|
var r = await fetch('/api/setup/network/microsoft-dev-tunnel/progress');
|
|
2436
2821
|
var d = await r.json();
|
|
2437
2822
|
if (!r.ok || !d.ok) throw new Error(d.error || 'Microsoft tunnel progress unavailable');
|
|
2438
2823
|
_renderMicrosoftProgress(d.progress || {});
|
|
2824
|
+
var login = d.progress && d.progress.login || {};
|
|
2825
|
+
var ms = _lastNetworkSettings && _lastNetworkSettings.microsoft_dev_tunnel || {};
|
|
2826
|
+
var checkedAt = Date.parse(login.login_checked_at || '');
|
|
2827
|
+
var shouldCheckLogin = login.configured
|
|
2828
|
+
&& !login.running
|
|
2829
|
+
&& !!login.finished_at
|
|
2830
|
+
&& login.exit_code === 0
|
|
2831
|
+
&& !login.signed_in
|
|
2832
|
+
&& !ms.signed_in
|
|
2833
|
+
&& !_microsoftLoginCheckInFlight
|
|
2834
|
+
&& (!Number.isFinite(checkedAt) || Date.now() - checkedAt > 5000);
|
|
2835
|
+
if (shouldCheckLogin) {
|
|
2836
|
+
checkMicrosoftTunnelLogin({ silent: true }).catch(function() {});
|
|
2837
|
+
}
|
|
2439
2838
|
return d.progress || {};
|
|
2440
2839
|
} catch (e) {
|
|
2441
2840
|
_setMicrosoftActionStatus(e.message || 'Microsoft tunnel progress unavailable', 'error');
|
|
@@ -2482,18 +2881,22 @@ function _renderMicrosoftTunnelTraffic(traffic) {
|
|
|
2482
2881
|
}).join('');
|
|
2483
2882
|
}
|
|
2484
2883
|
|
|
2485
|
-
function _microsoftProbeTone(probe) {
|
|
2884
|
+
function _microsoftProbeTone(probe, ms) {
|
|
2486
2885
|
if (!probe || !probe.checked) return '';
|
|
2487
2886
|
if (probe.ctm_reachable) return 'status-ok';
|
|
2488
|
-
if (probe.blocked_by_tunnel_auth || probe.diagnosis === 'tunnel_auth_required')
|
|
2887
|
+
if (probe.blocked_by_tunnel_auth || probe.diagnosis === 'tunnel_auth_required') {
|
|
2888
|
+
return _microsoftTunnelUsesCtmAuth(ms) ? 'status-error' : 'status-ok';
|
|
2889
|
+
}
|
|
2489
2890
|
return 'status-error';
|
|
2490
2891
|
}
|
|
2491
2892
|
|
|
2492
2893
|
function _microsoftProbeStatusText(probe, ms) {
|
|
2493
|
-
if (_microsoftProbeInFlight) return 'Checking the
|
|
2894
|
+
if (_microsoftProbeInFlight) return 'Checking the phone URL...';
|
|
2494
2895
|
if (!probe || !probe.checked) return 'Run this when the phone cannot load the tunnel URL.';
|
|
2495
2896
|
if (probe.ctm_reachable) return 'CTM is reachable through the tunnel from this Mac.';
|
|
2496
|
-
if (probe.blocked_by_tunnel_auth || probe.diagnosis === 'tunnel_auth_required')
|
|
2897
|
+
if (probe.blocked_by_tunnel_auth || probe.diagnosis === 'tunnel_auth_required') {
|
|
2898
|
+
return _microsoftTunnelUsesCtmAuth(ms) ? 'Unexpected Microsoft sign-in gate is blocking CTM.' : 'Private Microsoft sign-in gate is active.';
|
|
2899
|
+
}
|
|
2497
2900
|
if (probe.diagnosis === 'timeout') return 'The tunnel URL timed out before CTM responded.';
|
|
2498
2901
|
if (probe.diagnosis === 'not_configured') return 'Start the tunnel before checking the phone URL.';
|
|
2499
2902
|
return probe.status ? ('Tunnel check returned HTTP ' + probe.status + '.') : 'The tunnel URL is not reachable from this Mac.';
|
|
@@ -2502,13 +2905,17 @@ function _microsoftProbeStatusText(probe, ms) {
|
|
|
2502
2905
|
function _microsoftProbeNoteText(probe, ms) {
|
|
2503
2906
|
if (_microsoftProbeInFlight) return 'CTM is checking the same devtunnels.ms URL your phone opens.';
|
|
2504
2907
|
if (!probe || !probe.checked) {
|
|
2505
|
-
return
|
|
2908
|
+
return _microsoftTunnelUsesCtmAuth(ms)
|
|
2909
|
+
? 'CTM-authenticated mode should reach CTM without a Microsoft/GitHub browser challenge, then CTM asks the phone to pair or sign in.'
|
|
2910
|
+
: 'Private Microsoft mode may show the provider sign-in gate before CTM. After that, CTM asks the phone to pair or sign in with its CTM device token.';
|
|
2506
2911
|
}
|
|
2507
2912
|
if (probe.ctm_reachable) {
|
|
2508
2913
|
return 'The Mac-side check reached CTM. If the phone still fails, refresh the phone page and check the CTM traffic list below.';
|
|
2509
2914
|
}
|
|
2510
2915
|
if (probe.blocked_by_tunnel_auth || probe.diagnosis === 'tunnel_auth_required') {
|
|
2511
|
-
return
|
|
2916
|
+
return _microsoftTunnelUsesCtmAuth(ms)
|
|
2917
|
+
? 'This is not expected for CTM-authenticated mode. Apply the selected mode, then check the URL again.'
|
|
2918
|
+
: 'This is expected for private Microsoft Dev Tunnel access. Complete Microsoft/GitHub sign-in in the phone browser, then CTM will enforce pairing and device permissions.';
|
|
2512
2919
|
}
|
|
2513
2920
|
return probe.message || 'Use Recover Now if the tunnel process should be running, then check the URL again.';
|
|
2514
2921
|
}
|
|
@@ -2521,7 +2928,7 @@ function _renderMicrosoftTunnelProbe(probe, ms, ready) {
|
|
|
2521
2928
|
if (!panel) return;
|
|
2522
2929
|
panel.style.display = ready ? '' : 'none';
|
|
2523
2930
|
panel.classList.remove('status-ok', 'status-warn', 'status-error');
|
|
2524
|
-
var tone = _microsoftProbeTone(probe);
|
|
2931
|
+
var tone = _microsoftProbeTone(probe, ms);
|
|
2525
2932
|
if (tone) panel.classList.add(tone);
|
|
2526
2933
|
if (status) status.textContent = _microsoftProbeStatusText(probe, ms);
|
|
2527
2934
|
if (note) note.textContent = _microsoftProbeNoteText(probe, ms);
|
|
@@ -2531,6 +2938,19 @@ function _renderMicrosoftTunnelProbe(probe, ms, ready) {
|
|
|
2531
2938
|
}
|
|
2532
2939
|
}
|
|
2533
2940
|
|
|
2941
|
+
function _renderMicrosoftPrivateAccessWarning(ms) {
|
|
2942
|
+
var warning = document.getElementById('setup-ms-private-warning');
|
|
2943
|
+
var resetBtn = document.getElementById('setup-ms-reset-private');
|
|
2944
|
+
if (!warning) return false;
|
|
2945
|
+
var needsReset = _microsoftNeedsPrivateAccessReset(ms);
|
|
2946
|
+
warning.style.display = needsReset ? '' : 'none';
|
|
2947
|
+
if (resetBtn) {
|
|
2948
|
+
resetBtn.disabled = !needsReset || _microsoftSetupInFlight;
|
|
2949
|
+
resetBtn.textContent = _microsoftSetupInFlight ? 'Working...' : 'Reset to private';
|
|
2950
|
+
}
|
|
2951
|
+
return needsReset;
|
|
2952
|
+
}
|
|
2953
|
+
|
|
2534
2954
|
function _renderMicrosoftDevTunnelSetup(ms, d) {
|
|
2535
2955
|
ms = ms || {};
|
|
2536
2956
|
var managed = ms.managed_tunnel || {};
|
|
@@ -2544,6 +2964,7 @@ function _renderMicrosoftDevTunnelSetup(ms, d) {
|
|
|
2544
2964
|
var mobileUrl = document.getElementById('setup-ms-mobile-url');
|
|
2545
2965
|
var inspectUrl = document.getElementById('setup-ms-inspect-url');
|
|
2546
2966
|
var setupBtn = document.getElementById('setup-ms-setup');
|
|
2967
|
+
var switchLoginBtn = document.getElementById('setup-ms-switch-login');
|
|
2547
2968
|
var stopBtn = document.getElementById('setup-ms-stop');
|
|
2548
2969
|
var installCommand = document.getElementById('setup-ms-install-command');
|
|
2549
2970
|
var loginCommand = document.getElementById('setup-ms-login-command');
|
|
@@ -2560,16 +2981,45 @@ function _renderMicrosoftDevTunnelSetup(ms, d) {
|
|
|
2560
2981
|
var inspect = managed.inspect_url || ms.inspect_url || '';
|
|
2561
2982
|
var accountText = _microsoftTunnelAccountText(ms);
|
|
2562
2983
|
var availability = _microsoftAvailabilityCopy(ms, ready);
|
|
2984
|
+
var needsPrivateReset = _microsoftNeedsPrivateAccessReset(ms);
|
|
2985
|
+
var accessMode = _microsoftTunnelSelectedAccessMode(ms);
|
|
2986
|
+
var currentAccessMode = _microsoftTunnelCurrentAccessMode(ms);
|
|
2987
|
+
var accessModeMatches = currentAccessMode === accessMode;
|
|
2988
|
+
var ctmAuthenticated = accessMode === 'ctm_authenticated';
|
|
2989
|
+
var ctmModeBtn = document.getElementById('setup-ms-mode-ctm');
|
|
2990
|
+
var privateModeBtn = document.getElementById('setup-ms-mode-private');
|
|
2991
|
+
var providerLabel = _msProviderLabel();
|
|
2992
|
+
_renderMicrosoftPrivateAccessWarning(ms);
|
|
2993
|
+
_renderMicrosoftLoginProvider();
|
|
2994
|
+
|
|
2995
|
+
if (ctmModeBtn) {
|
|
2996
|
+
ctmModeBtn.setAttribute('aria-pressed', ctmAuthenticated ? 'true' : 'false');
|
|
2997
|
+
ctmModeBtn.disabled = _microsoftSetupInFlight;
|
|
2998
|
+
}
|
|
2999
|
+
if (privateModeBtn) {
|
|
3000
|
+
privateModeBtn.setAttribute('aria-pressed', !ctmAuthenticated ? 'true' : 'false');
|
|
3001
|
+
privateModeBtn.disabled = _microsoftSetupInFlight;
|
|
3002
|
+
}
|
|
2563
3003
|
|
|
2564
3004
|
if (summary) {
|
|
2565
3005
|
if (!installed) {
|
|
2566
3006
|
summary.textContent = 'CTM can install Microsoft Dev Tunnels, open sign-in on this Mac, then start a browser URL for your phone.';
|
|
2567
3007
|
} else if (!signedIn) {
|
|
2568
|
-
summary.textContent = 'Sign in once with
|
|
3008
|
+
summary.textContent = 'Sign in once with ' + providerLabel + ' on this Mac. After sign-in finishes, CTM starts the tunnel and creates the phone pairing QR.';
|
|
3009
|
+
} else if (needsPrivateReset) {
|
|
3010
|
+
summary.textContent = ctmAuthenticated
|
|
3011
|
+
? 'Microsoft tunnel is ready for CTM-authenticated phone access. CTM will apply this mode before sharing the URL.'
|
|
3012
|
+
: 'Microsoft tunnel is running, but anonymous connect access is still enabled from older setup. Reset to private before sharing the URL.';
|
|
3013
|
+
} else if (ready && accessModeMatches) {
|
|
3014
|
+
summary.textContent = ctmAuthenticated
|
|
3015
|
+
? 'Microsoft tunnel is running in CTM-authenticated mode. Open the phone URL and CTM will handle pairing and sign-in.'
|
|
3016
|
+
: 'Microsoft tunnel is running privately. Open the phone URL, pass Microsoft/GitHub tunnel auth, then CTM will handle pairing and sign-in.';
|
|
2569
3017
|
} else if (ready) {
|
|
2570
|
-
summary.textContent =
|
|
3018
|
+
summary.textContent = ctmAuthenticated
|
|
3019
|
+
? 'Microsoft tunnel is running with the private Microsoft gate. Apply CTM-authenticated mode so the phone can reach CTM directly.'
|
|
3020
|
+
: 'Microsoft tunnel is running with CTM-authenticated access. Apply private Microsoft gate if you want provider auth before CTM.';
|
|
2571
3021
|
} else {
|
|
2572
|
-
summary.textContent = 'CTM can start the
|
|
3022
|
+
summary.textContent = 'CTM can start the tunnel, make the URL reachable by the phone, and create the phone pairing QR.';
|
|
2573
3023
|
}
|
|
2574
3024
|
}
|
|
2575
3025
|
if (account) {
|
|
@@ -2580,8 +3030,12 @@ function _renderMicrosoftDevTunnelSetup(ms, d) {
|
|
|
2580
3030
|
if (inspectUrl) inspectUrl.textContent = inspect || 'Start tunnel first';
|
|
2581
3031
|
if (readyLinks) readyLinks.style.display = ready ? '' : 'none';
|
|
2582
3032
|
if (panelStatus) {
|
|
2583
|
-
panelStatus.textContent =
|
|
3033
|
+
panelStatus.textContent = needsPrivateReset
|
|
3034
|
+
? 'Reset to private'
|
|
3035
|
+
: ready && accessModeMatches
|
|
2584
3036
|
? (keepAwake.enabled && keepAwake.running ? 'Ready, kept awake' : 'Ready')
|
|
3037
|
+
: ready
|
|
3038
|
+
? 'Apply mode'
|
|
2585
3039
|
: (!installed ? 'Needs setup' : (!signedIn ? 'Sign in required' : 'Ready to start'));
|
|
2586
3040
|
}
|
|
2587
3041
|
if (availabilityStatus) {
|
|
@@ -2590,9 +3044,11 @@ function _renderMicrosoftDevTunnelSetup(ms, d) {
|
|
|
2590
3044
|
}
|
|
2591
3045
|
if (availabilityNote) {
|
|
2592
3046
|
var lastRecovered = managed.last_watchdog_recovered_at ? new Date(managed.last_watchdog_recovered_at).toLocaleString() : '';
|
|
2593
|
-
|
|
3047
|
+
var baseNote = keepAwake.enabled
|
|
2594
3048
|
? (keepAwake.running ? 'macOS sleep prevention is active while CTM is running.' : 'CTM will try to restart the keep-awake assertion automatically.')
|
|
2595
3049
|
: (lastRecovered ? 'Last tunnel recovery: ' + lastRecovered : 'Default mode recovers after wake, but it cannot serve the phone while the Mac is asleep.');
|
|
3050
|
+
var lastEvent = _msLastHostEventText(managed.host_events);
|
|
3051
|
+
availabilityNote.textContent = lastEvent ? baseNote + ' ' + lastEvent : baseNote;
|
|
2596
3052
|
}
|
|
2597
3053
|
if (keepAwakeInput) {
|
|
2598
3054
|
keepAwakeInput.checked = !!keepAwake.enabled;
|
|
@@ -2612,21 +3068,31 @@ function _renderMicrosoftDevTunnelSetup(ms, d) {
|
|
|
2612
3068
|
if (runCommand) runCommand.textContent = managed.run_command || (commands.host_temporary && commands.host_temporary.display) || '';
|
|
2613
3069
|
_setMicrosoftStep('setup-ms-step-install', installed ? 'done' : 'active');
|
|
2614
3070
|
_setMicrosoftStep('setup-ms-step-login', signedIn ? 'done' : (installed ? 'active' : 'pending'));
|
|
2615
|
-
_setMicrosoftStep('setup-ms-step-run', ready ? 'done' : (installed && signedIn ? 'active' : 'pending'));
|
|
3071
|
+
_setMicrosoftStep('setup-ms-step-run', ready && accessModeMatches ? 'done' : (installed && signedIn ? 'active' : 'pending'));
|
|
2616
3072
|
if (setupBtn) {
|
|
2617
|
-
setupBtn.disabled = ready || _microsoftSetupInFlight;
|
|
2618
|
-
setupBtn.textContent = ready
|
|
3073
|
+
setupBtn.disabled = (ready && accessModeMatches) || _microsoftSetupInFlight;
|
|
3074
|
+
setupBtn.textContent = ready && accessModeMatches
|
|
2619
3075
|
? 'Ready'
|
|
2620
|
-
: (_microsoftSetupInFlight ? 'Working...' : (!installed ? 'Set Up' : (!signedIn ? 'Open Sign-In' : 'Start Tunnel')));
|
|
2621
|
-
setupBtn.title = ready ? 'Microsoft tunnel phone access is ready to use.' : '';
|
|
3076
|
+
: (_microsoftSetupInFlight ? 'Working...' : (!installed ? 'Set Up' : (!signedIn ? 'Open Sign-In' : (ready ? 'Apply Mode' : 'Start Tunnel'))));
|
|
3077
|
+
setupBtn.title = ready && accessModeMatches ? 'Microsoft tunnel phone access is ready to use.' : '';
|
|
3078
|
+
}
|
|
3079
|
+
if (switchLoginBtn) {
|
|
3080
|
+
switchLoginBtn.style.display = installed && signedIn ? '' : 'none';
|
|
3081
|
+
switchLoginBtn.disabled = _microsoftSetupInFlight;
|
|
3082
|
+
switchLoginBtn.textContent = 'Use Different Account';
|
|
3083
|
+
switchLoginBtn.title = 'Sign out of the current Dev Tunnels account and start ' + providerLabel + ' device sign-in.';
|
|
2622
3084
|
}
|
|
2623
3085
|
if (stopBtn) {
|
|
2624
3086
|
stopBtn.disabled = !ready;
|
|
2625
3087
|
}
|
|
2626
3088
|
if (securityNote) {
|
|
2627
|
-
securityNote.textContent =
|
|
2628
|
-
? '
|
|
2629
|
-
:
|
|
3089
|
+
securityNote.textContent = needsPrivateReset
|
|
3090
|
+
? 'Anonymous Dev Tunnel connect access is detected. Reset to private before sharing the desktop or phone URL.'
|
|
3091
|
+
: (ctmAuthenticated
|
|
3092
|
+
? 'CTM-authenticated mode creates a connect-only anonymous Dev Tunnel rule so the browser can reach CTM. CTM still requires pairing, device token, route permissions, and passkey step-up.'
|
|
3093
|
+
: (ready
|
|
3094
|
+
? 'Private Microsoft tunnel is running. Remote browsers pass Microsoft/GitHub tunnel auth first; CTM still requires pairing, device token, route permissions, and passkey step-up.'
|
|
3095
|
+
: 'Private Microsoft gate creates no anonymous tunnel access; remote browsers must pass Microsoft/GitHub auth before CTM loads.'));
|
|
2630
3096
|
}
|
|
2631
3097
|
_renderMicrosoftTunnelTraffic(ms.traffic || {});
|
|
2632
3098
|
_renderMicrosoftTunnelProbe(_microsoftTunnelProbe || ms.public_probe || null, ms, ready);
|
|
@@ -2646,30 +3112,31 @@ function _renderAccessMethodSelection(d) {
|
|
|
2646
3112
|
var cfPossible = _cloudflareAutoPossible(cfAuto);
|
|
2647
3113
|
var msReady = _microsoftTunnelReady(d);
|
|
2648
3114
|
var msCanStart = _microsoftTunnelCanStart(d);
|
|
3115
|
+
var msNeedsPrivateReset = _microsoftNeedsPrivateAccessReset(ms);
|
|
3116
|
+
var msModeMismatch = msReady && !_microsoftAccessModeMatchesSelection(ms);
|
|
2649
3117
|
_setMethodButton('setup-method-microsoft', method === 'microsoft');
|
|
2650
3118
|
_setMethodButton('setup-method-walle', method === 'walle');
|
|
2651
3119
|
_setMethodButton('setup-method-tailscale', method === 'tailscale');
|
|
2652
3120
|
_setMethodButton('setup-method-cloudflare', method === 'cloudflare');
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
3121
|
+
// Live connection-health overlays the setup-state badge when we have a health record
|
|
3122
|
+
// (Ready / Attention / Offline–Reconnect / Expired–Reconnect / Error); not_configured
|
|
3123
|
+
// falls back to the setup wizard state (Install / Sign in / Start / Set up).
|
|
3124
|
+
_applyHealthBadge('microsoft', 'setup-method-microsoft-badge',
|
|
3125
|
+
msNeedsPrivateReset ? 'Reset' : (msModeMismatch ? 'Apply' : (msReady ? 'Ready' : (msCanStart ? 'Start' : (ms.installed ? 'Sign in' : 'Install')))),
|
|
3126
|
+
msNeedsPrivateReset ? 'warn' : (msModeMismatch ? 'warn' : (msReady ? 'ok' : (msCanStart ? 'info' : (ms.installed ? 'warn' : 'missing')))));
|
|
3127
|
+
_applyHealthBadge('walle', 'setup-method-walle-badge',
|
|
2660
3128
|
_walleRemoteHostedPairingReady(d) ? 'Ready' : (walleReady ? 'Relay pending' : 'Set up'),
|
|
2661
|
-
_walleRemoteHostedPairingReady(d) ? 'ok' : (walleReady ? 'warn' : 'info')
|
|
2662
|
-
|
|
2663
|
-
_setMethodBadge(
|
|
2664
|
-
'setup-method-tailscale-badge',
|
|
3129
|
+
_walleRemoteHostedPairingReady(d) ? 'ok' : (walleReady ? 'warn' : 'info'));
|
|
3130
|
+
_applyHealthBadge('tailscale', 'setup-method-tailscale-badge',
|
|
2665
3131
|
tsReady ? 'Ready' : (ts.available ? 'Auto setup' : 'Not detected'),
|
|
2666
|
-
tsReady ? 'ok' : (ts.available ? 'info' : 'missing')
|
|
2667
|
-
);
|
|
3132
|
+
tsReady ? 'ok' : (ts.available ? 'info' : 'missing'));
|
|
2668
3133
|
_setMethodBadge(
|
|
2669
3134
|
'setup-method-cloudflare-badge',
|
|
2670
3135
|
cfReady ? 'Ready' : (cfPossible ? 'API setup' : 'Install'),
|
|
2671
3136
|
cfReady ? 'ok' : (cfPossible ? 'info' : 'warn')
|
|
2672
3137
|
);
|
|
3138
|
+
// IA: foreground the selected method; collapse the rest under "Other ways to connect".
|
|
3139
|
+
_applyRecommendedLayout(method);
|
|
2673
3140
|
|
|
2674
3141
|
var msPanel = document.getElementById('setup-microsoft-details');
|
|
2675
3142
|
var wallePanel = document.getElementById('setup-walle-remote-details');
|
|
@@ -2700,6 +3167,13 @@ function _renderAccessMethodSelection(d) {
|
|
|
2700
3167
|
? 'Ready on ' + (cfOrigin || managed.hostname || 'the Cloudflare URL')
|
|
2701
3168
|
: (cfPossible ? 'API setup can create Access, DNS, tunnel, and phone QR. No cloudflared login cert is required.' : 'Install cloudflared before auto setup.');
|
|
2702
3169
|
}
|
|
3170
|
+
// When a method is live-broken, its panel status line shows the health detail (so the
|
|
3171
|
+
// single status truth is consistent with the badge + alert banner).
|
|
3172
|
+
[['microsoft', 'setup-ms-panel-status'], ['walle', 'setup-walle-remote-panel-status'], ['tailscale', 'setup-tailscale-panel-status']].forEach(function(pair) {
|
|
3173
|
+
var rec = _healthByMethod[pair[0]];
|
|
3174
|
+
var el = document.getElementById(pair[1]);
|
|
3175
|
+
if (el && rec && _isBrokenHealth(rec.state)) el.textContent = rec.detail;
|
|
3176
|
+
});
|
|
2703
3177
|
}
|
|
2704
3178
|
|
|
2705
3179
|
function selectPhoneAccessMethod(method) {
|
|
@@ -2710,6 +3184,130 @@ function selectPhoneAccessMethod(method) {
|
|
|
2710
3184
|
_applySelectedPhoneOrigin(d, true);
|
|
2711
3185
|
}
|
|
2712
3186
|
|
|
3187
|
+
// ---- Connection health: live status + expiry + Reconnect + auto-recovery -------------
|
|
3188
|
+
function _methodDisplayName(method) {
|
|
3189
|
+
return method === 'microsoft' ? 'Microsoft tunnel'
|
|
3190
|
+
: method === 'walle' ? 'Walle Remote'
|
|
3191
|
+
: method === 'tailscale' ? 'Tailscale'
|
|
3192
|
+
: method === 'cloudflare' ? 'Cloudflare Access' : String(method || '');
|
|
3193
|
+
}
|
|
3194
|
+
|
|
3195
|
+
function _isBrokenHealth(state) { return state === 'offline' || state === 'expired' || state === 'error'; }
|
|
3196
|
+
|
|
3197
|
+
function _healthBadgeForState(state) {
|
|
3198
|
+
switch (state) {
|
|
3199
|
+
case 'ok': return { text: 'Ready', tone: 'ok' };
|
|
3200
|
+
case 'warning': return { text: 'Attention', tone: 'warn' };
|
|
3201
|
+
case 'offline': return { text: 'Offline — Reconnect', tone: 'error' };
|
|
3202
|
+
case 'expired': return { text: 'Expired — Reconnect', tone: 'error' };
|
|
3203
|
+
case 'error': return { text: 'Error', tone: 'error' };
|
|
3204
|
+
default: return null; // not_configured → fall back to the setup-state badge
|
|
3205
|
+
}
|
|
3206
|
+
}
|
|
3207
|
+
|
|
3208
|
+
// Use the live health badge when we have one; otherwise the setup-state fallback.
|
|
3209
|
+
function _applyHealthBadge(method, badgeId, fallbackText, fallbackTone) {
|
|
3210
|
+
var rec = _healthByMethod[method];
|
|
3211
|
+
var hb = rec ? _healthBadgeForState(rec.state) : null;
|
|
3212
|
+
if (hb) _setMethodBadge(badgeId, hb.text, hb.tone);
|
|
3213
|
+
else _setMethodBadge(badgeId, fallbackText, fallbackTone);
|
|
3214
|
+
}
|
|
3215
|
+
|
|
3216
|
+
async function loadConnectionHealth() {
|
|
3217
|
+
try {
|
|
3218
|
+
var r = await fetch('/api/setup/connection-health');
|
|
3219
|
+
var d = await r.json();
|
|
3220
|
+
if (!r.ok || !d || d.ok === false) return;
|
|
3221
|
+
_lastConnectionHealth = d;
|
|
3222
|
+
_healthByMethod = {};
|
|
3223
|
+
(d.methods || []).forEach(function(m) { _healthByMethod[m.method] = m; });
|
|
3224
|
+
_renderConnectionHealth(d);
|
|
3225
|
+
} catch (e) { /* keep last rendered state; retry next tick */ }
|
|
3226
|
+
}
|
|
3227
|
+
|
|
3228
|
+
function _scheduleConnectionHealthPoll() {
|
|
3229
|
+
clearTimeout(_connectionHealthTimer);
|
|
3230
|
+
_connectionHealthTimer = setTimeout(function() {
|
|
3231
|
+
var section = document.getElementById('setup-section-access');
|
|
3232
|
+
var visible = section && section.offsetParent !== null;
|
|
3233
|
+
if (visible) loadConnectionHealth();
|
|
3234
|
+
_scheduleConnectionHealthPoll();
|
|
3235
|
+
}, 20000);
|
|
3236
|
+
}
|
|
3237
|
+
|
|
3238
|
+
// Re-fetch health the moment the user returns to the tab/window, so a connection they
|
|
3239
|
+
// fixed externally auto-recovers immediately (not just on the next 20s poll).
|
|
3240
|
+
function _refreshAccessHealthOnReturn() {
|
|
3241
|
+
var section = document.getElementById('setup-section-access');
|
|
3242
|
+
if (section && section.offsetParent !== null) loadConnectionHealth();
|
|
3243
|
+
}
|
|
3244
|
+
|
|
3245
|
+
function _renderConnectionHealth(health) {
|
|
3246
|
+
// Re-render badges/status with the health overlay (uses _healthByMethod), then the
|
|
3247
|
+
// Reconnect banner and any transition notifications.
|
|
3248
|
+
_renderAccessMethodSelection(_lastNetworkSettings || {});
|
|
3249
|
+
_renderAccessHealthAlert((health && health.methods) || []);
|
|
3250
|
+
((health && health.transitions) || []).forEach(function(t) {
|
|
3251
|
+
var rec = _healthByMethod[t.method] || {};
|
|
3252
|
+
var verb = rec.state === 'expired' ? 'expired' : (rec.state === 'error' ? 'has an error' : 'is offline');
|
|
3253
|
+
_notifyConnectionBroken(t.method, _methodDisplayName(t.method) + ' ' + verb, rec.detail || '');
|
|
3254
|
+
});
|
|
3255
|
+
}
|
|
3256
|
+
|
|
3257
|
+
function _renderAccessHealthAlert(records) {
|
|
3258
|
+
var el = document.getElementById('setup-access-alert');
|
|
3259
|
+
if (!el) return;
|
|
3260
|
+
var broken = (records || []).filter(function(r) { return _isBrokenHealth(r.state); });
|
|
3261
|
+
if (!broken.length) { el.style.display = 'none'; el.innerHTML = ''; return; }
|
|
3262
|
+
var KNOWN = { microsoft: 1, walle: 1, tailscale: 1, cloudflare: 1 };
|
|
3263
|
+
el.innerHTML = broken.map(function(r) {
|
|
3264
|
+
var method = KNOWN[r.method] ? r.method : ''; // whitelist before it enters the onclick
|
|
3265
|
+
var name = _escHtml(_methodDisplayName(r.method));
|
|
3266
|
+
var detail = _escHtml(r.detail || (r.state === 'expired' ? 'Expired — reconnect.' : 'Offline — reconnect.'));
|
|
3267
|
+
var btn = (r.action && method)
|
|
3268
|
+
? '<button type="button" class="btn small" onclick="SETUP.triggerReconnect(\'' + method + '\')">Reconnect</button>'
|
|
3269
|
+
: '';
|
|
3270
|
+
return '<div class="setup-access-alert-row"><span>⚠ <strong>' + name + '</strong> — ' + detail + '</span>' + btn + '</div>';
|
|
3271
|
+
}).join('');
|
|
3272
|
+
el.style.display = '';
|
|
3273
|
+
}
|
|
3274
|
+
|
|
3275
|
+
function _notifyConnectionBroken(method, title, detail) {
|
|
3276
|
+
try { if (typeof showToast === 'function') showToast(title + ' — click to reconnect', 'var(--red, #e5484d)', 9000, function() { triggerReconnect(method); }); } catch (e) {}
|
|
3277
|
+
try {
|
|
3278
|
+
if (typeof Notification !== 'undefined' && Notification.permission === 'granted' && document.hidden) {
|
|
3279
|
+
new Notification('Phone access offline', { body: title + (detail ? ' — ' + detail : '') });
|
|
3280
|
+
}
|
|
3281
|
+
} catch (e) {}
|
|
3282
|
+
}
|
|
3283
|
+
|
|
3284
|
+
async function triggerReconnect(method) {
|
|
3285
|
+
var rec = _healthByMethod[method];
|
|
3286
|
+
if (!rec || !rec.action || !rec.action.endpoint) { loadConnectionHealth(); return; }
|
|
3287
|
+
try {
|
|
3288
|
+
var verb = rec.action.method || 'POST';
|
|
3289
|
+
var opts = { method: verb };
|
|
3290
|
+
if (verb !== 'GET') { opts.headers = { 'Content-Type': 'application/json' }; opts.body = '{}'; }
|
|
3291
|
+
await fetch(rec.action.endpoint, opts);
|
|
3292
|
+
} catch (e) {}
|
|
3293
|
+
setTimeout(loadConnectionHealth, 800); // re-probe so the badge/banner auto-recover
|
|
3294
|
+
}
|
|
3295
|
+
|
|
3296
|
+
// Phase 3 IA: foreground the (selected/recommended) method; collapse the rest under
|
|
3297
|
+
// "Other ways to connect". Idempotent — reparents a button only when it isn't already
|
|
3298
|
+
// in the right container (no flicker on re-render).
|
|
3299
|
+
function _applyRecommendedLayout(foreground) {
|
|
3300
|
+
var primary = document.getElementById('setup-access-primary');
|
|
3301
|
+
var others = document.getElementById('setup-access-others-list');
|
|
3302
|
+
if (!primary || !others) return;
|
|
3303
|
+
['microsoft', 'walle', 'tailscale'].forEach(function(m) {
|
|
3304
|
+
var btn = document.getElementById('setup-method-' + m);
|
|
3305
|
+
if (!btn) return;
|
|
3306
|
+
var target = (m === foreground) ? primary : others;
|
|
3307
|
+
if (btn.parentNode !== target) target.appendChild(btn);
|
|
3308
|
+
});
|
|
3309
|
+
}
|
|
3310
|
+
|
|
2713
3311
|
function _renderWalleRemoteDevices(devices) {
|
|
2714
3312
|
var list = document.getElementById('setup-walle-remote-devices');
|
|
2715
3313
|
if (!list) return;
|
|
@@ -2751,9 +3349,9 @@ function _renderWalleRemoteSetup(remote) {
|
|
|
2751
3349
|
if (hostedReady) {
|
|
2752
3350
|
summary.textContent = 'This Mac is connected to hosted Walle Relay. Pair a phone from Walle mobile to continue CTM work without Tailscale.';
|
|
2753
3351
|
} else if (status.ok) {
|
|
2754
|
-
summary.textContent = 'The local CTM relay contract is ready, but hosted Walle Relay pairing is not connected yet. m.walle.sh is not a working CTM phone URL for this install; use
|
|
3352
|
+
summary.textContent = 'The local CTM relay contract is ready, but hosted Walle Relay pairing is not connected yet. m.walle.sh is not a working CTM phone URL for this install; use Microsoft tunnel or Tailscale until hosted relay is enabled.';
|
|
2755
3353
|
} else {
|
|
2756
|
-
summary.textContent = 'Walle Remote status is unavailable.
|
|
3354
|
+
summary.textContent = 'Walle Remote status is unavailable. Microsoft tunnel and Tailscale remain available below.';
|
|
2757
3355
|
}
|
|
2758
3356
|
}
|
|
2759
3357
|
if (panelStatus) {
|
|
@@ -2802,7 +3400,7 @@ async function createWalleRemoteClaim() {
|
|
|
2802
3400
|
_setDeviceError('');
|
|
2803
3401
|
var status = _lastRemoteStatus || await loadWalleRemoteStatus();
|
|
2804
3402
|
if (!_walleRemoteHostedPairingReady({ remote_relay: status })) {
|
|
2805
|
-
_setDeviceError('Walle Remote hosted pairing is not connected yet, so CTM will not generate a
|
|
3403
|
+
_setDeviceError('Walle Remote hosted pairing is not connected yet, so CTM will not generate a Walle Remote pairing QR. Use Microsoft tunnel or Tailscale until hosted Walle Relay is enabled.');
|
|
2806
3404
|
_renderWalleRemoteSetup(status);
|
|
2807
3405
|
return null;
|
|
2808
3406
|
}
|
|
@@ -2810,6 +3408,9 @@ async function createWalleRemoteClaim() {
|
|
|
2810
3408
|
await _persistDeviceLabel(label, { immediate: true }).catch(function(e) {
|
|
2811
3409
|
_setDeviceError(e.message || 'Phone label save failed');
|
|
2812
3410
|
});
|
|
3411
|
+
await _persistDeviceScopes(_selectedDeviceScopes(), { immediate: true }).catch(function(e) {
|
|
3412
|
+
_setDeviceError(e.message || 'Phone permission save failed');
|
|
3413
|
+
});
|
|
2813
3414
|
var origin = _preferredPhoneOrigin({ remote_relay: status }, 'walle');
|
|
2814
3415
|
var btn = document.getElementById('setup-walle-remote-pair');
|
|
2815
3416
|
var original = btn ? btn.textContent : 'Pair Phone';
|
|
@@ -3143,6 +3744,14 @@ async function loadNetworkSettings() {
|
|
|
3143
3744
|
} catch (e) {
|
|
3144
3745
|
if (err) { err.textContent = e.message; err.style.display = 'block'; }
|
|
3145
3746
|
_setNetworkStatus('Network settings unavailable', false);
|
|
3747
|
+
// Network settings never loaded — clear the "Resolving…" loading state so
|
|
3748
|
+
// the Pairing Origin field doesn't stay stuck on it. Let the user type one.
|
|
3749
|
+
var bestUrl = document.getElementById('setup-phone-best-url');
|
|
3750
|
+
if (bestUrl && bestUrl.textContent === 'Resolving…') bestUrl.textContent = 'Unavailable';
|
|
3751
|
+
var phoneInput = document.getElementById('setup-device-origin');
|
|
3752
|
+
if (phoneInput && phoneInput.placeholder === 'Resolving phone URL…') {
|
|
3753
|
+
phoneInput.placeholder = 'Enter your phone URL manually';
|
|
3754
|
+
}
|
|
3146
3755
|
return null;
|
|
3147
3756
|
}
|
|
3148
3757
|
}
|
|
@@ -3190,12 +3799,13 @@ async function _postMicrosoftTunnelInstall() {
|
|
|
3190
3799
|
return d;
|
|
3191
3800
|
}
|
|
3192
3801
|
|
|
3193
|
-
async function _postMicrosoftTunnelLogin(deviceCode) {
|
|
3802
|
+
async function _postMicrosoftTunnelLogin(deviceCode, options) {
|
|
3803
|
+
options = options || {};
|
|
3194
3804
|
_startMicrosoftProgressPolling();
|
|
3195
3805
|
var r = await fetch('/api/setup/network/microsoft-dev-tunnel/login', {
|
|
3196
3806
|
method: 'POST',
|
|
3197
3807
|
headers: { 'Content-Type': 'application/json' },
|
|
3198
|
-
body: JSON.stringify({ device_code: deviceCode !== false, provider:
|
|
3808
|
+
body: JSON.stringify({ device_code: deviceCode !== false, provider: _msLoginProvider(), force_new: !!options.forceNew }),
|
|
3199
3809
|
});
|
|
3200
3810
|
var d = await r.json();
|
|
3201
3811
|
if (!r.ok || !d.ok) throw new Error(d.error || 'Could not start devtunnel login');
|
|
@@ -3253,9 +3863,9 @@ async function setupMicrosoftTunnel() {
|
|
|
3253
3863
|
if (err) err.style.display = 'none';
|
|
3254
3864
|
if (btn) { btn.disabled = true; btn.textContent = 'Working...'; }
|
|
3255
3865
|
try {
|
|
3256
|
-
var d =
|
|
3866
|
+
var d = await loadNetworkSettings() || _lastNetworkSettings || {};
|
|
3257
3867
|
var ms = d.microsoft_dev_tunnel || {};
|
|
3258
|
-
if (_microsoftTunnelReady(d)) {
|
|
3868
|
+
if (_microsoftTunnelReady(d) && _microsoftAccessModeMatchesSelection(ms)) {
|
|
3259
3869
|
_setMicrosoftActionStatus('Microsoft tunnel is already ready.', 'ok');
|
|
3260
3870
|
return;
|
|
3261
3871
|
}
|
|
@@ -3270,18 +3880,19 @@ async function setupMicrosoftTunnel() {
|
|
|
3270
3880
|
}
|
|
3271
3881
|
|
|
3272
3882
|
if (!ms.signed_in) {
|
|
3273
|
-
|
|
3883
|
+
var signInLabel = _msProviderLabel();
|
|
3884
|
+
_setMicrosoftActionStatus('Opening ' + signInLabel + ' sign-in. Enter the code shown below on the ' + signInLabel + ' page; no phone notification is sent.', '');
|
|
3274
3885
|
if (btn) btn.textContent = 'Waiting...';
|
|
3275
3886
|
await _postMicrosoftTunnelLogin(true);
|
|
3276
3887
|
d = await _waitForMicrosoftSignIn(90000, 2000);
|
|
3277
3888
|
if (!d) {
|
|
3278
|
-
_setMicrosoftActionStatus('Sign-in is still waiting. Enter the displayed code on the
|
|
3889
|
+
_setMicrosoftActionStatus('Sign-in is still waiting. Enter the displayed code on the ' + signInLabel + ' page, choose the account there, then click Set Up again.', '');
|
|
3279
3890
|
return;
|
|
3280
3891
|
}
|
|
3281
3892
|
ms = d.microsoft_dev_tunnel || {};
|
|
3282
3893
|
}
|
|
3283
3894
|
|
|
3284
|
-
if (!_microsoftTunnelReady(d)) {
|
|
3895
|
+
if (!_microsoftTunnelReady(d) || !_microsoftAccessModeMatchesSelection(ms)) {
|
|
3285
3896
|
await startMicrosoftTunnel();
|
|
3286
3897
|
}
|
|
3287
3898
|
} catch (e) {
|
|
@@ -3302,7 +3913,7 @@ async function startMicrosoftTunnelLogin(deviceCode) {
|
|
|
3302
3913
|
if (btn) { btn.disabled = true; btn.textContent = 'Starting...'; }
|
|
3303
3914
|
try {
|
|
3304
3915
|
await _postMicrosoftTunnelLogin(deviceCode !== false);
|
|
3305
|
-
_setMicrosoftActionStatus('Sign-in started. Enter the displayed code on the
|
|
3916
|
+
_setMicrosoftActionStatus('Sign-in started. Enter the displayed code on the ' + _msProviderLabel() + ' page; CTM will detect the account after Dev Tunnels finishes.', 'ok');
|
|
3306
3917
|
setupToast('Microsoft tunnel login started');
|
|
3307
3918
|
} catch (e) {
|
|
3308
3919
|
if (err) { err.textContent = e.message; err.style.display = 'block'; }
|
|
@@ -3312,6 +3923,39 @@ async function startMicrosoftTunnelLogin(deviceCode) {
|
|
|
3312
3923
|
}
|
|
3313
3924
|
}
|
|
3314
3925
|
|
|
3926
|
+
async function switchMicrosoftTunnelLogin() {
|
|
3927
|
+
var err = document.getElementById('setup-network-err');
|
|
3928
|
+
var btn = document.getElementById('setup-ms-switch-login');
|
|
3929
|
+
if (err) err.style.display = 'none';
|
|
3930
|
+
if (btn) { btn.disabled = true; btn.textContent = 'Signing out...'; }
|
|
3931
|
+
_setMicrosoftActionStatus('Signing out of the current Dev Tunnels account...', '');
|
|
3932
|
+
try {
|
|
3933
|
+
var logoutRes = await fetch('/api/setup/network/microsoft-dev-tunnel/logout', {
|
|
3934
|
+
method: 'POST',
|
|
3935
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3936
|
+
body: '{}',
|
|
3937
|
+
});
|
|
3938
|
+
var logoutData = await logoutRes.json();
|
|
3939
|
+
if (!logoutRes.ok || !logoutData.ok) throw new Error(logoutData.error || 'Could not sign out of Dev Tunnels');
|
|
3940
|
+
if (_lastNetworkSettings) {
|
|
3941
|
+
_lastNetworkSettings.microsoft_dev_tunnel = logoutData.setup || _lastNetworkSettings.microsoft_dev_tunnel || {};
|
|
3942
|
+
}
|
|
3943
|
+
_renderMicrosoftDevTunnelSetup((_lastNetworkSettings || {}).microsoft_dev_tunnel || {}, _lastNetworkSettings || {});
|
|
3944
|
+
var switchLabel = _msProviderLabel();
|
|
3945
|
+
_setMicrosoftActionStatus('Signed out. Starting ' + switchLabel + ' sign-in now...', '');
|
|
3946
|
+
if (btn) btn.textContent = 'Opening ' + switchLabel + '...';
|
|
3947
|
+
var loginData = await _postMicrosoftTunnelLogin(true, { forceNew: true });
|
|
3948
|
+
_renderMicrosoftProgress(loginData.progress || {});
|
|
3949
|
+
_setMicrosoftActionStatus(switchLabel + ' sign-in started. Enter the displayed code on the ' + switchLabel + ' page, then click Set Up again after it finishes.', 'ok');
|
|
3950
|
+
setupToast(switchLabel + ' sign-in started');
|
|
3951
|
+
} catch (e) {
|
|
3952
|
+
if (err) { err.textContent = e.message; err.style.display = 'block'; }
|
|
3953
|
+
_setMicrosoftActionStatus(e.message || 'Could not switch Dev Tunnels account', 'error');
|
|
3954
|
+
} finally {
|
|
3955
|
+
if (btn) { btn.disabled = false; btn.textContent = 'Use Different Account'; }
|
|
3956
|
+
}
|
|
3957
|
+
}
|
|
3958
|
+
|
|
3315
3959
|
async function startMicrosoftTunnel() {
|
|
3316
3960
|
var err = document.getElementById('setup-network-err');
|
|
3317
3961
|
var btn = document.getElementById('setup-ms-setup');
|
|
@@ -3321,10 +3965,12 @@ async function startMicrosoftTunnel() {
|
|
|
3321
3965
|
if (btn) { btn.disabled = true; btn.textContent = 'Starting...'; }
|
|
3322
3966
|
var completed = false;
|
|
3323
3967
|
try {
|
|
3968
|
+
var settings = _lastNetworkSettings || {};
|
|
3969
|
+
var mode = _microsoftTunnelSelectedAccessMode(settings.microsoft_dev_tunnel || {});
|
|
3324
3970
|
var r = await fetch('/api/setup/network/microsoft-dev-tunnel/start', {
|
|
3325
3971
|
method: 'POST',
|
|
3326
3972
|
headers: { 'Content-Type': 'application/json' },
|
|
3327
|
-
body: JSON.stringify({}),
|
|
3973
|
+
body: JSON.stringify({ access_mode: mode }),
|
|
3328
3974
|
});
|
|
3329
3975
|
var d = await r.json();
|
|
3330
3976
|
if (!r.ok || !d.ok) throw new Error(d.error || 'Microsoft tunnel setup failed');
|
|
@@ -3358,7 +4004,10 @@ async function startMicrosoftTunnel() {
|
|
|
3358
4004
|
_renderPhoneSetupGuidance('microsoft', updatedNetwork);
|
|
3359
4005
|
_renderDeviceClaimAction(updatedNetwork);
|
|
3360
4006
|
_setNetworkStatus('Microsoft tunnel ready', true);
|
|
3361
|
-
_setMicrosoftActionStatus(
|
|
4007
|
+
_setMicrosoftActionStatus(mode === 'ctm_authenticated'
|
|
4008
|
+
? 'Microsoft tunnel is ready. The phone URL should load CTM directly, then CTM will handle pairing.'
|
|
4009
|
+
: 'Microsoft tunnel is ready. Sign in to Microsoft/GitHub in the phone browser, then CTM will handle pairing.',
|
|
4010
|
+
'ok');
|
|
3362
4011
|
setupToast('Microsoft tunnel ready');
|
|
3363
4012
|
completed = true;
|
|
3364
4013
|
var createClaim = !!(document.getElementById('setup-ms-create-claim') || {}).checked;
|
|
@@ -3433,6 +4082,80 @@ async function recoverMicrosoftTunnel() {
|
|
|
3433
4082
|
}
|
|
3434
4083
|
}
|
|
3435
4084
|
|
|
4085
|
+
async function resetMicrosoftTunnelPrivateAccess() {
|
|
4086
|
+
if (_microsoftSetupInFlight) return;
|
|
4087
|
+
_microsoftSetupInFlight = true;
|
|
4088
|
+
var err = document.getElementById('setup-network-err');
|
|
4089
|
+
var btn = document.getElementById('setup-ms-reset-private');
|
|
4090
|
+
if (err) err.style.display = 'none';
|
|
4091
|
+
if (btn) { btn.disabled = true; btn.textContent = 'Resetting...'; }
|
|
4092
|
+
_setMicrosoftActionStatus('Resetting Microsoft Dev Tunnel access to private...', '');
|
|
4093
|
+
try {
|
|
4094
|
+
var settings = _lastNetworkSettings || await loadNetworkSettings() || {};
|
|
4095
|
+
var ms = settings.microsoft_dev_tunnel || {};
|
|
4096
|
+
var managed = ms.managed_tunnel || {};
|
|
4097
|
+
var r = await fetch('/api/setup/network/microsoft-dev-tunnel/reset-private', {
|
|
4098
|
+
method: 'POST',
|
|
4099
|
+
headers: { 'Content-Type': 'application/json' },
|
|
4100
|
+
body: JSON.stringify({ tunnel_id: managed.tunnel_id || ms.tunnel_id || '' }),
|
|
4101
|
+
});
|
|
4102
|
+
var d = await r.json();
|
|
4103
|
+
if (!r.ok || !d.ok) throw new Error(d.error || 'Could not reset Microsoft tunnel access to private');
|
|
4104
|
+
var latest = d.microsoft_dev_tunnel || await loadNetworkSettings();
|
|
4105
|
+
if (latest && latest.microsoft_dev_tunnel) {
|
|
4106
|
+
settings = latest;
|
|
4107
|
+
} else {
|
|
4108
|
+
settings.microsoft_dev_tunnel = {
|
|
4109
|
+
...ms,
|
|
4110
|
+
managed_tunnel: {
|
|
4111
|
+
...managed,
|
|
4112
|
+
...(d.managed_tunnel || {}),
|
|
4113
|
+
access: d.access || (d.managed_tunnel && d.managed_tunnel.access) || {},
|
|
4114
|
+
},
|
|
4115
|
+
access: d.access || {},
|
|
4116
|
+
};
|
|
4117
|
+
}
|
|
4118
|
+
_microsoftTunnelAccessMode = 'private_microsoft';
|
|
4119
|
+
_microsoftTunnelAccessModeTouched = true;
|
|
4120
|
+
_lastNetworkSettings = settings;
|
|
4121
|
+
_renderMicrosoftDevTunnelSetup(settings.microsoft_dev_tunnel || {}, settings);
|
|
4122
|
+
_renderAccessMethodSelection(settings);
|
|
4123
|
+
_renderPhoneSetupGuidance(_selectedPhoneAccessMethod || 'microsoft', settings);
|
|
4124
|
+
_setMicrosoftActionStatus('Anonymous access removed. Microsoft Dev Tunnel is private again.', 'ok');
|
|
4125
|
+
setupToast('Microsoft tunnel reset to private');
|
|
4126
|
+
} catch (e) {
|
|
4127
|
+
if (err) { err.textContent = e.message; err.style.display = 'block'; }
|
|
4128
|
+
_setMicrosoftActionStatus(e.message || 'Could not reset Microsoft tunnel access to private', 'error');
|
|
4129
|
+
} finally {
|
|
4130
|
+
_microsoftSetupInFlight = false;
|
|
4131
|
+
var latestSettings = _lastNetworkSettings || {};
|
|
4132
|
+
_renderMicrosoftDevTunnelSetup((latestSettings && latestSettings.microsoft_dev_tunnel) || {}, latestSettings);
|
|
4133
|
+
}
|
|
4134
|
+
}
|
|
4135
|
+
|
|
4136
|
+
async function setMicrosoftTunnelAccessMode(mode) {
|
|
4137
|
+
var normalized = _normalizeMicrosoftTunnelAccessMode(mode) || 'ctm_authenticated';
|
|
4138
|
+
if (_microsoftTunnelAccessMode === normalized && _microsoftTunnelAccessModeTouched) return;
|
|
4139
|
+
_microsoftTunnelAccessMode = normalized;
|
|
4140
|
+
_microsoftTunnelAccessModeTouched = true;
|
|
4141
|
+
var settings = _lastNetworkSettings || {};
|
|
4142
|
+
var ms = settings.microsoft_dev_tunnel || {};
|
|
4143
|
+
_renderMicrosoftDevTunnelSetup(ms, settings);
|
|
4144
|
+
_renderPhoneSetupGuidance(_selectedPhoneAccessMethod || 'microsoft', settings);
|
|
4145
|
+
var ready = _microsoftTunnelReady(settings);
|
|
4146
|
+
if (ready && !_microsoftAccessModeMatchesSelection(ms)) {
|
|
4147
|
+
_setMicrosoftActionStatus(normalized === 'ctm_authenticated'
|
|
4148
|
+
? 'CTM-authenticated mode selected. Click Apply Mode to let phones reach CTM (CTM passkey + device token still gate access).'
|
|
4149
|
+
: 'Private Microsoft gate selected. Click Apply Mode — note: the interactive sign-in breaks the phone PWA/WebSocket, so use this only for a plain desktop browser.',
|
|
4150
|
+
'');
|
|
4151
|
+
} else {
|
|
4152
|
+
_setMicrosoftActionStatus(normalized === 'ctm_authenticated'
|
|
4153
|
+
? 'CTM-authenticated mode selected for the next tunnel start.'
|
|
4154
|
+
: 'Private Microsoft gate selected for the next tunnel start (breaks the phone PWA — desktop browser only).',
|
|
4155
|
+
'');
|
|
4156
|
+
}
|
|
4157
|
+
}
|
|
4158
|
+
|
|
3436
4159
|
async function probeMicrosoftTunnel() {
|
|
3437
4160
|
var err = document.getElementById('setup-network-err');
|
|
3438
4161
|
var latest = _lastNetworkSettings || {};
|
|
@@ -3455,7 +4178,10 @@ async function probeMicrosoftTunnel() {
|
|
|
3455
4178
|
}
|
|
3456
4179
|
_renderMicrosoftDevTunnelSetup((_lastNetworkSettings || {}).microsoft_dev_tunnel || ms, _lastNetworkSettings || latest);
|
|
3457
4180
|
if (probe.blocked_by_tunnel_auth || probe.diagnosis === 'tunnel_auth_required') {
|
|
3458
|
-
_setMicrosoftActionStatus(
|
|
4181
|
+
_setMicrosoftActionStatus(_microsoftTunnelUsesCtmAuth((_lastNetworkSettings || {}).microsoft_dev_tunnel || ms)
|
|
4182
|
+
? 'The phone URL is still blocked by Microsoft tunnel auth. Apply CTM-authenticated mode, then check again.'
|
|
4183
|
+
: 'Private Microsoft sign-in gate is active. Sign in on the phone browser, then CTM will ask for pairing.',
|
|
4184
|
+
_microsoftTunnelUsesCtmAuth((_lastNetworkSettings || {}).microsoft_dev_tunnel || ms) ? 'error' : 'ok');
|
|
3459
4185
|
} else if (probe.ctm_reachable) {
|
|
3460
4186
|
_setMicrosoftActionStatus('Phone URL reached CTM from this Mac.', 'ok');
|
|
3461
4187
|
} else {
|
|
@@ -3710,7 +4436,7 @@ function _selectedDeviceScopes() {
|
|
|
3710
4436
|
document.querySelectorAll('#setup-device-card [data-device-scope]').forEach(function(cb) {
|
|
3711
4437
|
if (cb.checked) scopes.push(cb.getAttribute('data-device-scope'));
|
|
3712
4438
|
});
|
|
3713
|
-
return scopes
|
|
4439
|
+
return _normalizeDeviceScopes(scopes);
|
|
3714
4440
|
}
|
|
3715
4441
|
|
|
3716
4442
|
function _scopeLabel(scope) {
|
|
@@ -3844,10 +4570,14 @@ async function _syncVisibleDeviceClaimScopes() {
|
|
|
3844
4570
|
|
|
3845
4571
|
function _onDeviceScopesChanged(event) {
|
|
3846
4572
|
var target = event && event.target;
|
|
4573
|
+
_deviceScopeUserTouched = true;
|
|
3847
4574
|
_normalizeDeviceScopeCheckboxes(target);
|
|
3848
4575
|
var scopes = _selectedDeviceScopes();
|
|
4576
|
+
_persistDeviceScopes(scopes, { immediate: true }).catch(function(e) {
|
|
4577
|
+
_setDeviceError(e.message || 'Phone permission save failed');
|
|
4578
|
+
});
|
|
3849
4579
|
if (!_deviceClaimVisible()) {
|
|
3850
|
-
_setDeviceScopeStatus('Selected permissions for the next QR: ' + _scopeListText(scopes) + '.', '');
|
|
4580
|
+
_setDeviceScopeStatus('Selected permissions for the next QR: ' + _scopeListText(scopes) + '. CTM will remember this after restart.', '');
|
|
3851
4581
|
return;
|
|
3852
4582
|
}
|
|
3853
4583
|
if (_scopeKey(scopes) === _scopeKey(_activeDeviceClaim.scopes)) {
|
|
@@ -3887,6 +4617,7 @@ function _invalidateVisibleDeviceClaim(reason) {
|
|
|
3887
4617
|
function initDeviceScopeControls() {
|
|
3888
4618
|
if (_deviceScopeControlsBound) return;
|
|
3889
4619
|
_deviceScopeControlsBound = true;
|
|
4620
|
+
_normalizeDeviceScopeCheckboxes();
|
|
3890
4621
|
document.querySelectorAll('#setup-device-card [data-device-scope]').forEach(function(cb) {
|
|
3891
4622
|
cb.addEventListener('change', _onDeviceScopesChanged);
|
|
3892
4623
|
});
|
|
@@ -3951,6 +4682,11 @@ function _deviceDuplicateText(device) {
|
|
|
3951
4682
|
return String(device.duplicate_count) + ' similar pairings';
|
|
3952
4683
|
}
|
|
3953
4684
|
|
|
4685
|
+
function _deviceDuplicateActionText(device) {
|
|
4686
|
+
if (!device || Number(device.duplicate_count || 0) <= 1) return '';
|
|
4687
|
+
return device.duplicate_of ? 'Keep this phone' : 'Keep newest';
|
|
4688
|
+
}
|
|
4689
|
+
|
|
3954
4690
|
function _deviceIsRemoved(device) {
|
|
3955
4691
|
if (!device) return false;
|
|
3956
4692
|
var auth = String(device.authorization_status || '').toLowerCase();
|
|
@@ -3982,6 +4718,7 @@ function _renderDevices(devices) {
|
|
|
3982
4718
|
list.innerHTML = rows.map(function(d) {
|
|
3983
4719
|
var last = d.last_used_at ? new Date(d.last_used_at).toLocaleString() : 'Never used';
|
|
3984
4720
|
var duplicate = _deviceDuplicateText(d);
|
|
4721
|
+
var duplicateAction = _deviceDuplicateActionText(d);
|
|
3985
4722
|
return '<div class="setup-device-row">'
|
|
3986
4723
|
+ '<div class="setup-device-main">'
|
|
3987
4724
|
+ '<label class="setup-device-label-editor"><span>Paired device label</span><input type="text" value="' + _escHtml(d.label || 'Phone') + '" autocomplete="off" data-device-label-input></label>'
|
|
@@ -3991,6 +4728,7 @@ function _renderDevices(devices) {
|
|
|
3991
4728
|
+ '<div class="setup-device-actions">'
|
|
3992
4729
|
+ '<span class="setup-device-status ' + _escHtml(_deviceStatusClass(d)) + '">' + _escHtml(_deviceStatusLabel(d)) + '</span>'
|
|
3993
4730
|
+ '<span class="setup-device-last">Last seen: ' + _escHtml(last) + '</span>'
|
|
4731
|
+
+ (duplicateAction ? '<button class="setup-btn setup-btn-secondary btn-xs" type="button" data-device-keep-duplicates="' + _escHtml(d.id) + '">' + _escHtml(duplicateAction) + '</button>' : '')
|
|
3994
4732
|
+ '<button class="setup-btn setup-btn-secondary btn-xs" type="button" data-device-save-label="' + _escHtml(d.id) + '">Save</button>'
|
|
3995
4733
|
+ '<button class="setup-btn setup-btn-danger btn-xs" type="button" data-device-remove="' + _escHtml(d.id) + '">Remove</button>'
|
|
3996
4734
|
+ '</div>'
|
|
@@ -4001,6 +4739,11 @@ function _renderDevices(devices) {
|
|
|
4001
4739
|
removeDeviceConnection(btn.getAttribute('data-device-remove'));
|
|
4002
4740
|
});
|
|
4003
4741
|
});
|
|
4742
|
+
list.querySelectorAll('[data-device-keep-duplicates]').forEach(function(btn) {
|
|
4743
|
+
btn.addEventListener('click', function() {
|
|
4744
|
+
revokeDuplicateDeviceConnections(btn.getAttribute('data-device-keep-duplicates'));
|
|
4745
|
+
});
|
|
4746
|
+
});
|
|
4004
4747
|
list.querySelectorAll('[data-device-save-label]').forEach(function(btn) {
|
|
4005
4748
|
btn.addEventListener('click', function() {
|
|
4006
4749
|
var row = btn.closest('.setup-device-row');
|
|
@@ -4024,6 +4767,119 @@ function _renderDevices(devices) {
|
|
|
4024
4767
|
});
|
|
4025
4768
|
}
|
|
4026
4769
|
|
|
4770
|
+
function _pairingRequestDetailText(request) {
|
|
4771
|
+
var details = [];
|
|
4772
|
+
if (request && request.device_hint) details.push(request.device_hint);
|
|
4773
|
+
if (request && request.origin) {
|
|
4774
|
+
try { details.push(new URL(request.origin).host); }
|
|
4775
|
+
catch { details.push(request.origin); }
|
|
4776
|
+
}
|
|
4777
|
+
if (request && request.expires_at) {
|
|
4778
|
+
var remaining = Number(request.expires_at) - Date.now();
|
|
4779
|
+
if (remaining > 0) details.push('expires in ' + Math.max(1, Math.ceil(remaining / 60000)) + ' min');
|
|
4780
|
+
}
|
|
4781
|
+
return details.join(' · ');
|
|
4782
|
+
}
|
|
4783
|
+
|
|
4784
|
+
function _renderPairingRequests(requests) {
|
|
4785
|
+
var list = document.getElementById('setup-pairing-requests');
|
|
4786
|
+
if (!list) return;
|
|
4787
|
+
var rows = Array.isArray(requests) ? requests.filter(function(request) {
|
|
4788
|
+
return request && request.status === 'pending';
|
|
4789
|
+
}) : [];
|
|
4790
|
+
if (!rows.length) {
|
|
4791
|
+
list.innerHTML = '';
|
|
4792
|
+
return;
|
|
4793
|
+
}
|
|
4794
|
+
list.innerHTML = '<div class="setup-device-empty" style="margin-top:10px;color:var(--yellow);">Phone requests waiting for approval</div>'
|
|
4795
|
+
+ rows.map(function(request) {
|
|
4796
|
+
return '<div class="setup-device-row setup-pairing-request-row" data-pairing-request-row="' + _escHtml(request.id) + '">'
|
|
4797
|
+
+ '<div class="setup-device-main">'
|
|
4798
|
+
+ '<strong>Approve phone code ' + _escHtml(request.code || '') + '</strong>'
|
|
4799
|
+
+ '<span class="setup-device-meta">' + _escHtml(request.label || 'Phone') + (request.scopes && request.scopes.length ? ' · requested ' + _escHtml(_scopeListText(request.scopes)) : '') + '</span>'
|
|
4800
|
+
+ '<span class="setup-device-meta">' + _escHtml(_pairingRequestDetailText(request)) + '</span>'
|
|
4801
|
+
+ '</div>'
|
|
4802
|
+
+ '<div class="setup-device-actions">'
|
|
4803
|
+
+ '<span class="setup-device-status recent">Waiting</span>'
|
|
4804
|
+
+ '<button class="setup-btn setup-btn-primary btn-xs" type="button" data-pairing-approve="' + _escHtml(request.id) + '">Approve</button>'
|
|
4805
|
+
+ '<button class="setup-btn setup-btn-secondary btn-xs" type="button" data-pairing-reject="' + _escHtml(request.id) + '">Reject</button>'
|
|
4806
|
+
+ '</div>'
|
|
4807
|
+
+ '</div>';
|
|
4808
|
+
}).join('');
|
|
4809
|
+
list.querySelectorAll('[data-pairing-approve]').forEach(function(btn) {
|
|
4810
|
+
btn.addEventListener('click', function() {
|
|
4811
|
+
approvePairingRequest(btn.getAttribute('data-pairing-approve'));
|
|
4812
|
+
});
|
|
4813
|
+
});
|
|
4814
|
+
list.querySelectorAll('[data-pairing-reject]').forEach(function(btn) {
|
|
4815
|
+
btn.addEventListener('click', function() {
|
|
4816
|
+
rejectPairingRequest(btn.getAttribute('data-pairing-reject'));
|
|
4817
|
+
});
|
|
4818
|
+
});
|
|
4819
|
+
}
|
|
4820
|
+
|
|
4821
|
+
function _schedulePairingRequestsPoll(delayMs) {
|
|
4822
|
+
clearTimeout(_pairingRequestsPollTimer);
|
|
4823
|
+
_pairingRequestsPollTimer = setTimeout(function() {
|
|
4824
|
+
loadPairingRequests().catch(function() {});
|
|
4825
|
+
}, Math.max(2000, Number(delayMs || 5000)));
|
|
4826
|
+
}
|
|
4827
|
+
|
|
4828
|
+
async function loadPairingRequests() {
|
|
4829
|
+
try {
|
|
4830
|
+
var r = await fetch('/api/auth/pairing-requests');
|
|
4831
|
+
var d = await r.json();
|
|
4832
|
+
if (!r.ok || !d.ok) throw new Error(d.error || 'Pairing requests unavailable');
|
|
4833
|
+
var requests = d.requests || [];
|
|
4834
|
+
_renderPairingRequests(requests);
|
|
4835
|
+
_schedulePairingRequestsPoll(requests.length ? 3000 : 5000);
|
|
4836
|
+
return requests;
|
|
4837
|
+
} catch (e) {
|
|
4838
|
+
var list = document.getElementById('setup-pairing-requests');
|
|
4839
|
+
if (list) list.innerHTML = '<div class="setup-device-empty" style="margin-top:10px;color:var(--red);">' + _escHtml(e.message || 'Pairing requests unavailable') + '</div>';
|
|
4840
|
+
_schedulePairingRequestsPoll(8000);
|
|
4841
|
+
return [];
|
|
4842
|
+
}
|
|
4843
|
+
}
|
|
4844
|
+
|
|
4845
|
+
async function approvePairingRequest(requestId) {
|
|
4846
|
+
if (!requestId) return;
|
|
4847
|
+
_setDeviceError('');
|
|
4848
|
+
try {
|
|
4849
|
+
await _persistDeviceLabel(_deviceLabelValue(), { immediate: true });
|
|
4850
|
+
await _persistDeviceScopes(_selectedDeviceScopes(), { immediate: true });
|
|
4851
|
+
var r = await fetch('/api/auth/pairing-requests/' + encodeURIComponent(requestId) + '/approve', {
|
|
4852
|
+
method: 'POST',
|
|
4853
|
+
headers: { 'Content-Type': 'application/json' },
|
|
4854
|
+
body: JSON.stringify({ label: _deviceLabelValue(), scopes: _selectedDeviceScopes() }),
|
|
4855
|
+
});
|
|
4856
|
+
var d = await r.json();
|
|
4857
|
+
if (!r.ok || !d.ok) throw new Error(d.message || d.error || 'Pairing approval failed');
|
|
4858
|
+
setupToast('Phone pairing approved');
|
|
4859
|
+
await loadPairingRequests();
|
|
4860
|
+
} catch (e) {
|
|
4861
|
+
_setDeviceError(e.message || 'Pairing approval failed');
|
|
4862
|
+
}
|
|
4863
|
+
}
|
|
4864
|
+
|
|
4865
|
+
async function rejectPairingRequest(requestId) {
|
|
4866
|
+
if (!requestId) return;
|
|
4867
|
+
_setDeviceError('');
|
|
4868
|
+
try {
|
|
4869
|
+
var r = await fetch('/api/auth/pairing-requests/' + encodeURIComponent(requestId) + '/reject', {
|
|
4870
|
+
method: 'POST',
|
|
4871
|
+
headers: { 'Content-Type': 'application/json' },
|
|
4872
|
+
body: '{}',
|
|
4873
|
+
});
|
|
4874
|
+
var d = await r.json();
|
|
4875
|
+
if (!r.ok || !d.ok) throw new Error(d.message || d.error || 'Pairing rejection failed');
|
|
4876
|
+
setupToast('Phone pairing rejected', 'warning');
|
|
4877
|
+
await loadPairingRequests();
|
|
4878
|
+
} catch (e) {
|
|
4879
|
+
_setDeviceError(e.message || 'Pairing rejection failed');
|
|
4880
|
+
}
|
|
4881
|
+
}
|
|
4882
|
+
|
|
4027
4883
|
async function removeDeviceConnection(deviceId) {
|
|
4028
4884
|
if (!deviceId) return;
|
|
4029
4885
|
if (typeof window.confirm === 'function' && !window.confirm('Remove this phone connection from CTM? It will need to pair again before it can control this Mac.')) return;
|
|
@@ -4042,6 +4898,26 @@ async function removeDeviceConnection(deviceId) {
|
|
|
4042
4898
|
|
|
4043
4899
|
var revokeDevice = removeDeviceConnection;
|
|
4044
4900
|
|
|
4901
|
+
async function revokeDuplicateDeviceConnections(deviceId) {
|
|
4902
|
+
if (!deviceId) return;
|
|
4903
|
+
if (typeof window.confirm === 'function' && !window.confirm('Keep this phone pairing and revoke other active duplicate pairings for the same phone?')) return;
|
|
4904
|
+
_setDeviceError('');
|
|
4905
|
+
try {
|
|
4906
|
+
var r = await fetch('/api/auth/device-duplicates/revoke', {
|
|
4907
|
+
method: 'POST',
|
|
4908
|
+
headers: { 'Content-Type': 'application/json' },
|
|
4909
|
+
body: JSON.stringify({ keep_device_id: deviceId }),
|
|
4910
|
+
});
|
|
4911
|
+
var d = await r.json();
|
|
4912
|
+
if (!r.ok || !d.ok) throw new Error(d.message || d.error || 'Duplicate cleanup failed');
|
|
4913
|
+
setupToast(d.revoked ? 'Duplicate phone pairings revoked' : 'No active duplicate pairings to revoke');
|
|
4914
|
+
await loadDevices();
|
|
4915
|
+
if (_lastRemoteStatus) await loadWalleRemoteStatus();
|
|
4916
|
+
} catch (e) {
|
|
4917
|
+
_setDeviceError(e.message || 'Duplicate cleanup failed');
|
|
4918
|
+
}
|
|
4919
|
+
}
|
|
4920
|
+
|
|
4045
4921
|
async function saveDeviceLabel(deviceId, label) {
|
|
4046
4922
|
var clean = String(label || '').trim().replace(/\s+/g, ' ') || 'Phone';
|
|
4047
4923
|
_setDeviceError('');
|
|
@@ -4090,7 +4966,7 @@ async function createDeviceClaim(options) {
|
|
|
4090
4966
|
|| preferredOrigin
|
|
4091
4967
|
|| window.location.origin;
|
|
4092
4968
|
if (_isWalleHostedMobileOrigin(origin)) {
|
|
4093
|
-
var message = 'm.walle.sh is the Walle Remote app, not a direct CTM claim endpoint. Use Walle Remote pairing after hosted relay is connected, or select
|
|
4969
|
+
var message = 'm.walle.sh is the Walle Remote app, not a direct CTM claim endpoint. Use Walle Remote pairing after hosted relay is connected, or select Microsoft tunnel/Tailscale for direct phone access.';
|
|
4094
4970
|
_hideDeviceClaim();
|
|
4095
4971
|
_setDeviceError(message);
|
|
4096
4972
|
if (opts.throwOnError) throw new Error(message);
|
|
@@ -4098,6 +4974,7 @@ async function createDeviceClaim(options) {
|
|
|
4098
4974
|
}
|
|
4099
4975
|
try {
|
|
4100
4976
|
await _persistDeviceLabel(label, { immediate: true });
|
|
4977
|
+
await _persistDeviceScopes(_selectedDeviceScopes(), { immediate: true });
|
|
4101
4978
|
var r = await fetch('/api/auth/device-claims', {
|
|
4102
4979
|
method: 'POST',
|
|
4103
4980
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -4308,6 +5185,129 @@ function _embedDimMsg(text) {
|
|
|
4308
5185
|
return d;
|
|
4309
5186
|
}
|
|
4310
5187
|
|
|
5188
|
+
function _embedPolicyLabel(policy) {
|
|
5189
|
+
if (policy === 'local_only') return 'Local only';
|
|
5190
|
+
if (policy === 'cloud_allowed') return 'Cloud allowed';
|
|
5191
|
+
if (policy === 'off') return 'Off';
|
|
5192
|
+
return 'Auto, local first';
|
|
5193
|
+
}
|
|
5194
|
+
|
|
5195
|
+
function _embedPolicyHint(policy) {
|
|
5196
|
+
if (policy === 'local_only') return 'Uses Ollama only. Memory text stays on this machine.';
|
|
5197
|
+
if (policy === 'cloud_allowed') return 'Uses local first, then an existing cloud embedding key if local is unavailable.';
|
|
5198
|
+
if (policy === 'off') return 'Disables semantic Memory Search. Keyword search still works.';
|
|
5199
|
+
return 'Turns on local semantic search automatically and asks before using cloud embeddings.';
|
|
5200
|
+
}
|
|
5201
|
+
|
|
5202
|
+
function _embedProviderLabel(model, providers) {
|
|
5203
|
+
var found = (providers || []).filter(function(p) { return p.model === model; })[0];
|
|
5204
|
+
return found ? found.label : model;
|
|
5205
|
+
}
|
|
5206
|
+
|
|
5207
|
+
function _embedMakeButton(label, className, onClick) {
|
|
5208
|
+
var btn = document.createElement('button');
|
|
5209
|
+
btn.className = className || 'setup-btn setup-btn-secondary btn-xs';
|
|
5210
|
+
btn.textContent = label;
|
|
5211
|
+
btn.onclick = onClick;
|
|
5212
|
+
return btn;
|
|
5213
|
+
}
|
|
5214
|
+
|
|
5215
|
+
function _embedAddBenefitDetails(parent, totalMemories) {
|
|
5216
|
+
var details = document.createElement('details');
|
|
5217
|
+
details.className = 'embed-learn-more';
|
|
5218
|
+
var summary = document.createElement('summary');
|
|
5219
|
+
summary.textContent = 'Learn about benefits and ROI';
|
|
5220
|
+
details.appendChild(summary);
|
|
5221
|
+
|
|
5222
|
+
var list = document.createElement('ul');
|
|
5223
|
+
[
|
|
5224
|
+
'Finds memories by meaning, so Wall-E can recover context even when your wording changed.',
|
|
5225
|
+
'Reduces repeated explanations because older sessions, notes, and work facts become easier to retrieve.',
|
|
5226
|
+
'Local embeddings are private and free. Cloud embeddings require opt-in because memory text is sent to the provider.',
|
|
5227
|
+
totalMemories > 0
|
|
5228
|
+
? 'Current corpus: about ' + totalMemories.toLocaleString() + ' searchable memory items once backfill completes.'
|
|
5229
|
+
: 'It starts helping as soon as Wall-E has memories to index.',
|
|
5230
|
+
].forEach(function(text) {
|
|
5231
|
+
var li = document.createElement('li');
|
|
5232
|
+
li.textContent = text;
|
|
5233
|
+
list.appendChild(li);
|
|
5234
|
+
});
|
|
5235
|
+
details.appendChild(list);
|
|
5236
|
+
parent.appendChild(details);
|
|
5237
|
+
}
|
|
5238
|
+
|
|
5239
|
+
function renderEmbeddingAutoPanel(data, providers) {
|
|
5240
|
+
var setupState = data.setup || {};
|
|
5241
|
+
var panel = document.createElement('div');
|
|
5242
|
+
panel.className = 'embed-auto-panel embed-auto-' + (setupState.status || 'unknown');
|
|
5243
|
+
|
|
5244
|
+
var top = document.createElement('div');
|
|
5245
|
+
top.className = 'embed-auto-top';
|
|
5246
|
+
|
|
5247
|
+
var title = document.createElement('div');
|
|
5248
|
+
title.className = 'embed-auto-title';
|
|
5249
|
+
title.textContent = setupState.status === 'off'
|
|
5250
|
+
? 'Memory Search is off'
|
|
5251
|
+
: (setupState.activeLabel ? 'Memory Search is on' : 'Memory Search is ready to automate');
|
|
5252
|
+
top.appendChild(title);
|
|
5253
|
+
|
|
5254
|
+
var policy = document.createElement('span');
|
|
5255
|
+
policy.className = 'badge badge-ready';
|
|
5256
|
+
policy.textContent = _embedPolicyLabel(setupState.policy);
|
|
5257
|
+
policy.title = _embedPolicyHint(setupState.policy);
|
|
5258
|
+
top.appendChild(policy);
|
|
5259
|
+
panel.appendChild(top);
|
|
5260
|
+
|
|
5261
|
+
var body = document.createElement('div');
|
|
5262
|
+
body.className = 'embed-auto-body';
|
|
5263
|
+
body.textContent = setupState.message || 'Semantic search indexes memory in the background.';
|
|
5264
|
+
panel.appendChild(body);
|
|
5265
|
+
|
|
5266
|
+
if (setupState.status === 'cloud_opt_in_available') {
|
|
5267
|
+
var warning = document.createElement('div');
|
|
5268
|
+
warning.className = 'embed-optin-warning';
|
|
5269
|
+
warning.textContent = 'Cloud opt-in sends memory text and search queries to the selected embedding provider. Use this when recall quality matters more than local-only processing.';
|
|
5270
|
+
panel.appendChild(warning);
|
|
5271
|
+
}
|
|
5272
|
+
|
|
5273
|
+
_embedAddBenefitDetails(panel, data.totalMemories || 0);
|
|
5274
|
+
|
|
5275
|
+
var actions = document.createElement('div');
|
|
5276
|
+
actions.className = 'embed-policy-actions';
|
|
5277
|
+
|
|
5278
|
+
if (setupState.status === 'off') {
|
|
5279
|
+
actions.appendChild(_embedMakeButton('Turn on auto', 'setup-btn setup-btn-primary btn-xs', function() {
|
|
5280
|
+
setEmbeddingPolicy('local_first');
|
|
5281
|
+
}));
|
|
5282
|
+
} else if (setupState.cloudOptInAvailable) {
|
|
5283
|
+
var cloudLabel = setupState.cloudAvailableModel
|
|
5284
|
+
? 'Allow cloud Memory Search (' + _embedProviderLabel(setupState.cloudAvailableModel, providers) + ')'
|
|
5285
|
+
: 'Allow cloud Memory Search';
|
|
5286
|
+
actions.appendChild(_embedMakeButton(cloudLabel, 'setup-btn setup-btn-primary btn-xs', function() {
|
|
5287
|
+
setEmbeddingPolicy('cloud_allowed');
|
|
5288
|
+
}));
|
|
5289
|
+
actions.appendChild(_embedMakeButton('Stay local only', 'setup-btn setup-btn-secondary btn-xs', function() {
|
|
5290
|
+
setEmbeddingPolicy('local_only');
|
|
5291
|
+
}));
|
|
5292
|
+
} else {
|
|
5293
|
+
actions.appendChild(_embedMakeButton('Auto local first', 'setup-btn setup-btn-secondary btn-xs' + (setupState.policy === 'local_first' ? ' active' : ''), function() {
|
|
5294
|
+
setEmbeddingPolicy('local_first');
|
|
5295
|
+
}));
|
|
5296
|
+
actions.appendChild(_embedMakeButton('Local only', 'setup-btn setup-btn-secondary btn-xs' + (setupState.policy === 'local_only' ? ' active' : ''), function() {
|
|
5297
|
+
setEmbeddingPolicy('local_only');
|
|
5298
|
+
}));
|
|
5299
|
+
actions.appendChild(_embedMakeButton('Cloud allowed', 'setup-btn setup-btn-secondary btn-xs' + (setupState.policy === 'cloud_allowed' ? ' active' : ''), function() {
|
|
5300
|
+
setEmbeddingPolicy('cloud_allowed');
|
|
5301
|
+
}));
|
|
5302
|
+
actions.appendChild(_embedMakeButton('Off', 'setup-btn setup-btn-secondary btn-xs' + (setupState.policy === 'off' ? ' active' : ''), function() {
|
|
5303
|
+
setEmbeddingPolicy('off');
|
|
5304
|
+
}));
|
|
5305
|
+
}
|
|
5306
|
+
|
|
5307
|
+
panel.appendChild(actions);
|
|
5308
|
+
return panel;
|
|
5309
|
+
}
|
|
5310
|
+
|
|
4311
5311
|
async function loadEmbeddings() {
|
|
4312
5312
|
var container = document.getElementById('setup-embed-providers');
|
|
4313
5313
|
var statsEl = document.getElementById('setup-embed-stats');
|
|
@@ -4318,7 +5318,13 @@ async function loadEmbeddings() {
|
|
|
4318
5318
|
var r = await fetch('/api/setup/embeddings');
|
|
4319
5319
|
var d = await r.json();
|
|
4320
5320
|
if (!d.providers || d.providers.length === 0) {
|
|
4321
|
-
|
|
5321
|
+
var emptyFrag = document.createDocumentFragment();
|
|
5322
|
+
emptyFrag.appendChild(renderEmbeddingAutoPanel(d, []));
|
|
5323
|
+
emptyFrag.appendChild(_embedDimMsg('No embedding providers available.'));
|
|
5324
|
+
container.replaceChildren(emptyFrag);
|
|
5325
|
+
if (dotEl) dotEl.className = 'status-dot missing';
|
|
5326
|
+
if (statsEl) statsEl.textContent = 'No embedding provider active';
|
|
5327
|
+
if (nudgeEl) nudgeEl.style.display = 'none';
|
|
4322
5328
|
return;
|
|
4323
5329
|
}
|
|
4324
5330
|
|
|
@@ -4332,6 +5338,7 @@ async function loadEmbeddings() {
|
|
|
4332
5338
|
|
|
4333
5339
|
_embedProvidersCache = sorted;
|
|
4334
5340
|
var frag = document.createDocumentFragment();
|
|
5341
|
+
frag.appendChild(renderEmbeddingAutoPanel(d, sorted));
|
|
4335
5342
|
var hasActive = false, hasGeminiKey = false, activeUnreachable = false;
|
|
4336
5343
|
|
|
4337
5344
|
for (var i = 0; i < sorted.length; i++) {
|
|
@@ -4469,8 +5476,12 @@ async function loadEmbeddings() {
|
|
|
4469
5476
|
|
|
4470
5477
|
async function switchEmbedding(model, label) {
|
|
4471
5478
|
var currentActive = _embedProvidersCache && _embedProvidersCache.find(function(p) { return p.isActive; });
|
|
5479
|
+
var nextProvider = _embedProvidersCache && _embedProvidersCache.find(function(p) { return p.model === model; });
|
|
4472
5480
|
var hasOldEmbeddings = currentActive && currentActive.embeddingCount > 0;
|
|
4473
5481
|
var msg = 'Switch to ' + label + '?\n\nThis will start embedding all memories with the new provider.';
|
|
5482
|
+
if (nextProvider && nextProvider.provider !== 'ollama') {
|
|
5483
|
+
msg += '\n\nPrivacy note: this sends memory text and future search queries to ' + label + ' for embeddings. Use Auto local-first or Local only if you want local processing.';
|
|
5484
|
+
}
|
|
4474
5485
|
if (hasOldEmbeddings) {
|
|
4475
5486
|
msg += '\n\nOld ' + currentActive.label + ' embeddings (' + currentActive.embeddingCount.toLocaleString() + ') are preserved \u2014 you can delete them from this page to save space.';
|
|
4476
5487
|
}
|
|
@@ -4490,6 +5501,26 @@ async function switchEmbedding(model, label) {
|
|
|
4490
5501
|
} catch (e) { setupToast(e.message, 'error'); }
|
|
4491
5502
|
}
|
|
4492
5503
|
|
|
5504
|
+
async function setEmbeddingPolicy(policy) {
|
|
5505
|
+
if (policy === 'cloud_allowed') {
|
|
5506
|
+
var ok = confirm('Allow cloud Memory Search?\n\nThis can improve recall when local embeddings are unavailable, but memory text and search queries will be sent to the selected cloud embedding provider. You can switch back to Local only at any time.');
|
|
5507
|
+
if (!ok) return;
|
|
5508
|
+
}
|
|
5509
|
+
try {
|
|
5510
|
+
var r = await fetch('/api/setup/embeddings/policy', {
|
|
5511
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
5512
|
+
body: JSON.stringify({ policy: policy }),
|
|
5513
|
+
});
|
|
5514
|
+
var d = await r.json();
|
|
5515
|
+
if (d.ok) {
|
|
5516
|
+
setupToast('Memory Search: ' + _embedPolicyLabel(d.policy || policy));
|
|
5517
|
+
setTimeout(loadEmbeddings, 800);
|
|
5518
|
+
} else {
|
|
5519
|
+
setupToast(d.message || 'Policy update failed', 'error');
|
|
5520
|
+
}
|
|
5521
|
+
} catch (e) { setupToast(e.message, 'error'); }
|
|
5522
|
+
}
|
|
5523
|
+
|
|
4493
5524
|
async function deleteEmbedding(model, label, count) {
|
|
4494
5525
|
if (!confirm('Delete ' + count.toLocaleString() + ' embeddings from ' + label + '?\n\nThis frees disk space but cannot be undone.')) return;
|
|
4495
5526
|
try {
|
|
@@ -4756,15 +5787,155 @@ async function restoreExpandedPcard() {
|
|
|
4756
5787
|
} catch (_) { /* leave all collapsed */ }
|
|
4757
5788
|
}
|
|
4758
5789
|
|
|
5790
|
+
function _storageField(id) {
|
|
5791
|
+
var el = document.getElementById(id);
|
|
5792
|
+
return el ? String(el.value || '').trim() : '';
|
|
5793
|
+
}
|
|
5794
|
+
|
|
5795
|
+
function _setStorageStatus(message, kind) {
|
|
5796
|
+
var status = document.getElementById('setup-storage-status');
|
|
5797
|
+
if (!status) return;
|
|
5798
|
+
status.textContent = message || '';
|
|
5799
|
+
status.className = 'setup-storage-status' + (kind ? ' ' + kind : '');
|
|
5800
|
+
}
|
|
5801
|
+
|
|
5802
|
+
function _storagePayload() {
|
|
5803
|
+
var move = document.getElementById('setup-storage-move-backups');
|
|
5804
|
+
return {
|
|
5805
|
+
services: {
|
|
5806
|
+
ctm: {
|
|
5807
|
+
data_dir: _storageField('setup-storage-ctm-data-dir'),
|
|
5808
|
+
backup_dir: _storageField('setup-storage-ctm-backup-dir'),
|
|
5809
|
+
move_backups: !move || move.checked,
|
|
5810
|
+
},
|
|
5811
|
+
walle: {
|
|
5812
|
+
data_dir: _storageField('setup-storage-walle-data-dir'),
|
|
5813
|
+
backup_dir: _storageField('setup-storage-walle-backup-dir'),
|
|
5814
|
+
move_backups: !move || move.checked,
|
|
5815
|
+
},
|
|
5816
|
+
},
|
|
5817
|
+
};
|
|
5818
|
+
}
|
|
5819
|
+
|
|
5820
|
+
function _renderStoragePlan(plan) {
|
|
5821
|
+
var box = document.getElementById('setup-storage-plan');
|
|
5822
|
+
var applyBtn = document.getElementById('setup-storage-apply-btn');
|
|
5823
|
+
if (!box) return;
|
|
5824
|
+
if (!plan) {
|
|
5825
|
+
box.style.display = 'none';
|
|
5826
|
+
box.textContent = '';
|
|
5827
|
+
if (applyBtn) applyBtn.disabled = true;
|
|
5828
|
+
return;
|
|
5829
|
+
}
|
|
5830
|
+
var changes = [];
|
|
5831
|
+
['ctm', 'walle'].forEach(function(name) {
|
|
5832
|
+
var svc = plan.services && plan.services[name];
|
|
5833
|
+
if (!svc) return;
|
|
5834
|
+
if (svc.data_dir_changed) changes.push(svc.label + ' live DB: ' + svc.current_data_dir + ' -> ' + svc.target_data_dir);
|
|
5835
|
+
if (svc.backup_dir_changed) changes.push(svc.label + ' backups: ' + svc.current_backup_dir + ' -> ' + svc.target_backup_dir);
|
|
5836
|
+
});
|
|
5837
|
+
var warnings = Array.isArray(plan.warnings) ? plan.warnings : [];
|
|
5838
|
+
box.style.display = 'block';
|
|
5839
|
+
box.innerHTML = ''
|
|
5840
|
+
+ '<h3>' + (changes.length ? 'Migration Preview' : 'No storage changes detected') + '</h3>'
|
|
5841
|
+
+ (changes.length ? '<ul>' + changes.map(function(line) { return '<li>' + _escHtml(line) + '</li>'; }).join('') + '</ul>' : '<div class="setup-backup-folder-status">The configured live and backup paths already match the requested values.</div>')
|
|
5842
|
+
+ (warnings.length ? '<div class="setup-backup-folder-status">Warnings</div><ul>' + warnings.map(function(w) { return '<li>' + _escHtml(w.message || String(w)) + '</li>'; }).join('') + '</ul>' : '')
|
|
5843
|
+
+ (plan.steps && plan.steps.length ? '<div class="setup-backup-folder-status">Steps: ' + _escHtml(plan.steps.join(' -> ')) + '</div>' : '');
|
|
5844
|
+
if (applyBtn) applyBtn.disabled = changes.length === 0;
|
|
5845
|
+
}
|
|
5846
|
+
|
|
5847
|
+
async function saveBackupDir() {
|
|
5848
|
+
var status = document.getElementById('setup-storage-status');
|
|
5849
|
+
if (status) status.textContent = 'Saving backup folders...';
|
|
5850
|
+
try {
|
|
5851
|
+
var move = document.getElementById('setup-storage-move-backups');
|
|
5852
|
+
var r = await fetch('/api/storage/backup-dirs', {
|
|
5853
|
+
method: 'PUT',
|
|
5854
|
+
headers: { 'Content-Type': 'application/json' },
|
|
5855
|
+
body: JSON.stringify({
|
|
5856
|
+
ctm_backup_dir: _storageField('setup-storage-ctm-backup-dir'),
|
|
5857
|
+
walle_backup_dir: _storageField('setup-storage-walle-backup-dir'),
|
|
5858
|
+
move_existing: !move || move.checked,
|
|
5859
|
+
}),
|
|
5860
|
+
});
|
|
5861
|
+
var d = await r.json().catch(function() { return {}; });
|
|
5862
|
+
if (!r.ok || d.error || d.ok === false) {
|
|
5863
|
+
var details = d.errors && d.errors.length ? ': ' + d.errors.map(function(e) { return e.service + ' ' + e.error; }).join('; ') : '';
|
|
5864
|
+
throw new Error((d.error || 'Failed to save backup folders') + details);
|
|
5865
|
+
}
|
|
5866
|
+
_setStorageStatus('Backup folders saved. Existing backup snapshots were ' + (!move || move.checked ? 'moved when needed.' : 'left in place.'), 'ok');
|
|
5867
|
+
_renderStoragePlan(null);
|
|
5868
|
+
setupToast('Backup folders saved');
|
|
5869
|
+
if (typeof window.loadBackupsData === 'function') window.loadBackupsData();
|
|
5870
|
+
} catch (e) {
|
|
5871
|
+
_setStorageStatus(e.message || String(e), 'error');
|
|
5872
|
+
}
|
|
5873
|
+
}
|
|
5874
|
+
|
|
5875
|
+
async function resetBackupDir() {
|
|
5876
|
+
['setup-storage-ctm-backup-dir', 'setup-storage-walle-backup-dir'].forEach(function(id) {
|
|
5877
|
+
var input = document.getElementById(id);
|
|
5878
|
+
if (input) input.value = '';
|
|
5879
|
+
});
|
|
5880
|
+
return saveBackupDir();
|
|
5881
|
+
}
|
|
5882
|
+
|
|
5883
|
+
async function previewStorageMigration() {
|
|
5884
|
+
_setStorageStatus('Building storage migration preview...');
|
|
5885
|
+
try {
|
|
5886
|
+
var r = await fetch('/api/storage/migration/preview', {
|
|
5887
|
+
method: 'POST',
|
|
5888
|
+
headers: { 'Content-Type': 'application/json' },
|
|
5889
|
+
body: JSON.stringify(_storagePayload()),
|
|
5890
|
+
});
|
|
5891
|
+
var d = await r.json().catch(function() { return {}; });
|
|
5892
|
+
if (!r.ok || d.error) throw new Error(d.error || 'Failed to preview storage migration');
|
|
5893
|
+
_renderStoragePlan(d.plan);
|
|
5894
|
+
_setStorageStatus(d.plan && d.plan.requires_restart
|
|
5895
|
+
? 'Live database changes require a supervised maintenance window.'
|
|
5896
|
+
: 'Backup-only changes can be applied without restarting services.', 'ok');
|
|
5897
|
+
} catch (e) {
|
|
5898
|
+
_renderStoragePlan(null);
|
|
5899
|
+
_setStorageStatus(e.message || String(e), 'error');
|
|
5900
|
+
}
|
|
5901
|
+
}
|
|
5902
|
+
|
|
5903
|
+
async function applyStorageMigration() {
|
|
5904
|
+
if (!window.confirm('Apply the storage change now? CTM will stop services, copy and verify the database files, update configuration, then restart CTM and Wall-E.')) return;
|
|
5905
|
+
_setStorageStatus('Starting supervised storage migration...');
|
|
5906
|
+
try {
|
|
5907
|
+
var payload = _storagePayload();
|
|
5908
|
+
payload.confirm = true;
|
|
5909
|
+
var r = await fetch('/api/storage/migration/apply', {
|
|
5910
|
+
method: 'POST',
|
|
5911
|
+
headers: { 'Content-Type': 'application/json' },
|
|
5912
|
+
body: JSON.stringify(payload),
|
|
5913
|
+
});
|
|
5914
|
+
var d = await r.json().catch(function() { return {}; });
|
|
5915
|
+
if (!r.ok || d.error || d.ok === false) throw new Error(d.error || 'Failed to start storage migration');
|
|
5916
|
+
_renderStoragePlan(d.plan);
|
|
5917
|
+
_setStorageStatus(d.migration
|
|
5918
|
+
? 'Migration started. CTM may briefly disconnect while the supervisor restarts services.'
|
|
5919
|
+
: 'Storage changes applied.', 'ok');
|
|
5920
|
+
if (typeof window.loadBackupsData === 'function') window.loadBackupsData();
|
|
5921
|
+
} catch (e) {
|
|
5922
|
+
_setStorageStatus(e.message || String(e), 'error');
|
|
5923
|
+
}
|
|
5924
|
+
}
|
|
5925
|
+
|
|
4759
5926
|
// ── Public API ──────────────────────────────────────────────────────
|
|
4760
5927
|
SETUP.init = function() {
|
|
4761
5928
|
_setSetupActiveTab(_initialSetupTab());
|
|
4762
5929
|
initSetupTabs();
|
|
4763
5930
|
initProviderPicker();
|
|
5931
|
+
// Reflect the saved Dev Tunnels sign-in account choice before network data loads.
|
|
5932
|
+
_renderMicrosoftLoginProvider();
|
|
4764
5933
|
initProviderModelPreferenceTracking();
|
|
4765
5934
|
initPcardAccordion();
|
|
5935
|
+
initConnectedServicesLifecycle();
|
|
4766
5936
|
initCloudflareFieldHelpers();
|
|
4767
5937
|
initDeviceScopeControls();
|
|
5938
|
+
initDeviceScopeLifecycle();
|
|
4768
5939
|
initDeviceLabelLifecycle();
|
|
4769
5940
|
initSetupTempMemoryCapture();
|
|
4770
5941
|
// Inject the brand-mark SVG for each provider card (anthropic / openai /
|
|
@@ -4776,6 +5947,7 @@ SETUP.init = function() {
|
|
|
4776
5947
|
// string if that helper hasn't been loaded yet for any reason.
|
|
4777
5948
|
populateBrandIconsForPcards();
|
|
4778
5949
|
var deviceLabelPromise = loadDeviceLabelSetting();
|
|
5950
|
+
var deviceScopePromise = loadDeviceScopeSetting();
|
|
4779
5951
|
var statusPromise = loadStatus();
|
|
4780
5952
|
// Load card states first (sets toggles + auth radios), THEN decide which
|
|
4781
5953
|
// single card to expand. restoreExpandedPcard reads /api/settings + falls
|
|
@@ -4786,10 +5958,12 @@ SETUP.init = function() {
|
|
|
4786
5958
|
var embeddingsPromise = loadEmbeddings();
|
|
4787
5959
|
var hooksPromise = loadStatusHooks();
|
|
4788
5960
|
var networkPromise = loadNetworkSettings();
|
|
5961
|
+
loadConnectionHealth();
|
|
5962
|
+
_scheduleConnectionHealthPoll();
|
|
4789
5963
|
var devicesPromise = loadDevices();
|
|
5964
|
+
var pairingRequestsPromise = loadPairingRequests();
|
|
4790
5965
|
var healthPromise = loadHealthDashboard();
|
|
4791
5966
|
_scheduleHealthPoll();
|
|
4792
|
-
var tiersPromise = loadTaskTiers();
|
|
4793
5967
|
_restoreSetupTempStateSoon();
|
|
4794
5968
|
Promise.allSettled([
|
|
4795
5969
|
statusPromise,
|
|
@@ -4799,10 +5973,11 @@ SETUP.init = function() {
|
|
|
4799
5973
|
embeddingsPromise,
|
|
4800
5974
|
hooksPromise,
|
|
4801
5975
|
deviceLabelPromise,
|
|
5976
|
+
deviceScopePromise,
|
|
4802
5977
|
networkPromise,
|
|
4803
5978
|
devicesPromise,
|
|
5979
|
+
pairingRequestsPromise,
|
|
4804
5980
|
healthPromise,
|
|
4805
|
-
tiersPromise,
|
|
4806
5981
|
]).then(function() {
|
|
4807
5982
|
if (_setupTempMemory && _setupTempMemory.pendingRestore) restoreSetupTempState({ final: true });
|
|
4808
5983
|
});
|
|
@@ -4854,110 +6029,6 @@ async function saveStatusHooks() {
|
|
|
4854
6029
|
}
|
|
4855
6030
|
}
|
|
4856
6031
|
|
|
4857
|
-
// ── Task Tier Overrides (3c) ─────────────────────────────────────────
|
|
4858
|
-
// One row per Wall-E task type. Each row has provider+model dropdowns
|
|
4859
|
-
// that write through to /api/setup/task-default on change. The provider
|
|
4860
|
-
// dropdown is filtered to currently-enabled providers; the model
|
|
4861
|
-
// dropdown to that provider's registered models. "— Default —" clears.
|
|
4862
|
-
|
|
4863
|
-
var TASK_TIER_TYPES = [
|
|
4864
|
-
{ id: 'chat', label: 'Chat', hint: 'Wall-E chat tab + general' },
|
|
4865
|
-
{ id: 'think', label: 'Think', hint: 'initiative + reflect loops' },
|
|
4866
|
-
{ id: 'coding', label: 'Coding', hint: 'coding orchestrator' },
|
|
4867
|
-
{ id: 'coding-review', label: 'Coding Review', hint: 'cross-provider critic' },
|
|
4868
|
-
{ id: 'extract', label: 'Extract', hint: 'memory extraction' },
|
|
4869
|
-
{ id: 'embeddings', label: 'Embeddings', hint: 'vector search' },
|
|
4870
|
-
];
|
|
4871
|
-
|
|
4872
|
-
async function loadTaskTiers() {
|
|
4873
|
-
var rows = document.getElementById('setup-tier-rows');
|
|
4874
|
-
if (!rows) return;
|
|
4875
|
-
try {
|
|
4876
|
-
var [defR, modR] = await Promise.all([
|
|
4877
|
-
fetch('/api/setup/task-default').then(function(r) { return r.json(); }),
|
|
4878
|
-
fetch('/api/setup/all-models').then(function(r) { return r.json(); }),
|
|
4879
|
-
]);
|
|
4880
|
-
var defaults = (defR && Array.isArray(defR.defaults)) ? defR.defaults : [];
|
|
4881
|
-
var defaultsByTask = {};
|
|
4882
|
-
for (var d of defaults) defaultsByTask[d.task_type] = d;
|
|
4883
|
-
var allModels = (modR && Array.isArray(modR.models)) ? modR.models : [];
|
|
4884
|
-
|
|
4885
|
-
if (allModels.length === 0) {
|
|
4886
|
-
var empty = '<div style="color:var(--fg-dim);padding:6px 0;font-size:12px;">No models registered yet. Save a provider above first — its model will appear here.</div>';
|
|
4887
|
-
rows.innerHTML = window.DOMPurify ? DOMPurify.sanitize(empty) : empty;
|
|
4888
|
-
return;
|
|
4889
|
-
}
|
|
4890
|
-
|
|
4891
|
-
// Group models by provider for the 2-level picker
|
|
4892
|
-
var providersInUse = {};
|
|
4893
|
-
for (var m of allModels) {
|
|
4894
|
-
if (!providersInUse[m.provider_type]) providersInUse[m.provider_type] = { name: m.provider_name, models: [] };
|
|
4895
|
-
providersInUse[m.provider_type].models.push(m);
|
|
4896
|
-
}
|
|
4897
|
-
|
|
4898
|
-
var html = '';
|
|
4899
|
-
for (var t of TASK_TIER_TYPES) {
|
|
4900
|
-
var current = defaultsByTask[t.id];
|
|
4901
|
-
var currentProvider = current && current.provider_type ? current.provider_type : '';
|
|
4902
|
-
var currentModelId = current && current.model_registry_id ? current.model_registry_id : '';
|
|
4903
|
-
var providerOpts = '<option value="">— Default —</option>';
|
|
4904
|
-
for (var pt in providersInUse) providerOpts += '<option value="' + _escHtml(pt) + '"' + (pt === currentProvider ? ' selected' : '') + '>' + _escHtml(providersInUse[pt].name) + '</option>';
|
|
4905
|
-
var modelOpts = '<option value="">— Default —</option>';
|
|
4906
|
-
var modelsForProvider = currentProvider ? providersInUse[currentProvider]?.models || [] : [];
|
|
4907
|
-
for (var m of modelsForProvider) modelOpts += '<option value="' + _escHtml(m.id) + '"' + (m.id === currentModelId ? ' selected' : '') + '>' + _escHtml(m.display_name) + '</option>';
|
|
4908
|
-
html += '<div class="tier-row" style="display:grid;grid-template-columns:160px 1fr 1fr;align-items:center;gap:10px;padding:6px 0;border-bottom:1px solid var(--border);">'
|
|
4909
|
-
+ '<div><div style="font-size:13px;font-weight:500;">' + _escHtml(t.label) + '</div>'
|
|
4910
|
-
+ '<div style="font-size:11px;color:var(--fg-dim);">' + _escHtml(t.hint) + '</div></div>'
|
|
4911
|
-
+ '<div class="select-wrap"><select data-tier-provider="' + _escHtml(t.id) + '">' + providerOpts + '</select></div>'
|
|
4912
|
-
+ '<div class="select-wrap"><select data-tier-model="' + _escHtml(t.id) + '" ' + (currentProvider ? '' : 'disabled') + '>' + modelOpts + '</select></div>'
|
|
4913
|
-
+ '</div>';
|
|
4914
|
-
}
|
|
4915
|
-
rows.innerHTML = window.DOMPurify ? DOMPurify.sanitize(html, { ADD_ATTR: ['data-tier-provider', 'data-tier-model'] }) : html;
|
|
4916
|
-
|
|
4917
|
-
// Wire provider-change → repopulate models for that row
|
|
4918
|
-
rows.querySelectorAll('select[data-tier-provider]').forEach(function(provSel) {
|
|
4919
|
-
provSel.addEventListener('change', function() {
|
|
4920
|
-
var taskType = provSel.getAttribute('data-tier-provider');
|
|
4921
|
-
var modelSel = rows.querySelector('select[data-tier-model="' + CSS.escape(taskType) + '"]');
|
|
4922
|
-
if (!modelSel) return;
|
|
4923
|
-
var providerType = provSel.value;
|
|
4924
|
-
var modelOpts2 = '<option value="">— Default —</option>';
|
|
4925
|
-
if (providerType && providersInUse[providerType]) {
|
|
4926
|
-
for (var mm of providersInUse[providerType].models) modelOpts2 += '<option value="' + _escHtml(mm.id) + '">' + _escHtml(mm.display_name) + '</option>';
|
|
4927
|
-
modelSel.disabled = false;
|
|
4928
|
-
} else {
|
|
4929
|
-
modelSel.disabled = true;
|
|
4930
|
-
}
|
|
4931
|
-
modelSel.innerHTML = window.DOMPurify ? DOMPurify.sanitize(modelOpts2) : modelOpts2;
|
|
4932
|
-
// Clearing provider clears the override
|
|
4933
|
-
if (!providerType) saveTierDefault(taskType, null);
|
|
4934
|
-
});
|
|
4935
|
-
});
|
|
4936
|
-
// Wire model-change → save default
|
|
4937
|
-
rows.querySelectorAll('select[data-tier-model]').forEach(function(modelSel) {
|
|
4938
|
-
modelSel.addEventListener('change', function() {
|
|
4939
|
-
var taskType = modelSel.getAttribute('data-tier-model');
|
|
4940
|
-
saveTierDefault(taskType, modelSel.value || null);
|
|
4941
|
-
});
|
|
4942
|
-
});
|
|
4943
|
-
} catch (e) {
|
|
4944
|
-
var err = '<div style="color:var(--red);padding:6px 0;font-size:12px;">Failed to load task tiers: ' + _escHtml(e.message) + '</div>';
|
|
4945
|
-
rows.innerHTML = window.DOMPurify ? DOMPurify.sanitize(err) : err;
|
|
4946
|
-
}
|
|
4947
|
-
}
|
|
4948
|
-
|
|
4949
|
-
async function saveTierDefault(taskType, modelRegistryId) {
|
|
4950
|
-
try {
|
|
4951
|
-
var r = await fetch('/api/setup/task-default', {
|
|
4952
|
-
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
4953
|
-
body: JSON.stringify({ task_type: taskType, model_registry_id: modelRegistryId }),
|
|
4954
|
-
});
|
|
4955
|
-
var d = await r.json();
|
|
4956
|
-
if (d.ok) setupToast(taskType + ': ' + (modelRegistryId ? 'override set' : 'cleared'));
|
|
4957
|
-
else setupToast(d.error || 'Failed', 'error');
|
|
4958
|
-
} catch (e) { setupToast(e.message, 'error'); }
|
|
4959
|
-
}
|
|
4960
|
-
|
|
4961
6032
|
// ── Provider card state restore (3b) ──────────────────────────────────
|
|
4962
6033
|
// Pulls /api/setup/providers and reflects toggle / star / auth_method / model
|
|
4963
6034
|
// onto the new pcard UI. Runs after loadStatus so that loadStatus can no-op
|
|
@@ -5015,6 +6086,11 @@ async function loadProviderCardStates() {
|
|
|
5015
6086
|
|
|
5016
6087
|
var _healthPollTimer = null;
|
|
5017
6088
|
var _healthRunningTests = new Set();
|
|
6089
|
+
// Live connection-health (Access page): poll /api/setup/connection-health, drive badges +
|
|
6090
|
+
// Reconnect banner + transition notifications + auto-recovery.
|
|
6091
|
+
var _connectionHealthTimer = null;
|
|
6092
|
+
var _healthByMethod = {};
|
|
6093
|
+
var _lastConnectionHealth = null;
|
|
5018
6094
|
|
|
5019
6095
|
function _healthDotColor(p) {
|
|
5020
6096
|
if (!p.enabled) return 'var(--fg-dim)';
|
|
@@ -5235,8 +6311,12 @@ SETUP.cleanup = function() {
|
|
|
5235
6311
|
_setupMemoryCaptureTimer = null;
|
|
5236
6312
|
clearTimeout(_embedRefreshTimer);
|
|
5237
6313
|
_embedRefreshTimer = null;
|
|
6314
|
+
clearTimeout(_connectedServicesRefreshTimer);
|
|
6315
|
+
_connectedServicesRefreshTimer = null;
|
|
5238
6316
|
clearTimeout(_healthPollTimer);
|
|
5239
6317
|
_healthPollTimer = null;
|
|
6318
|
+
clearTimeout(_pairingRequestsPollTimer);
|
|
6319
|
+
_pairingRequestsPollTimer = null;
|
|
5240
6320
|
};
|
|
5241
6321
|
|
|
5242
6322
|
// Expose functions called from onclick attributes in HTML
|
|
@@ -5256,17 +6336,31 @@ SETUP.reauthGoogleAccount = reauthGoogleAccount;
|
|
|
5256
6336
|
SETUP.testMcpConnection = testMcpConnection;
|
|
5257
6337
|
SETUP.fixMcpConfigs = fixMcpConfigs;
|
|
5258
6338
|
SETUP.saveDataDirs = saveDataDirs;
|
|
6339
|
+
SETUP.saveBackupDir = saveBackupDir;
|
|
6340
|
+
SETUP.resetBackupDir = resetBackupDir;
|
|
6341
|
+
SETUP.previewStorageMigration = previewStorageMigration;
|
|
6342
|
+
SETUP.applyStorageMigration = applyStorageMigration;
|
|
6343
|
+
SETUP.showTab = _setSetupActiveTab;
|
|
5259
6344
|
SETUP.loadNetworkSettings = loadNetworkSettings;
|
|
5260
6345
|
SETUP.saveNetworkSettings = saveNetworkSettings;
|
|
5261
6346
|
SETUP.selectPhoneAccessMethod = selectPhoneAccessMethod;
|
|
6347
|
+
SETUP.triggerReconnect = triggerReconnect;
|
|
6348
|
+
SETUP.loadConnectionHealth = loadConnectionHealth;
|
|
5262
6349
|
SETUP.setupMicrosoftTunnel = setupMicrosoftTunnel;
|
|
5263
6350
|
SETUP.startMicrosoftTunnel = startMicrosoftTunnel;
|
|
5264
6351
|
SETUP.stopMicrosoftTunnel = stopMicrosoftTunnel;
|
|
6352
|
+
SETUP.setMicrosoftTunnelAccessMode = setMicrosoftTunnelAccessMode;
|
|
5265
6353
|
SETUP.startMicrosoftTunnelLogin = startMicrosoftTunnelLogin;
|
|
6354
|
+
SETUP.switchMicrosoftTunnelLogin = switchMicrosoftTunnelLogin;
|
|
6355
|
+
SETUP.setMicrosoftLoginProvider = setMicrosoftLoginProvider;
|
|
6356
|
+
SETUP.tryOtherMicrosoftLoginProvider = tryOtherMicrosoftLoginProvider;
|
|
5266
6357
|
SETUP.toggleMicrosoftKeepAwake = toggleMicrosoftKeepAwake;
|
|
5267
6358
|
SETUP.recoverMicrosoftTunnel = recoverMicrosoftTunnel;
|
|
6359
|
+
SETUP.resetMicrosoftTunnelPrivateAccess = resetMicrosoftTunnelPrivateAccess;
|
|
5268
6360
|
SETUP.probeMicrosoftTunnel = probeMicrosoftTunnel;
|
|
5269
6361
|
SETUP.copyMicrosoftLoginCode = copyMicrosoftLoginCode;
|
|
6362
|
+
SETUP.checkMicrosoftTunnelLogin = checkMicrosoftTunnelLogin;
|
|
6363
|
+
SETUP.regenerateMicrosoftLoginCode = regenerateMicrosoftLoginCode;
|
|
5270
6364
|
SETUP.clearMicrosoftTunnelTraffic = clearMicrosoftTunnelTraffic;
|
|
5271
6365
|
SETUP.applyCloudflareSetup = applyCloudflareSetup;
|
|
5272
6366
|
SETUP.applyTailscaleSetup = applyTailscaleSetup;
|
|
@@ -5275,6 +6369,9 @@ SETUP.cfCopyInstall = cfCopyInstall;
|
|
|
5275
6369
|
SETUP.cfCopyLogin = cfCopyLogin;
|
|
5276
6370
|
SETUP.cfReconfigure = cfReconfigure;
|
|
5277
6371
|
SETUP.loadDevices = loadDevices;
|
|
6372
|
+
SETUP.loadPairingRequests = loadPairingRequests;
|
|
6373
|
+
SETUP.approvePairingRequest = approvePairingRequest;
|
|
6374
|
+
SETUP.rejectPairingRequest = rejectPairingRequest;
|
|
5278
6375
|
SETUP.removeDeviceConnection = removeDeviceConnection;
|
|
5279
6376
|
SETUP.revokeDevice = revokeDevice;
|
|
5280
6377
|
SETUP.createDeviceClaim = createDeviceClaim;
|
|
@@ -5288,7 +6385,7 @@ SETUP.saveStatusHooks = saveStatusHooks;
|
|
|
5288
6385
|
|
|
5289
6386
|
window.SETUP = SETUP;
|
|
5290
6387
|
if (document.getElementById('setup-panel')?.classList.contains('active')
|
|
5291
|
-
|| /^#(?:setup(?:&|$)|setup-access$|setup-devices$|devices$)/.test(window.location.hash || '')) {
|
|
6388
|
+
|| /^#(?:setup(?:&|$)|setup-access$|setup-devices$|devices$|backups$)/.test(window.location.hash || '')) {
|
|
5292
6389
|
requestAnimationFrame(function() {
|
|
5293
6390
|
if (window.SETUP === SETUP) SETUP.init();
|
|
5294
6391
|
});
|