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.
Files changed (500) hide show
  1. package/README.md +27 -5
  2. package/package.json +2 -2
  3. package/template/CLAUDE.md +2 -2
  4. package/template/LICENSE +1 -1
  5. package/template/bin/ctm-dev-cleanup.js +24 -3
  6. package/template/bin/ctm-launch.sh +13 -0
  7. package/template/bin/dev.sh +156 -18
  8. package/template/bin/node-bin.sh +84 -0
  9. package/template/bin/pin-node.sh +51 -0
  10. package/template/claude-task-manager/api-prompts.js +1203 -182
  11. package/template/claude-task-manager/api-reviews.js +109 -15
  12. package/template/claude-task-manager/approval-agent.js +1360 -280
  13. package/template/claude-task-manager/bin/restart-ctm.sh +64 -23
  14. package/template/claude-task-manager/bin/storage-migration-supervisor.js +338 -0
  15. package/template/claude-task-manager/db.js +4417 -295
  16. package/template/claude-task-manager/docs/app-update-refresh-protocol.md +69 -0
  17. package/template/claude-task-manager/docs/approval-ai-refinement.md +138 -0
  18. package/template/claude-task-manager/docs/approval-rescue-loop.md +74 -0
  19. package/template/claude-task-manager/docs/codex-operational-warning-health.md +107 -0
  20. package/template/claude-task-manager/docs/codex-resume-state-guard-design.md +17 -12
  21. package/template/claude-task-manager/docs/codex-terminal-render-controller-handoff.md +311 -0
  22. package/template/claude-task-manager/docs/coding-agent-hooks-architecture.md +418 -0
  23. package/template/claude-task-manager/docs/conversation-import-freshness.md +20 -0
  24. package/template/claude-task-manager/docs/google-workspace-auth-health.md +77 -0
  25. package/template/claude-task-manager/docs/image-paste-ux.md +13 -0
  26. package/template/claude-task-manager/docs/ipad-web-preview.md +88 -0
  27. package/template/claude-task-manager/docs/main-loop-offload-architecture.md +66 -0
  28. package/template/claude-task-manager/docs/microsoft-dev-tunnel-phone-access-design.md +274 -519
  29. package/template/claude-task-manager/docs/mobile-live-streaming.md +27 -5
  30. package/template/claude-task-manager/docs/mobile-remote-submission-lifecycle.md +69 -0
  31. package/template/claude-task-manager/docs/phone-access-design.md +53 -15
  32. package/template/claude-task-manager/docs/phone-passkey-identity.md +122 -0
  33. package/template/claude-task-manager/docs/phone-setup.md +3 -0
  34. package/template/claude-task-manager/docs/prompt-editing-tree-design.md +25 -1
  35. package/template/claude-task-manager/docs/remote-desktop-access-design.md +268 -0
  36. package/template/claude-task-manager/docs/restart-lifecycle-architecture.md +95 -0
  37. package/template/claude-task-manager/docs/runtime-work-control-plane.md +53 -0
  38. package/template/claude-task-manager/docs/session-interactive-wait-surfaces.md +38 -0
  39. package/template/claude-task-manager/docs/session-needs-you-dismissal.md +84 -0
  40. package/template/claude-task-manager/docs/session-render-state-management-design.md +91 -3
  41. package/template/claude-task-manager/docs/session-standup-command-center-design.md +25 -1
  42. package/template/claude-task-manager/docs/session-title-authority.md +32 -0
  43. package/template/claude-task-manager/docs/session-workspace-binding.md +33 -0
  44. package/template/claude-task-manager/docs/skill-intent-resolution-design.md +72 -0
  45. package/template/claude-task-manager/docs/walle-mcp-supervisor-health.md +86 -0
  46. package/template/claude-task-manager/docs/walle-relay-phone-access-design.md +24 -15
  47. package/template/claude-task-manager/docs/walle-session-history-hydration.md +114 -0
  48. package/template/claude-task-manager/docs/walle-session-input-queue.md +104 -0
  49. package/template/claude-task-manager/docs/walle-session-model-catalog.md +90 -0
  50. package/template/claude-task-manager/docs/walle-session-model-preferences.md +15 -6
  51. package/template/claude-task-manager/git-utils.js +897 -27
  52. package/template/claude-task-manager/lib/agent-capabilities.js +33 -0
  53. package/template/claude-task-manager/lib/agent-cli-cache.js +37 -7
  54. package/template/claude-task-manager/lib/agent-hooks-installer.js +26 -2
  55. package/template/claude-task-manager/lib/agent-presets.js +17 -1
  56. package/template/claude-task-manager/lib/all-sessions-query.js +108 -0
  57. package/template/claude-task-manager/lib/approval-ai-refinement.js +488 -0
  58. package/template/claude-task-manager/lib/approval-self-adapt.js +168 -0
  59. package/template/claude-task-manager/lib/async-semaphore.js +44 -0
  60. package/template/claude-task-manager/lib/auth-context.js +5 -0
  61. package/template/claude-task-manager/lib/auth-rate-limit.js +47 -4
  62. package/template/claude-task-manager/lib/auth-rules.js +29 -2
  63. package/template/claude-task-manager/lib/auto-approval-verifier.js +129 -16
  64. package/template/claude-task-manager/lib/background-llm.js +144 -17
  65. package/template/claude-task-manager/lib/branch-inventory.js +212 -0
  66. package/template/claude-task-manager/lib/claude-desktop-sessions.js +15 -3
  67. package/template/claude-task-manager/lib/coalesce-sync-frames.js +151 -0
  68. package/template/claude-task-manager/lib/codex-launch-health.js +762 -0
  69. package/template/claude-task-manager/lib/codex-transcript-pager.js +51 -0
  70. package/template/claude-task-manager/lib/codex-zst.js +124 -0
  71. package/template/claude-task-manager/lib/coding-agent-models.js +233 -30
  72. package/template/claude-task-manager/lib/connection-health.js +232 -0
  73. package/template/claude-task-manager/lib/conversation-blob-parser.js +42 -0
  74. package/template/claude-task-manager/lib/conversation-tail-merge.js +89 -26
  75. package/template/claude-task-manager/lib/ctm-session-context-api.js +39 -10
  76. package/template/claude-task-manager/lib/cursor-conversation-store.js +354 -0
  77. package/template/claude-task-manager/lib/db-owner-worker-client.js +315 -0
  78. package/template/claude-task-manager/lib/document-review.js +141 -6
  79. package/template/claude-task-manager/lib/escalation-review.js +152 -0
  80. package/template/claude-task-manager/lib/graceful-shutdown.js +159 -0
  81. package/template/claude-task-manager/lib/headless-term-service.js +678 -0
  82. package/template/claude-task-manager/lib/heavy-worker-fallback.js +38 -0
  83. package/template/claude-task-manager/lib/jsonl-conversation-parser.js +542 -0
  84. package/template/claude-task-manager/lib/jsonl-range-reader.js +112 -0
  85. package/template/claude-task-manager/lib/main-db-census.js +216 -0
  86. package/template/claude-task-manager/lib/message-pagination.js +106 -4
  87. package/template/claude-task-manager/lib/microsoft-dev-tunnel-setup.js +750 -26
  88. package/template/claude-task-manager/lib/mobile-auth-api.js +274 -7
  89. package/template/claude-task-manager/lib/mobile-auth-store.js +592 -10
  90. package/template/claude-task-manager/lib/mobile-notification-dispatcher.js +15 -0
  91. package/template/claude-task-manager/lib/model-overview-brain-fallback.js +311 -0
  92. package/template/claude-task-manager/lib/model-overview-cache.js +141 -0
  93. package/template/claude-task-manager/lib/models-health-routing-notice.js +126 -0
  94. package/template/claude-task-manager/lib/node-pin-guard.js +93 -0
  95. package/template/claude-task-manager/lib/perf-tracker.js +242 -6
  96. package/template/claude-task-manager/lib/permission-match.js +76 -0
  97. package/template/claude-task-manager/lib/permission-sync.js +133 -20
  98. package/template/claude-task-manager/lib/process-title.js +35 -0
  99. package/template/claude-task-manager/lib/prompt-executions-query.js +25 -0
  100. package/template/claude-task-manager/lib/prompt-index-disk-cache.js +44 -0
  101. package/template/claude-task-manager/lib/prompt-intent.js +132 -0
  102. package/template/claude-task-manager/lib/provider-user-context.js +34 -0
  103. package/template/claude-task-manager/lib/read-pool-client.js +313 -0
  104. package/template/claude-task-manager/lib/readpool-breaker.js +31 -0
  105. package/template/claude-task-manager/lib/recent-sessions-breaker.js +12 -0
  106. package/template/claude-task-manager/lib/remote-feedback-client.js +72 -0
  107. package/template/claude-task-manager/lib/remote-relay-protocol.js +37 -4
  108. package/template/claude-task-manager/lib/remote-relay-store.js +159 -0
  109. package/template/claude-task-manager/lib/remote-submission-observer.js +278 -0
  110. package/template/claude-task-manager/lib/restart-guard.js +109 -0
  111. package/template/claude-task-manager/lib/restore-interruption-detector.js +439 -0
  112. package/template/claude-task-manager/lib/restore-policy.js +13 -0
  113. package/template/claude-task-manager/lib/restore-resume-batch.js +74 -0
  114. package/template/claude-task-manager/lib/restore-runtime.js +68 -0
  115. package/template/claude-task-manager/lib/restore-storm.js +34 -0
  116. package/template/claude-task-manager/lib/resume-cwd.js +36 -0
  117. package/template/claude-task-manager/lib/resume-preflight.js +313 -0
  118. package/template/claude-task-manager/lib/runtime-work-registry.js +444 -0
  119. package/template/claude-task-manager/lib/sanitize-openai-auth.js +31 -0
  120. package/template/claude-task-manager/lib/scheduler.js +21 -1
  121. package/template/claude-task-manager/lib/scrollback-snapshot-store.js +159 -0
  122. package/template/claude-task-manager/lib/serial-task-queue.js +64 -0
  123. package/template/claude-task-manager/lib/server-listeners.js +239 -0
  124. package/template/claude-task-manager/lib/session-capture.js +42 -7
  125. package/template/claude-task-manager/lib/session-content-backfill.js +131 -0
  126. package/template/claude-task-manager/lib/session-history.js +388 -43
  127. package/template/claude-task-manager/lib/session-host-manager.js +287 -0
  128. package/template/claude-task-manager/lib/session-image-refs.js +209 -0
  129. package/template/claude-task-manager/lib/session-jobs.js +399 -59
  130. package/template/claude-task-manager/lib/session-prompt-index.js +137 -0
  131. package/template/claude-task-manager/lib/session-restore.js +53 -0
  132. package/template/claude-task-manager/lib/session-standup.js +123 -23
  133. package/template/claude-task-manager/lib/session-state-bus.js +14 -0
  134. package/template/claude-task-manager/lib/session-stream.js +64 -16
  135. package/template/claude-task-manager/lib/session-timeline-summary.js +260 -0
  136. package/template/claude-task-manager/lib/session-token-usage.js +494 -0
  137. package/template/claude-task-manager/lib/session-workspace-binding.js +356 -0
  138. package/template/claude-task-manager/lib/setup-network-config.js +9 -0
  139. package/template/claude-task-manager/lib/size-cap.js +45 -0
  140. package/template/claude-task-manager/lib/size-cap.test.js +62 -0
  141. package/template/claude-task-manager/lib/skill-autocomplete.js +180 -1
  142. package/template/claude-task-manager/lib/skill-intent-resolver.js +304 -0
  143. package/template/claude-task-manager/lib/sqlite-driver.js +19 -3
  144. package/template/claude-task-manager/lib/standup-attention.js +7 -3
  145. package/template/claude-task-manager/lib/status-authority.js +39 -0
  146. package/template/claude-task-manager/lib/status-hooks.js +4 -0
  147. package/template/claude-task-manager/lib/storage-migration.js +235 -0
  148. package/template/claude-task-manager/lib/structured-capture.js +298 -0
  149. package/template/claude-task-manager/lib/sync-io-census.js +163 -0
  150. package/template/claude-task-manager/lib/tailscale-setup.js +6 -0
  151. package/template/claude-task-manager/lib/terminal-activity-evidence.js +33 -0
  152. package/template/claude-task-manager/lib/terminal-choice.js +364 -0
  153. package/template/claude-task-manager/lib/terminal-control-sanitize.js +17 -0
  154. package/template/claude-task-manager/lib/terminal-fingerprint.js +48 -0
  155. package/template/claude-task-manager/lib/terminal-output-flush.js +84 -0
  156. package/template/claude-task-manager/lib/timeline-order.js +122 -0
  157. package/template/claude-task-manager/lib/transcript-store.js +348 -43
  158. package/template/claude-task-manager/lib/transport-security.js +84 -1
  159. package/template/claude-task-manager/lib/wait-state.js +184 -0
  160. package/template/claude-task-manager/lib/walle-client.js +47 -5
  161. package/template/claude-task-manager/lib/walle-ctm-history.js +564 -4
  162. package/template/claude-task-manager/lib/walle-external-actions.js +135 -16
  163. package/template/claude-task-manager/lib/walle-history-hydration.js +46 -0
  164. package/template/claude-task-manager/lib/walle-native-health.js +403 -0
  165. package/template/claude-task-manager/lib/walle-repair.js +701 -0
  166. package/template/claude-task-manager/lib/walle-session-cache.js +109 -0
  167. package/template/claude-task-manager/lib/walle-session-context.js +57 -21
  168. package/template/claude-task-manager/lib/walle-session-model-catalog.js +34 -0
  169. package/template/claude-task-manager/lib/walle-supervisor.js +539 -63
  170. package/template/claude-task-manager/lib/walle-transcript.js +52 -0
  171. package/template/claude-task-manager/lib/worktree-active-sync.js +11 -7
  172. package/template/claude-task-manager/lib/worktree-cwd.js +32 -1
  173. package/template/claude-task-manager/package.json +1 -1
  174. package/template/claude-task-manager/prompt-harvest.js +89 -66
  175. package/template/claude-task-manager/providers/claude-code.js +51 -3
  176. package/template/claude-task-manager/providers/cursor.js +140 -45
  177. package/template/claude-task-manager/public/css/reviews.css +551 -61
  178. package/template/claude-task-manager/public/css/setup.css +191 -0
  179. package/template/claude-task-manager/public/css/walle-session.css +865 -10
  180. package/template/claude-task-manager/public/css/walle.css +154 -0
  181. package/template/claude-task-manager/public/designs/ai-providers-consolidation-v2.html +830 -0
  182. package/template/claude-task-manager/public/index.html +18516 -2058
  183. package/template/claude-task-manager/public/ipad.html +363 -0
  184. package/template/claude-task-manager/public/js/document-review-links.js +301 -0
  185. package/template/claude-task-manager/public/js/image-normalize.js +69 -36
  186. package/template/claude-task-manager/public/js/message-renderer.js +1265 -77
  187. package/template/claude-task-manager/public/js/prompts.js +66 -29
  188. package/template/claude-task-manager/public/js/reviews.js +901 -133
  189. package/template/claude-task-manager/public/js/session-activity-utils.js +11 -1
  190. package/template/claude-task-manager/public/js/session-search-utils.js +94 -10
  191. package/template/claude-task-manager/public/js/session-status-precedence.js +23 -5
  192. package/template/claude-task-manager/public/js/setup.js +1273 -176
  193. package/template/claude-task-manager/public/js/stream-view.js +691 -73
  194. package/template/claude-task-manager/public/js/terminal-reconciler.js +210 -0
  195. package/template/claude-task-manager/public/js/walle-session.js +2455 -158
  196. package/template/claude-task-manager/public/js/walle.js +455 -28
  197. package/template/claude-task-manager/public/m/app.css +2909 -262
  198. package/template/claude-task-manager/public/m/app.js +6601 -398
  199. package/template/claude-task-manager/public/m/claim.html +224 -17
  200. package/template/claude-task-manager/public/m/index.html +117 -21
  201. package/template/claude-task-manager/public/m/sw.js +3 -1
  202. package/template/claude-task-manager/public/manifest.json +2 -2
  203. package/template/claude-task-manager/public/prompts.html +30 -14
  204. package/template/claude-task-manager/queue-engine.js +507 -28
  205. package/template/claude-task-manager/scripts/repair-claude-session-images.js +27 -8
  206. package/template/claude-task-manager/server.js +14341 -2197
  207. package/template/claude-task-manager/session-integrity.js +160 -18
  208. package/template/claude-task-manager/session-search-ranking.js +1 -0
  209. package/template/claude-task-manager/session-utils.js +25 -5
  210. package/template/claude-task-manager/workers/approval-blocklist.js +96 -6
  211. package/template/claude-task-manager/workers/approval-widget-validator.js +14 -8
  212. package/template/claude-task-manager/workers/conversation-import-worker.js +11 -50
  213. package/template/claude-task-manager/workers/db-owner-worker.js +386 -0
  214. package/template/claude-task-manager/workers/harvest-worker.js +9 -55
  215. package/template/claude-task-manager/workers/headless-term-worker.js +9 -530
  216. package/template/claude-task-manager/workers/read-pool-worker.js +387 -0
  217. package/template/claude-task-manager/workers/scrollback-worker.js +11 -72
  218. package/template/claude-task-manager/workers/session-host-process.js +146 -0
  219. package/template/claude-task-manager/workers/session-integrity-worker.js +10 -54
  220. package/template/claude-task-manager/workers/state-detectors/base.js +18 -1
  221. package/template/claude-task-manager/workers/state-detectors/claude-code.js +182 -9
  222. package/template/claude-task-manager/workers/state-detectors/codex.js +150 -2
  223. package/template/claude-task-manager/workers/state-detectors/cursor.js +127 -0
  224. package/template/claude-task-manager/workers/state-detectors/gemini.js +21 -0
  225. package/template/claude-task-manager/workers/state-detectors/index.js +29 -0
  226. package/template/claude-task-manager/workers/state-detectors/opencode.js +103 -0
  227. package/template/docs/design/markdown-review-pane.md +206 -0
  228. package/template/docs/designs/2026-05-17-portkey-gateway-provider-ux.md +129 -38
  229. package/template/docs/designs/2026-05-20-mobile-worktree-finish-command.md +27 -0
  230. package/template/docs/designs/2026-05-22-ai-configuration-consolidation.md +248 -0
  231. package/template/docs/designs/ai-configuration-consolidation-mock.html +812 -0
  232. package/template/docs/private-memory-and-pii-policy.md +69 -0
  233. package/template/package.json +2 -1
  234. package/template/scripts/check-private-data.js +201 -0
  235. package/template/shared/sqlite-owner-guard.js +30 -0
  236. package/template/shared/sqlite-owner-write-queue.js +225 -0
  237. package/template/shared/sqlite-storage-policy.js +111 -0
  238. package/template/shared/sqlite-write-lock.js +428 -0
  239. package/template/wall-e/agent-runners/claude-code.js +5 -0
  240. package/template/wall-e/agent.js +166 -22
  241. package/template/wall-e/api-walle.js +524 -70
  242. package/template/wall-e/auth/provider-flows.js +11 -1
  243. package/template/wall-e/bin/walle-mcp-stdio.js +341 -17
  244. package/template/wall-e/brain.js +1614 -141
  245. package/template/wall-e/chat/attachment-blocks.js +96 -0
  246. package/template/wall-e/chat/attachments.js +2 -1
  247. package/template/wall-e/chat/capability-resolver.js +7 -7
  248. package/template/wall-e/chat/context-messages.js +28 -0
  249. package/template/wall-e/chat/conversation-frame.js +630 -0
  250. package/template/wall-e/chat/provider-messages.js +125 -0
  251. package/template/wall-e/chat.js +1002 -233
  252. package/template/wall-e/coding/acceptance-contract.js +170 -0
  253. package/template/wall-e/coding/acp-adapter.js +1 -1
  254. package/template/wall-e/coding/agent-catalog.js +3 -0
  255. package/template/wall-e/coding/artifact-store.js +93 -0
  256. package/template/wall-e/coding/capability-router.js +120 -0
  257. package/template/wall-e/coding/coding-run-controller.js +423 -0
  258. package/template/wall-e/coding/compaction-service.js +157 -12
  259. package/template/wall-e/coding/frontend-verification.js +258 -0
  260. package/template/wall-e/coding/lifecycle-hooks.js +75 -0
  261. package/template/wall-e/coding/local-preview-contract.js +157 -0
  262. package/template/wall-e/coding/permission-service.js +57 -13
  263. package/template/wall-e/coding/prompt-bundle.js +19 -1
  264. package/template/wall-e/coding/prompt-section-registry.js +227 -0
  265. package/template/wall-e/coding/provider-compat.js +15 -0
  266. package/template/wall-e/coding/runtime-events.js +224 -0
  267. package/template/wall-e/coding/runtime-mode.js +3 -0
  268. package/template/wall-e/coding/side-git-snapshot.js +160 -4
  269. package/template/wall-e/coding/snapshot-service.js +143 -1
  270. package/template/wall-e/coding/stream-processor.js +388 -34
  271. package/template/wall-e/coding/task-tool.js +141 -4
  272. package/template/wall-e/coding/tool-execution-controller.js +365 -0
  273. package/template/wall-e/coding/tool-registry.js +43 -5
  274. package/template/wall-e/coding/user-hooks.js +217 -0
  275. package/template/wall-e/coding-orchestrator.js +1330 -221
  276. package/template/wall-e/coding-prompts.js +20 -4
  277. package/template/wall-e/context/context-builder.js +15 -2
  278. package/template/wall-e/decision/confidence.js +1 -1
  279. package/template/wall-e/docs/coding-acceptance-contract.md +41 -0
  280. package/template/wall-e/docs/external-action-controller.md +26 -6
  281. package/template/wall-e/docs/telemetry-lifecycle.md +8 -2
  282. package/template/wall-e/embeddings.js +591 -53
  283. package/template/wall-e/external-action-controller.js +12 -0
  284. package/template/wall-e/http/auth.js +1 -0
  285. package/template/wall-e/http/chat-api.js +46 -11
  286. package/template/wall-e/http/model-admin.js +836 -34
  287. package/template/wall-e/lib/boot-profile.js +88 -0
  288. package/template/wall-e/lib/event-loop-monitor.js +93 -0
  289. package/template/wall-e/lib/service-health.js +194 -0
  290. package/template/wall-e/llm/anthropic.js +130 -5
  291. package/template/wall-e/llm/client.js +266 -63
  292. package/template/wall-e/llm/default-fallback.js +382 -0
  293. package/template/wall-e/llm/health.js +19 -0
  294. package/template/wall-e/llm/message-guard.js +78 -0
  295. package/template/wall-e/llm/model-catalog.js +252 -1
  296. package/template/wall-e/llm/openai.js +26 -4
  297. package/template/wall-e/llm/portkey-sync.js +654 -0
  298. package/template/wall-e/llm/provider-error.js +30 -2
  299. package/template/wall-e/llm/registry.js +5 -1
  300. package/template/wall-e/llm/request-compat.js +67 -0
  301. package/template/wall-e/loops/backfill.js +79 -23
  302. package/template/wall-e/loops/brain-optimize.js +67 -0
  303. package/template/wall-e/loops/ingest.js +25 -10
  304. package/template/wall-e/loops/question-digest.js +160 -0
  305. package/template/wall-e/loops/reflect.js +6 -4
  306. package/template/wall-e/loops/think.js +39 -12
  307. package/template/wall-e/mcp-server.js +318 -36
  308. package/template/wall-e/memory/ctm-context-client.js +52 -14
  309. package/template/wall-e/memory/ctm-operational-context.js +237 -0
  310. package/template/wall-e/memory/ctm-prompt-executions-client.js +128 -0
  311. package/template/wall-e/memory/ctm-session-context.js +111 -63
  312. package/template/wall-e/prompts/coding/deepseek.txt +3 -0
  313. package/template/wall-e/prompts/coding/gemini.txt +6 -0
  314. package/template/wall-e/prompts/coding/gpt.txt +6 -0
  315. package/template/wall-e/prompts/coding/local.txt +7 -0
  316. package/template/wall-e/runtime/decision-hooks.js +115 -0
  317. package/template/wall-e/runtime/devbox-gateway.js +82 -8
  318. package/template/wall-e/runtime/prompt-manifest.js +86 -0
  319. package/template/wall-e/runtime/tool-executor.js +269 -0
  320. package/template/wall-e/runtime/tool-result-envelope.js +138 -0
  321. package/template/wall-e/runtime/transcript-projection.js +60 -0
  322. package/template/wall-e/runtime/walle-runtime.js +224 -0
  323. package/template/wall-e/scripts/db-optimize/migrate.js +162 -0
  324. package/template/wall-e/scripts/db-optimize/recall-eval.js +117 -0
  325. package/template/wall-e/server.js +15 -0
  326. package/template/wall-e/session-files.js +9 -0
  327. package/template/wall-e/skills/_bundled/google-calendar/run.js +1 -1
  328. package/template/wall-e/skills/_bundled/gws-workspace/run.js +1 -1
  329. package/template/wall-e/skills/_bundled/slack-mentions/run.js +76 -6
  330. package/template/wall-e/skills/claude-code-reader.js +7 -3
  331. package/template/wall-e/skills/script-skill-runner.js +10 -0
  332. package/template/wall-e/skills/skill-planner.js +38 -0
  333. package/template/wall-e/tools/builtin-middleware.js +19 -9
  334. package/template/wall-e/tools/local-tools.js +1428 -16
  335. package/template/wall-e/tools/permission-checker.js +73 -5
  336. package/template/wall-e/tools/question-manager.js +117 -7
  337. package/template/wall-e/training/harvester.js +12 -28
  338. package/template/wall-e/training/replay.js +25 -80
  339. package/template/website/index.html +10 -10
  340. package/template/wall-e/eval/ab-test.js +0 -203
  341. package/template/wall-e/eval/agent-runner.js +0 -772
  342. package/template/wall-e/eval/agent-scorer.js +0 -461
  343. package/template/wall-e/eval/aggregator.js +0 -414
  344. package/template/wall-e/eval/allowed-test-commands.js +0 -34
  345. package/template/wall-e/eval/benchmark-generator.js +0 -113
  346. package/template/wall-e/eval/benchmarks/chat-eval.json +0 -1662
  347. package/template/wall-e/eval/benchmarks/chat.json +0 -82
  348. package/template/wall-e/eval/benchmarks/coding-agent-real.json +0 -1
  349. package/template/wall-e/eval/benchmarks/coding-agent.json +0 -1581
  350. package/template/wall-e/eval/benchmarks/coding.json +0 -122
  351. package/template/wall-e/eval/benchmarks/memory-retrieval.json +0 -234
  352. package/template/wall-e/eval/benchmarks/reasoning.json +0 -82
  353. package/template/wall-e/eval/benchmarks/swebench-lite-30.json +0 -212
  354. package/template/wall-e/eval/benchmarks.js +0 -669
  355. package/template/wall-e/eval/cc-replay.js +0 -719
  356. package/template/wall-e/eval/chat-eval.js +0 -525
  357. package/template/wall-e/eval/check-keys.js +0 -15
  358. package/template/wall-e/eval/check-providers.js +0 -42
  359. package/template/wall-e/eval/codex-cli-baseline.js +0 -669
  360. package/template/wall-e/eval/coding-agent-real.js +0 -570
  361. package/template/wall-e/eval/context-compactor.js +0 -251
  362. package/template/wall-e/eval/debug-agent003.js +0 -68
  363. package/template/wall-e/eval/diagnostics.js +0 -216
  364. package/template/wall-e/eval/eval-orchestrator.js +0 -642
  365. package/template/wall-e/eval/evaluate.js +0 -202
  366. package/template/wall-e/eval/evaluator.js +0 -373
  367. package/template/wall-e/eval/exporter.js +0 -212
  368. package/template/wall-e/eval/fixtures/express-basic/package.json +0 -9
  369. package/template/wall-e/eval/fixtures/express-basic/server.js +0 -115
  370. package/template/wall-e/eval/fixtures/express-basic/test.js +0 -83
  371. package/template/wall-e/eval/fixtures/express-buggy/package.json +0 -9
  372. package/template/wall-e/eval/fixtures/express-buggy/server.js +0 -113
  373. package/template/wall-e/eval/fixtures/express-buggy/test.js +0 -83
  374. package/template/wall-e/eval/fixtures/express-buggy-items/package.json +0 -9
  375. package/template/wall-e/eval/fixtures/express-buggy-items/server.js +0 -112
  376. package/template/wall-e/eval/fixtures/express-buggy-items/test.js +0 -83
  377. package/template/wall-e/eval/fixtures/express-buggy-search/package.json +0 -9
  378. package/template/wall-e/eval/fixtures/express-buggy-search/server.js +0 -121
  379. package/template/wall-e/eval/fixtures/express-buggy-search/test.js +0 -83
  380. package/template/wall-e/eval/fixtures/express-rename-data/data.js +0 -34
  381. package/template/wall-e/eval/fixtures/express-rename-data/package.json +0 -9
  382. package/template/wall-e/eval/fixtures/express-rename-data/server.js +0 -97
  383. package/template/wall-e/eval/fixtures/express-rename-data/test.js +0 -88
  384. package/template/wall-e/eval/fixtures/express-xss/package.json +0 -12
  385. package/template/wall-e/eval/fixtures/express-xss/server.js +0 -90
  386. package/template/wall-e/eval/fixtures/express-xss/test.js +0 -67
  387. package/template/wall-e/eval/fixtures/express-xss/views/profile.ejs +0 -9
  388. package/template/wall-e/eval/fixtures/fullstack-app/config/default.js +0 -9
  389. package/template/wall-e/eval/fixtures/fullstack-app/config/test.js +0 -13
  390. package/template/wall-e/eval/fixtures/fullstack-app/package.json +0 -11
  391. package/template/wall-e/eval/fixtures/fullstack-app/public/css/style.css +0 -137
  392. package/template/wall-e/eval/fixtures/fullstack-app/public/index.html +0 -46
  393. package/template/wall-e/eval/fixtures/fullstack-app/public/js/app.js +0 -121
  394. package/template/wall-e/eval/fixtures/fullstack-app/public/js/auth.js +0 -71
  395. package/template/wall-e/eval/fixtures/fullstack-app/public/js/items.js +0 -80
  396. package/template/wall-e/eval/fixtures/fullstack-app/public/js/users.js +0 -46
  397. package/template/wall-e/eval/fixtures/fullstack-app/public/login.html +0 -45
  398. package/template/wall-e/eval/fixtures/fullstack-app/public/register.html +0 -38
  399. package/template/wall-e/eval/fixtures/fullstack-app/scripts/migrate.js +0 -23
  400. package/template/wall-e/eval/fixtures/fullstack-app/scripts/seed.js +0 -46
  401. package/template/wall-e/eval/fixtures/fullstack-app/server/db.js +0 -99
  402. package/template/wall-e/eval/fixtures/fullstack-app/server/index.js +0 -94
  403. package/template/wall-e/eval/fixtures/fullstack-app/server/middleware/auth.js +0 -19
  404. package/template/wall-e/eval/fixtures/fullstack-app/server/middleware/logger.js +0 -19
  405. package/template/wall-e/eval/fixtures/fullstack-app/server/router.js +0 -50
  406. package/template/wall-e/eval/fixtures/fullstack-app/server/routes/auth.js +0 -69
  407. package/template/wall-e/eval/fixtures/fullstack-app/server/routes/health.js +0 -23
  408. package/template/wall-e/eval/fixtures/fullstack-app/server/routes/items.js +0 -88
  409. package/template/wall-e/eval/fixtures/fullstack-app/server/routes/users.js +0 -75
  410. package/template/wall-e/eval/fixtures/fullstack-app/server/test.js +0 -198
  411. package/template/wall-e/eval/fixtures/fullstack-app/server/utils/response.js +0 -34
  412. package/template/wall-e/eval/fixtures/fullstack-app/server/utils/validate.js +0 -26
  413. package/template/wall-e/eval/fixtures/fullstack-app/server.js +0 -8
  414. package/template/wall-e/eval/fixtures/fullstack-app/test.js +0 -12
  415. package/template/wall-e/eval/fixtures/monorepo-basic/package.json +0 -8
  416. package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/data.js +0 -58
  417. package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/middleware.js +0 -46
  418. package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/package.json +0 -8
  419. package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/routes.js +0 -64
  420. package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/server.js +0 -56
  421. package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/test.js +0 -116
  422. package/template/wall-e/eval/fixtures/monorepo-basic/packages/cli/commands.js +0 -61
  423. package/template/wall-e/eval/fixtures/monorepo-basic/packages/cli/index.js +0 -62
  424. package/template/wall-e/eval/fixtures/monorepo-basic/packages/cli/output.js +0 -43
  425. package/template/wall-e/eval/fixtures/monorepo-basic/packages/cli/package.json +0 -11
  426. package/template/wall-e/eval/fixtures/monorepo-basic/packages/cli/test.js +0 -44
  427. package/template/wall-e/eval/fixtures/monorepo-basic/packages/shared/formatters.js +0 -43
  428. package/template/wall-e/eval/fixtures/monorepo-basic/packages/shared/index.js +0 -12
  429. package/template/wall-e/eval/fixtures/monorepo-basic/packages/shared/package.json +0 -5
  430. package/template/wall-e/eval/fixtures/monorepo-basic/packages/shared/test.js +0 -55
  431. package/template/wall-e/eval/fixtures/monorepo-basic/packages/shared/validators.js +0 -29
  432. package/template/wall-e/eval/fixtures/monorepo-basic/test.js +0 -46
  433. package/template/wall-e/eval/fixtures/node-cli/index.js +0 -78
  434. package/template/wall-e/eval/fixtures/node-cli/package.json +0 -10
  435. package/template/wall-e/eval/fixtures/node-cli/test.js +0 -57
  436. package/template/wall-e/eval/fixtures/node-typed/package.json +0 -8
  437. package/template/wall-e/eval/fixtures/node-typed/src/handlers.js +0 -31
  438. package/template/wall-e/eval/fixtures/node-typed/src/utils.js +0 -33
  439. package/template/wall-e/eval/fixtures/node-typed/test.js +0 -36
  440. package/template/wall-e/eval/fixtures/python-flask/app.py +0 -14
  441. package/template/wall-e/eval/fixtures/python-flask/requirements.txt +0 -2
  442. package/template/wall-e/eval/fixtures/python-flask/test_app.py +0 -25
  443. package/template/wall-e/eval/fixtures/wall-e-subset/brain.js +0 -105
  444. package/template/wall-e/eval/fixtures/wall-e-subset/eval/aggregator.js +0 -101
  445. package/template/wall-e/eval/fixtures/wall-e-subset/eval/benchmarks/chat.json +0 -20
  446. package/template/wall-e/eval/fixtures/wall-e-subset/eval/benchmarks/coding.json +0 -32
  447. package/template/wall-e/eval/fixtures/wall-e-subset/eval/benchmarks.js +0 -64
  448. package/template/wall-e/eval/fixtures/wall-e-subset/eval/fixtures/simple-project/package.json +0 -6
  449. package/template/wall-e/eval/fixtures/wall-e-subset/eval/fixtures/simple-project/server.js +0 -31
  450. package/template/wall-e/eval/fixtures/wall-e-subset/eval/fixtures/simple-project/test.js +0 -18
  451. package/template/wall-e/eval/fixtures/wall-e-subset/eval/fixtures/simple-project/utils.js +0 -34
  452. package/template/wall-e/eval/fixtures/wall-e-subset/eval/runner.js +0 -104
  453. package/template/wall-e/eval/fixtures/wall-e-subset/eval/scorer.js +0 -73
  454. package/template/wall-e/eval/fixtures/wall-e-subset/eval/test.js +0 -134
  455. package/template/wall-e/eval/fixtures/wall-e-subset/llm/client.js +0 -99
  456. package/template/wall-e/eval/fixtures/wall-e-subset/llm/providers.js +0 -63
  457. package/template/wall-e/eval/fixtures/wall-e-subset/llm/test.js +0 -70
  458. package/template/wall-e/eval/fixtures/wall-e-subset/package.json +0 -10
  459. package/template/wall-e/eval/fixtures/wall-e-subset/test.js +0 -86
  460. package/template/wall-e/eval/harvester.js +0 -685
  461. package/template/wall-e/eval/head-to-head.js +0 -388
  462. package/template/wall-e/eval/humaneval-adapter.js +0 -321
  463. package/template/wall-e/eval/list-models.js +0 -31
  464. package/template/wall-e/eval/livecodebench-adapter.js +0 -291
  465. package/template/wall-e/eval/mail-integration.js +0 -443
  466. package/template/wall-e/eval/manifest.js +0 -186
  467. package/template/wall-e/eval/meta-harness/adapters/coding-agent.js +0 -57
  468. package/template/wall-e/eval/meta-harness/bootstrap-snapshot.js +0 -149
  469. package/template/wall-e/eval/meta-harness/candidate-store.js +0 -117
  470. package/template/wall-e/eval/meta-harness/cli.js +0 -86
  471. package/template/wall-e/eval/meta-harness/domain-spec.js +0 -154
  472. package/template/wall-e/eval/meta-harness/domains/coding-agent.domain.json +0 -84
  473. package/template/wall-e/eval/meta-harness/examples/env-bootstrap-candidate.js +0 -29
  474. package/template/wall-e/eval/meta-harness/experience-store.js +0 -174
  475. package/template/wall-e/eval/meta-harness/frontier.js +0 -96
  476. package/template/wall-e/eval/meta-harness/harness-interface.js +0 -90
  477. package/template/wall-e/eval/meta-harness/leakage-guard.js +0 -80
  478. package/template/wall-e/eval/meta-harness/optimizer.js +0 -207
  479. package/template/wall-e/eval/meta-harness/proposer-runner.js +0 -110
  480. package/template/wall-e/eval/meta-harness/reporting.js +0 -58
  481. package/template/wall-e/eval/meta-harness/telemetry.js +0 -27
  482. package/template/wall-e/eval/meta-harness/validation.js +0 -81
  483. package/template/wall-e/eval/promoter.js +0 -228
  484. package/template/wall-e/eval/provider-normalizer.js +0 -33
  485. package/template/wall-e/eval/replay.js +0 -395
  486. package/template/wall-e/eval/run-agent-benchmarks.js +0 -386
  487. package/template/wall-e/eval/run-codex-cli-baseline.js +0 -177
  488. package/template/wall-e/eval/run-coding-agent-real.js +0 -187
  489. package/template/wall-e/eval/run-eval.js +0 -435
  490. package/template/wall-e/eval/run-model-comparison.js +0 -142
  491. package/template/wall-e/eval/session-evaluator.js +0 -187
  492. package/template/wall-e/eval/session-miner.js +0 -207
  493. package/template/wall-e/eval/session-retrieval-benchmark.js +0 -150
  494. package/template/wall-e/eval/session-transcripts.js +0 -509
  495. package/template/wall-e/eval/shadow.js +0 -161
  496. package/template/wall-e/eval/swebench-adapter.js +0 -345
  497. package/template/wall-e/eval/swebench-docker.js +0 -192
  498. package/template/wall-e/eval/train.py +0 -320
  499. package/template/wall-e/eval/trainer.js +0 -232
  500. 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 = 'provider';
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: 'provider',
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 = 'provider';
174
- if (!document.getElementById('setup-section-' + target)) target = 'provider';
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
- return _setupTabFromLocation() || _setupTempMemory.activeTab || _activeSetupTab || 'provider';
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 || 'provider',
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
- var account = _microsoftTunnelAccountText(ms);
2098
- return account
2099
- ? 'If Microsoft asks you to sign in, use this same account: ' + account + '. CTM still requires the pairing QR and device permissions.'
2100
- : 'If Microsoft asks you to sign in, use the same Microsoft or GitHub account shown on this Mac. CTM still requires the pairing QR and device permissions.';
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 = method === 'walle'
2179
- ? (walleReady ? _walleRemoteUsableMobileUrl(_lastNetworkSettings || {}) : 'Hosted relay unavailable')
2180
- : (origin ? origin.replace(/\/+$/, '') + '/m/' : (method === 'microsoft' ? 'Start tunnel first' : ''));
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 Cloudflare Access or Tailscale for phone access.';
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. They are written into the QR claim when it is created.', '');
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 hasLoginFallback = !!(login.login_url || login.device_code);
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') return 'status-ok';
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 private phone URL...';
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') return 'Private Microsoft sign-in is active before CTM.';
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 'If this reports a Microsoft gate, that is expected for a private tunnel. On the phone, sign in with the same account shown above.';
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 'The tunnel is private. This Mac-side check is stopped by Microsoft because it is not a signed-in browser session; your phone should sign in with the same Microsoft/GitHub account, then CTM will load.';
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 GitHub on this Mac. After sign-in finishes, CTM starts the tunnel and creates the phone pairing QR.';
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 = 'Private Microsoft tunnel is running. Open the phone URL, sign in with the same account if asked, then pair with CTM.';
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 private tunnel now, remove stale anonymous access, and create the phone pairing QR.';
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 = ready
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
- availabilityNote.textContent = keepAwake.enabled
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 = ready
2628
- ? 'Private Microsoft access is on. CTM also requires the pairing QR, device token, route permissions, and passkey step-up for high-risk actions.'
2629
- : 'Set Up keeps anonymous access off and resets stale public access before the tunnel is marked ready. Do not share the pairing QR or claim link.';
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
- _setMethodBadge(
2654
- 'setup-method-microsoft-badge',
2655
- msReady ? 'Ready' : (msCanStart ? 'Start' : (ms.installed ? 'Sign in' : 'Install')),
2656
- msReady ? 'ok' : (msCanStart ? 'info' : (ms.installed ? 'warn' : 'missing'))
2657
- );
2658
- _setMethodBadge(
2659
- 'setup-method-walle-badge',
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>&#9888; <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 Cloudflare Access or Tailscale until hosted relay is enabled.';
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. Tailscale and Cloudflare remain available below.';
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 Tailscale-based pairing QR. Use Cloudflare Access fallback for phone access until hosted Walle Relay is enabled.');
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: 'github' }),
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 = _lastNetworkSettings || await loadNetworkSettings() || {};
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
- _setMicrosoftActionStatus('Opening GitHub sign-in. Enter the code shown below on the GitHub page; no phone notification is sent.', '');
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 GitHub page, choose the account there, then click Set Up again.', '');
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 GitHub page; CTM will detect the account after Dev Tunnels finishes.', 'ok');
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('Microsoft tunnel is ready. Use the phone URL or Pair Phone below.', 'ok');
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('Private Microsoft sign-in is active before CTM. On the phone, sign in with the same Microsoft/GitHub account shown here.', 'ok');
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.length ? scopes : ['read', 'respond'];
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 Cloudflare Access/Tailscale for direct phone access.';
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
- container.replaceChildren(_embedDimMsg('No embedding providers available.'));
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
  });