create-walle 0.10.0 → 0.10.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +88 -41
- package/lib/owner-write-safety.js +135 -0
- package/lib/public-runtime.js +13 -5
- package/lib/release-schema.json +3 -2
- package/lib/runtime-closure-policy.json +7 -5
- package/package.json +10 -4
- package/template/bin/deps-doctor.js +1 -1
- package/template/bin/node-bin.sh +11 -0
- package/template/coding-task-manager/api-prompts.js +10 -22
- package/template/coding-task-manager/bin/capture-codex-rollout-records.js +417 -0
- package/template/coding-task-manager/bin/census-codex-lead-history.js +325 -0
- package/template/coding-task-manager/bin/claude-hook.js +7 -3
- package/template/coding-task-manager/bin/claude-table-parity.js +307 -0
- package/template/coding-task-manager/bin/conversation-storage-auto-migration-lib.js +16 -1
- package/template/coding-task-manager/bin/conversation-storage-generation-evidence.js +9 -2
- package/template/coding-task-manager/bin/conversation-storage-service-entry-lib.js +16 -1
- package/template/coding-task-manager/bin/conversation-storage-walle-owner.js +247 -0
- package/template/coding-task-manager/bin/ctm-machine-pair.js +160 -0
- package/template/coding-task-manager/bin/lead-wait-census.js +1348 -0
- package/template/coding-task-manager/bin/repair-foreign-clone.js +564 -0
- package/template/coding-task-manager/bin/repair-resume-echo-titles.js +248 -0
- package/template/coding-task-manager/bin/seam-replay-codex.js +301 -0
- package/template/coding-task-manager/bin/write-lock-bench.js +111 -0
- package/template/coding-task-manager/db.js +705 -286
- package/template/coding-task-manager/git-utils.js +44 -0
- package/template/coding-task-manager/lib/adopted-host-redial.js +4 -0
- package/template/coding-task-manager/lib/agent-presets.js +15 -0
- package/template/coding-task-manager/lib/agent-title-apply.js +88 -0
- package/template/coding-task-manager/lib/agent-title-backfill-read.js +92 -0
- package/template/coding-task-manager/lib/api/host-stats.js +98 -0
- package/template/coding-task-manager/lib/api/ingress.js +35 -12
- package/template/coding-task-manager/lib/api/machine-pairing.js +285 -0
- package/template/coding-task-manager/lib/api/machine-pool.js +307 -0
- package/template/coding-task-manager/lib/api/machines.js +461 -0
- package/template/coding-task-manager/lib/api/misc-ops.js +20 -5
- package/template/coding-task-manager/lib/api/providers.js +53 -6
- package/template/coding-task-manager/lib/api/resume-from-store.js +25 -0
- package/template/coding-task-manager/lib/api/sessions.js +160 -8
- package/template/coding-task-manager/lib/api/transcript-history.js +17 -4
- package/template/coding-task-manager/lib/api/worktrees.js +38 -37
- package/template/coding-task-manager/lib/approval-drift.js +16 -1
- package/template/coding-task-manager/lib/approval-logical-frame.js +5 -1
- package/template/coding-task-manager/lib/approval-scope.js +68 -4
- package/template/coding-task-manager/lib/auth-context.js +32 -2
- package/template/coding-task-manager/lib/auth-rate-limit.js +41 -1
- package/template/coding-task-manager/lib/auth-rules.js +186 -45
- package/template/coding-task-manager/lib/broadcast-serialize.js +2 -1
- package/template/coding-task-manager/lib/browser-session-broker.js +9 -1
- package/template/coding-task-manager/lib/browser-use-adapter.js +33 -2
- package/template/coding-task-manager/lib/claude-markdown-ansi.js +27 -85
- package/template/coding-task-manager/lib/claude-session-title.js +6 -30
- package/template/coding-task-manager/lib/claude-table-layout.js +322 -0
- package/template/coding-task-manager/lib/client-diag-handler.js +25 -0
- package/template/coding-task-manager/lib/codex-config-guard.js +33 -3
- package/template/coding-task-manager/lib/codex-harness-qualification.js +12 -0
- package/template/coding-task-manager/lib/codex-history-terminal-renderer.js +33 -33
- package/template/coding-task-manager/lib/codex-launch-health.js +29 -2
- package/template/coding-task-manager/lib/codex-link-preparation.js +9 -8
- package/template/coding-task-manager/lib/codex-local-links.js +267 -0
- package/template/coding-task-manager/lib/codex-markdown-lists.js +160 -0
- package/template/coding-task-manager/lib/codex-native-history-prefix.js +4 -0
- package/template/coding-task-manager/lib/codex-paths.js +2 -1
- package/template/coding-task-manager/lib/codex-source-handoff.js +186 -0
- package/template/coding-task-manager/lib/coding-agent-models.js +21 -30
- package/template/coding-task-manager/lib/coding-session-model-adapters.js +27 -2
- package/template/coding-task-manager/lib/coding-session-model-projection.js +5 -0
- package/template/coding-task-manager/lib/coding-session-model-runtime.js +1 -2
- package/template/coding-task-manager/lib/coding-session-model-state.js +10 -0
- package/template/coding-task-manager/lib/coding-session-model-sync-state.js +0 -1
- package/template/coding-task-manager/lib/conversation-import/cursor-guard-owner.js +10 -0
- package/template/coding-task-manager/lib/create/resolve-launch-params.js +8 -1
- package/template/coding-task-manager/lib/ctm-boot-lifecycle.js +17 -1
- package/template/coding-task-manager/lib/db-domain-tables.js +7 -0
- package/template/coding-task-manager/lib/db-domains.js +21 -2
- package/template/coding-task-manager/lib/db-owner-op-registry.js +7 -0
- package/template/coding-task-manager/lib/db-owner-worker-client.js +22 -0
- package/template/coding-task-manager/lib/db-schema-source-files.js +1 -0
- package/template/coding-task-manager/lib/feature-manifest.js +1 -3
- package/template/coding-task-manager/lib/fs-watcher.js +3 -2
- package/template/coding-task-manager/lib/harness-history-transition.js +41 -14
- package/template/coding-task-manager/lib/headless-inflight-ops.js +203 -0
- package/template/coding-task-manager/lib/headless-missing-reply.js +42 -0
- package/template/coding-task-manager/lib/headless-term-service.js +60 -23
- package/template/coding-task-manager/lib/history-transition-read-op.js +179 -23
- package/template/coding-task-manager/lib/host-control-dial.js +167 -0
- package/template/coding-task-manager/lib/host-handoff-orchestrator.js +16 -2
- package/template/coding-task-manager/lib/host-handoff.js +51 -13
- package/template/coding-task-manager/lib/host-health-ledger.js +164 -12
- package/template/coding-task-manager/lib/host-self-truth-watchdog.js +4 -0
- package/template/coding-task-manager/lib/host-stats.js +328 -0
- package/template/coding-task-manager/lib/host-threadpool-evacuation.js +160 -0
- package/template/coding-task-manager/lib/host-threadpool-monitor.js +14 -1
- package/template/coding-task-manager/lib/hosted-input-dispatch.js +203 -0
- package/template/coding-task-manager/lib/hosted-sync-worker.js +113 -5
- package/template/coding-task-manager/lib/instance-id-ownership-manifest.js +4 -0
- package/template/coding-task-manager/lib/instance-manifest-runtime.js +3 -0
- package/template/coding-task-manager/lib/instance-manifest.js +13 -1
- package/template/coding-task-manager/lib/instance-sync-status.js +15 -0
- package/template/coding-task-manager/lib/kernel/diag-log.js +12 -0
- package/template/coding-task-manager/lib/launch-model-override.js +160 -1
- package/template/coding-task-manager/lib/launch-plan-worker.js +59 -2
- package/template/coding-task-manager/lib/lead-model-observer.js +17 -2
- package/template/coding-task-manager/lib/lead-model-switch-runtime.js +57 -4
- package/template/coding-task-manager/lib/lead-model-switch.js +11 -2
- package/template/coding-task-manager/lib/lead-model-transition-runtime.js +42 -4
- package/template/coding-task-manager/lib/lead-model-transition-store.js +61 -3
- package/template/coding-task-manager/lib/legacy-draft-cancellation-recovery.js +111 -0
- package/template/coding-task-manager/lib/login-tripwire.js +18 -0
- package/template/coding-task-manager/lib/machine-pairing.js +238 -0
- package/template/coding-task-manager/lib/machine-pool.js +409 -0
- package/template/coding-task-manager/lib/machine-registry.js +749 -0
- package/template/coding-task-manager/lib/machine-self.js +66 -0
- package/template/coding-task-manager/lib/main-thread-lint-exemptions.json +56 -4
- package/template/coding-task-manager/lib/media-journal-control-reader.js +473 -0
- package/template/coding-task-manager/lib/media-journal.js +84 -27
- package/template/coding-task-manager/lib/media-store-options.js +43 -15
- package/template/coding-task-manager/lib/migrations/v56-host-samples-and-machine-pool.js +76 -0
- package/template/coding-task-manager/lib/mirror-feed-sanitize.js +12 -3
- package/template/coding-task-manager/lib/model-execution-recipe-metadata.js +16 -3
- package/template/coding-task-manager/lib/model-proxy/anthropic-face.js +4 -0
- package/template/coding-task-manager/lib/model-proxy/chat-response-translators.js +3 -0
- package/template/coding-task-manager/lib/model-proxy/collaboration-codec.js +94 -0
- package/template/coding-task-manager/lib/model-proxy/handoff.js +494 -0
- package/template/coding-task-manager/lib/model-proxy/lead-worker-picks.js +25 -0
- package/template/coding-task-manager/lib/model-proxy/native-lead-listener.js +61 -7
- package/template/coding-task-manager/lib/model-proxy/proxy-cli.js +200 -0
- package/template/coding-task-manager/lib/model-proxy/proxy-instance-record.js +257 -0
- package/template/coding-task-manager/lib/model-proxy/responses-face.js +24 -2
- package/template/coding-task-manager/lib/model-proxy/stream-accounting.js +205 -0
- package/template/coding-task-manager/lib/model-proxy/tokens.js +78 -0
- package/template/coding-task-manager/lib/model-proxy/upstream-outcome.js +9 -0
- package/template/coding-task-manager/lib/native-history-control.js +62 -14
- package/template/coding-task-manager/lib/native-lead-settings-client.js +2 -1
- package/template/coding-task-manager/lib/native-lead-tls.js +18 -4
- package/template/coding-task-manager/lib/oauth-proxy-supervisor.js +628 -64
- package/template/coding-task-manager/lib/owner-action-manifest-data.json +14 -0
- package/template/coding-task-manager/lib/painted-row-recorder.js +9 -1
- package/template/coding-task-manager/lib/prompt-admission-trace.js +69 -0
- package/template/coding-task-manager/lib/prompt-submission-contract.js +21 -1
- package/template/coding-task-manager/lib/provider-login-broker.js +192 -9
- package/template/coding-task-manager/lib/provider-login-capture.js +153 -0
- package/template/coding-task-manager/lib/queue-draft-status-overlay.js +36 -0
- package/template/coding-task-manager/lib/raw-codex-history-window.js +5 -3
- package/template/coding-task-manager/lib/raw-history-block.js +3 -1
- package/template/coding-task-manager/lib/read-pool-client.js +36 -24
- package/template/coding-task-manager/lib/read-pool-op-registry.js +10 -1
- package/template/coding-task-manager/lib/read-pool-telemetry.js +14 -5
- package/template/coding-task-manager/lib/recent-catalog-query.js +26 -0
- package/template/coding-task-manager/lib/recent-session-catalog-response.js +9 -0
- package/template/coding-task-manager/lib/remote-command-context.js +12 -0
- package/template/coding-task-manager/lib/remote-command-runtime.js +18 -5
- package/template/coding-task-manager/lib/remote-machine-catalog.js +1062 -0
- package/template/coding-task-manager/lib/remote-machine-create.js +693 -0
- package/template/coding-task-manager/lib/remote-machine-link.js +939 -0
- package/template/coding-task-manager/lib/remote-machine-proxy.js +520 -0
- package/template/coding-task-manager/lib/remote-session-forward.js +306 -0
- package/template/coding-task-manager/lib/remote-submission-status.js +6 -0
- package/template/coding-task-manager/lib/restart-outcome-truth.js +20 -3
- package/template/coding-task-manager/lib/restart-preflight.js +9 -2
- package/template/coding-task-manager/lib/restore-backoff-gate.js +10 -2
- package/template/coding-task-manager/lib/restore-prepare-retry.js +12 -5
- package/template/coding-task-manager/lib/restore-resume-batch.js +22 -1
- package/template/coding-task-manager/lib/resume-blocked-wire.js +62 -1
- package/template/coding-task-manager/lib/resume-cwd.js +13 -0
- package/template/coding-task-manager/lib/resume-from-store.js +12 -1
- package/template/coding-task-manager/lib/resume-origin-facts.js +38 -1
- package/template/coding-task-manager/lib/resume-preflight.js +21 -16
- package/template/coding-task-manager/lib/review-repo-admission.js +39 -6
- package/template/coding-task-manager/lib/review-repo-registry.js +17 -0
- package/template/coding-task-manager/lib/root-delivery-runtime.js +72 -15
- package/template/coding-task-manager/lib/root-delivery-selection.js +37 -1
- package/template/coding-task-manager/lib/root-prompt-admission.js +43 -11
- package/template/coding-task-manager/lib/root-prompt-flow.js +175 -38
- package/template/coding-task-manager/lib/scrollback-archive-store.js +7 -4
- package/template/coding-task-manager/lib/server-memory-sampler-runtime.js +10 -0
- package/template/coding-task-manager/lib/server-memory-sampler.js +41 -2
- package/template/coding-task-manager/lib/session-analysis-memory.js +2 -0
- package/template/coding-task-manager/lib/session-create/codex-native-provider-gate.js +218 -0
- package/template/coding-task-manager/lib/session-create/codex-resume-exit-classify.js +52 -0
- package/template/coding-task-manager/lib/session-create/delegate-worker.js +95 -6
- package/template/coding-task-manager/lib/session-create/identity.js +20 -1
- package/template/coding-task-manager/lib/session-create/prepared.js +69 -1
- package/template/coding-task-manager/lib/session-create/provider-launch-context.js +77 -0
- package/template/coding-task-manager/lib/session-create/provider-process.js +38 -39
- package/template/coding-task-manager/lib/session-create/response.js +30 -12
- package/template/coding-task-manager/lib/session-create/transcript.js +13 -4
- package/template/coding-task-manager/lib/session-create/worker-continuation.js +43 -1
- package/template/coding-task-manager/lib/session-create/worker-notices.js +94 -5
- package/template/coding-task-manager/lib/session-create/worker-observability.js +99 -7
- package/template/coding-task-manager/lib/session-create/worker-stop.js +11 -3
- package/template/coding-task-manager/lib/session-create/worker-wait-watcher.js +315 -0
- package/template/coding-task-manager/lib/session-geometry-authority-handler.js +10 -1
- package/template/coding-task-manager/lib/session-host-manager.js +152 -206
- package/template/coding-task-manager/lib/session-jobs.js +17 -9
- package/template/coding-task-manager/lib/session-lifecycle.js +20 -50
- package/template/coding-task-manager/lib/session-model-action-preconditions.js +68 -3
- package/template/coding-task-manager/lib/session-resource-sampler.js +67 -9
- package/template/coding-task-manager/lib/session-standup.js +67 -4
- package/template/coding-task-manager/lib/session-status-ledger.js +42 -3
- package/template/coding-task-manager/lib/session-status-runtime.js +45 -1
- package/template/coding-task-manager/lib/session-status.js +2 -2
- package/template/coding-task-manager/lib/session-title-mirror.js +25 -57
- package/template/coding-task-manager/lib/session-transcript-trash.js +8 -1
- package/template/coding-task-manager/lib/startup-pointer-write.js +57 -0
- package/template/coding-task-manager/lib/state-sync/painted-frame-field.js +11 -0
- package/template/coding-task-manager/lib/storage/coding-session-model-store.js +21 -22
- package/template/coding-task-manager/lib/surviving-owner-bootstrap.js +7 -2
- package/template/coding-task-manager/lib/surviving-owner-recovery.js +2 -1
- package/template/coding-task-manager/lib/terminal-draft-http-status.js +25 -0
- package/template/coding-task-manager/lib/transcript-archiver.js +229 -229
- package/template/coding-task-manager/lib/transcript-frame-cache.js +12 -2
- package/template/coding-task-manager/lib/transcript-guard-job.js +55 -4
- package/template/coding-task-manager/lib/transcript-link.js +77 -0
- package/template/coding-task-manager/lib/transcript-manifest-store.js +8 -4
- package/template/coding-task-manager/lib/transcript-source.js +8 -3
- package/template/coding-task-manager/lib/transcript-store.js +40 -0
- package/template/coding-task-manager/lib/tunnel-lane-governor.js +6 -3
- package/template/coding-task-manager/lib/typing-interaction-trace.js +136 -2
- package/template/coding-task-manager/lib/typing-worker-stages.js +208 -1
- package/template/coding-task-manager/lib/usage-ledger/claude-parse.js +53 -2
- package/template/coding-task-manager/lib/usage-ledger/ingest.js +13 -4
- package/template/coding-task-manager/lib/usage-ledger/read.js +14 -0
- package/template/coding-task-manager/lib/usage-ledger/schema.js +49 -1
- package/template/coding-task-manager/lib/usage-ledger/store.js +337 -6
- package/template/coding-task-manager/lib/usage-ledger/task-read.js +123 -5
- package/template/coding-task-manager/lib/visible-revision-watchdog.js +8 -12
- package/template/coding-task-manager/lib/wake-skip-reasons.js +46 -0
- package/template/coding-task-manager/lib/walle-supervisor.js +98 -14
- package/template/coding-task-manager/lib/worker-failure-classify.js +13 -1
- package/template/coding-task-manager/lib/ws/input.js +1 -0
- package/template/coding-task-manager/lib/ws/state-sync.js +19 -2
- package/template/coding-task-manager/lib/ws/terminal-attach.js +32 -1
- package/template/coding-task-manager/lib/ws-router.js +68 -1
- package/template/coding-task-manager/lib/ws-send-admission.js +22 -4
- package/template/coding-task-manager/oauth-proxy.js +99 -55
- package/template/coding-task-manager/package-lock.json +24 -0
- package/template/coding-task-manager/package.json +3 -0
- package/template/coding-task-manager/protocol/message-table.json +0 -10
- package/template/coding-task-manager/protocol/messages.js +0 -10
- package/template/coding-task-manager/protocol/ws-schema.json +0 -20
- package/template/coding-task-manager/protocol/ws-schema.md +0 -2
- package/template/coding-task-manager/providers/codex-mcp-extract.js +121 -19
- package/template/coding-task-manager/providers/codex-mcp.js +14 -5
- package/template/coding-task-manager/public/css/app.css +279 -2
- package/template/coding-task-manager/public/css/reviews.css +4 -0
- package/template/coding-task-manager/public/index.html +297 -240
- package/template/coding-task-manager/public/js/app/machines-page.js +1252 -0
- package/template/coding-task-manager/public/js/app/models-tab.js +546 -36
- package/template/coding-task-manager/public/js/app/worktrees-panel.js +7 -5
- package/template/coding-task-manager/public/js/background-work.js +13 -7
- package/template/coding-task-manager/public/js/client-health-core.js +81 -0
- package/template/coding-task-manager/public/js/client-health-main.js +61 -0
- package/template/coding-task-manager/public/js/coding-session-model-presentation.js +44 -4
- package/template/coding-task-manager/public/js/core/session-commands.js +676 -0
- package/template/coding-task-manager/public/js/core/session-selectors.js +5 -1
- package/template/coding-task-manager/public/js/history-app-seam.js +26 -2
- package/template/coding-task-manager/public/js/history-painted-rows.js +17 -2
- package/template/coding-task-manager/public/js/history-reader-park.js +13 -1
- package/template/coding-task-manager/public/js/history-scroller.js +187 -9
- package/template/coding-task-manager/public/js/history-transcript-pages.js +5 -5
- package/template/coding-task-manager/public/js/machine-pool-shared.js +436 -0
- package/template/coding-task-manager/public/js/private-state-scope.js +31 -3
- package/template/coding-task-manager/public/js/prompt-submission-client.js +173 -4
- package/template/coding-task-manager/public/js/prompt-submission-tray.js +281 -0
- package/template/coding-task-manager/public/js/protocol.js +0 -10
- package/template/coding-task-manager/public/js/queue-panel-submission-reconcile.js +174 -1
- package/template/coding-task-manager/public/js/reviews.js +7 -3
- package/template/coding-task-manager/public/js/root-draft-lifetime.js +139 -2
- package/template/coding-task-manager/public/js/root-queue-builder-lifetime.js +16 -0
- package/template/coding-task-manager/public/js/row-join.js +18 -3
- package/template/coding-task-manager/public/js/session-model-picker.js +1 -1
- package/template/coding-task-manager/public/js/session-search-utils.js +46 -19
- package/template/coding-task-manager/public/js/session-usage-view.js +1 -0
- package/template/coding-task-manager/public/js/state-sync-client.js +4 -0
- package/template/coding-task-manager/public/js/terminal/runtime-02.js +16 -7
- package/template/coding-task-manager/public/js/terminal/runtime-03.js +41 -0
- package/template/coding-task-manager/public/js/terminal/runtime-04.js +41 -4
- package/template/coding-task-manager/public/js/terminal/runtime-12.js +52 -2
- package/template/coding-task-manager/public/js/terminal/runtime-16.js +12 -12
- package/template/coding-task-manager/public/js/terminal/runtime-18.js +14 -0
- package/template/coding-task-manager/public/js/terminal/runtime-19.js +8 -4
- package/template/coding-task-manager/public/js/terminal/runtime-20.js +210 -73
- package/template/coding-task-manager/public/js/terminal/runtime-21.js +19 -6
- package/template/coding-task-manager/public/js/terminal/runtime-22.js +86 -0
- package/template/coding-task-manager/public/js/terminal-history-viewport.js +1 -3
- package/template/coding-task-manager/public/js/terminal-wheel.js +39 -2
- package/template/coding-task-manager/public/js/walle/chat-conversation.js +11 -3
- package/template/coding-task-manager/public/js/walle-session.js +30 -7
- package/template/coding-task-manager/public/m/app.css +226 -8
- package/template/coding-task-manager/public/m/app.js +502 -9
- package/template/coding-task-manager/public/m/index.html +38 -1
- package/template/coding-task-manager/public/m/machines.js +1313 -0
- package/template/coding-task-manager/public/m/pool-failover.js +391 -0
- package/template/coding-task-manager/public/m/private-queue-actions.js +26 -7
- package/template/coding-task-manager/public/m/sw.js +5 -0
- package/template/coding-task-manager/queue-engine.js +13 -0
- package/template/coding-task-manager/server/approvals.js +5 -6
- package/template/coding-task-manager/server/boot.js +136 -17
- package/template/coding-task-manager/server.js +964 -425
- package/template/coding-task-manager/session-integrity.js +7 -9
- package/template/coding-task-manager/workers/db-owner-worker.js +53 -7
- package/template/coding-task-manager/workers/read-pool-worker.js +17 -6
- package/template/coding-task-manager/workers/session-host-pool-process.js +18 -4
- package/template/coding-task-manager/workers/session-host-process.js +14 -3
- package/template/coding-task-manager/workers/transcript-archive-worker.js +1 -1
- package/template/coding-task-manager/workers/worktree-worker.js +34 -0
- package/template/package-lock.json +2 -2
- package/template/package.json +1 -1
- package/template/scripts/conversation-storage-v2-migration.js +78 -11
- package/template/shared/conversation-storage-v2-contract.js +42 -6
- package/template/shared/fixtures/worker-status-payloads.json +392 -0
- package/template/shared/git-remote-key.js +83 -0
- package/template/shared/host-identity.js +98 -2
- package/template/shared/owner-write-safety.js +135 -0
- package/template/shared/public-runtime.js +13 -5
- package/template/shared/release-schema.json +3 -2
- package/template/shared/report-citations.js +14 -0
- package/template/shared/sqlite-write-lock.js +361 -223
- package/template/shared/storage/contract.js +35 -3
- package/template/wall-e/agent.js +21 -0
- package/template/wall-e/api-walle.js +5 -0
- package/template/wall-e/bin/brain-purge-legacy-ctm.js +3 -2
- package/template/wall-e/bin/walle-mcp-stdio.js +12 -6
- package/template/wall-e/brain/helper-rules-schema.js +129 -0
- package/template/wall-e/brain/migrations.js +51 -22
- package/template/wall-e/brain/model-deployment-facts.js +31 -1
- package/template/wall-e/brain/owner-call-whitelist.js +16 -0
- package/template/wall-e/brain/schema-ensures.js +22 -0
- package/template/wall-e/brain.js +1 -1
- package/template/wall-e/coding/host-capabilities.js +2 -2
- package/template/wall-e/coding/journal-dispatcher.js +51 -0
- package/template/wall-e/coding/session-journal.js +28 -40
- package/template/wall-e/coding-orchestrator.js +23 -26
- package/template/wall-e/fly.toml +1 -1
- package/template/wall-e/http/auth.js +18 -1
- package/template/wall-e/http/blog-views-api.js +318 -0
- package/template/wall-e/http/helper-rules-api.js +321 -0
- package/template/wall-e/http/model-proxy-providers.js +3 -1
- package/template/wall-e/http/telemetry-api.js +12 -0
- package/template/wall-e/lib/blog-view-policy.js +88 -0
- package/template/wall-e/lib/blog-view-store.js +169 -0
- package/template/wall-e/lib/client-config-lock.js +2 -1
- package/template/wall-e/lib/coding-journal-writer-child-client.js +67 -18
- package/template/wall-e/lib/error-kinds.js +10 -1
- package/template/wall-e/lib/feature-manifest.js +1 -1
- package/template/wall-e/lib/helper-rules/cell-state.js +123 -0
- package/template/wall-e/lib/helper-rules/evaluate.js +357 -0
- package/template/wall-e/lib/helper-rules/explore-rubric.md +12 -0
- package/template/wall-e/lib/helper-rules/grade.js +277 -0
- package/template/wall-e/lib/helper-rules/label.js +247 -0
- package/template/wall-e/lib/helper-rules/lead-call-hook.js +522 -0
- package/template/wall-e/lib/helper-rules/store.js +667 -0
- package/template/wall-e/lib/mcp-integration.js +5 -6
- package/template/wall-e/lib/runtime-worker-pool.js +1 -0
- package/template/wall-e/lib/scheduler-worker-jobs.js +6 -0
- package/template/wall-e/lib/telemetry-db-schema.js +37 -0
- package/template/wall-e/lib/telemetry-process-client.js +76 -1
- package/template/wall-e/llm/default-fallback.js +4 -0
- package/template/wall-e/llm/model-catalog.js +13 -4
- package/template/wall-e/llm/model-roles.js +6 -0
- package/template/wall-e/llm/portkey-http.js +150 -0
- package/template/wall-e/llm/portkey-sync.js +15 -145
- package/template/wall-e/llm/worker-mcp-routing.js +61 -0
- package/template/wall-e/llm/worker-route.js +43 -1
- package/template/wall-e/loops/brain-custom-writes.js +45 -0
- package/template/wall-e/mcp-protocol-contract.js +41 -1
- package/template/wall-e/mcp-server.js +13 -2
- package/template/wall-e/mcp-worker-schemas.js +20 -2
- package/template/wall-e/memory/ctm-context-client.js +25 -5
- package/template/wall-e/shared/release-schema.json +3 -2
- package/template/wall-e/shared/sqlite-write-lock.js +361 -223
- package/template/wall-e/shared/storage/contract.js +35 -3
- package/template/wall-e/skills/_templates/delegate-worker/VERSION +1 -1
- package/template/wall-e/skills/_templates/delegate-worker/released-sha256.json +6 -0
- package/template/wall-e/skills/_templates/delegate-worker/templates/claude/SKILL.md +7 -2
- package/template/wall-e/skills/_templates/delegate-worker/templates/codex/SKILL.md +7 -2
- package/template/wall-e/tools/local-tools.js +12 -5
- package/template/wall-e/workers/brain-owner-worker.js +9 -0
- package/template/wall-e/workers/coding-journal-writer-child.js +8 -1
- package/template/wall-e/workers/telemetry-worker.js +8 -0
- package/template/coding-task-manager/lib/giant-freeze-park.js +0 -31
|
@@ -0,0 +1,1313 @@
|
|
|
1
|
+
/* ===================================================================
|
|
2
|
+
CtmPhoneMachines — the phone's machine pool surfaces (WP-6).
|
|
3
|
+
|
|
4
|
+
Design: docs/machine-pool-picker-pairing-dashboard-2026-10-24.html §5 D-1/D-2/D-3/D-4/D-9/D-12,
|
|
5
|
+
P-7, P-8; §6.4 client block; §8 WP-6. Ruling in force: PHONE = DESKTOP PARITY.
|
|
6
|
+
|
|
7
|
+
Two surfaces, both the phone twin of a desktop one:
|
|
8
|
+
1. the Machines page inside More — twin of public/js/app/machines-page.js (This Mac + every
|
|
9
|
+
registry member: same groups, same badges, same sparklines, same actions, same Add dialog,
|
|
10
|
+
Show a code, Leave pool, Join pool, New session here, "Sign in to <member> too");
|
|
11
|
+
2. the picker search sheet for N > 4 — twin of the desktop "All machines (N)…" search card
|
|
12
|
+
(D-1/D-2/D-3: Favorites / Recent / All / Offline, search over displayName/hostname/os/arch/
|
|
13
|
+
url/gpu.name, offline rows disabled with "last seen …", hidden rows never rendered).
|
|
14
|
+
|
|
15
|
+
One copy, not two: every DOM-free presenter (order, search text, groups, load line, sparkline,
|
|
16
|
+
pairing container, card facts) comes from public/js/machine-pool-shared.js, which the phone loads
|
|
17
|
+
before this file. Settings are MACHINE-OWNED (D-4 rev 2): rename / favorite / hide POST
|
|
18
|
+
/api/machine-self on the machine they describe (`?machine=<id>` for a member). Nothing here writes
|
|
19
|
+
localStorage — the only client-side machine state is the P-7 dial cache (public/m/pool-failover.js),
|
|
20
|
+
plus the code being typed into an open sign-in's box, which is held in this module's own memory
|
|
21
|
+
(state.signInDrafts, per machine+agent) because the More page rebuilds the card under it.
|
|
22
|
+
|
|
23
|
+
Classic script, like public/m/model-sheet.js: the app creates it with DOM/transport/clock/timer
|
|
24
|
+
injected, so every presenter is provable in a vm without a browser. It owns exactly ONE timer: the
|
|
25
|
+
bounded sign-in re-read (SIGN_IN_POLL_MS, at most SIGN_IN_POLL_MAX_TICKS ticks) started by a
|
|
26
|
+
sign-in on a machine, which stops as soon as that flow closes, its block leaves the page, or the
|
|
27
|
+
ten-minute cap is reached. Nothing else here schedules work.
|
|
28
|
+
=================================================================== */
|
|
29
|
+
(function (root) {
|
|
30
|
+
'use strict';
|
|
31
|
+
|
|
32
|
+
var STALE_NOTE = 'stats stale';
|
|
33
|
+
var STATUS_ID = 'phone-machines-status';
|
|
34
|
+
// D-12: the same 24 h window the desktop page asks for, so a spark is the same spark on both.
|
|
35
|
+
var HISTORY_MS = 24 * 60 * 60 * 1000;
|
|
36
|
+
// WP-3 fix (C): the box prints its sign-in link ~1 s AFTER the POST answers, so one read is not
|
|
37
|
+
// enough — the block would sit on "Waiting for the sign-in page…" forever. One bounded re-read.
|
|
38
|
+
var SIGN_IN_POLL_MS = 2000;
|
|
39
|
+
var SIGN_IN_POLL_MAX_TICKS = 300;
|
|
40
|
+
var SIGN_IN_OPEN_STATUSES = ['starting', 'running', 'awaiting_user'];
|
|
41
|
+
|
|
42
|
+
function defaultEsc(value) {
|
|
43
|
+
return String(value == null ? '' : value).replace(/[&<>"']/g, function (ch) {
|
|
44
|
+
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[ch];
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function normId(value) {
|
|
49
|
+
return String(value == null ? '' : value).trim();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function machineName(machine) {
|
|
53
|
+
var m = machine || {};
|
|
54
|
+
return String(m.displayName || m.hostname || m.url || m.instanceId || '');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// P-8: the credential the hub dialled the row with. `env` rows are one-way members and carry the
|
|
58
|
+
// "Join pool" action, exactly as on the desktop page.
|
|
59
|
+
function credentialBadge(machine, isSelf) {
|
|
60
|
+
var credential = String((machine && machine.peerCredential) || (isSelf ? 'self' : 'missing'));
|
|
61
|
+
if (isSelf) return { text: 'This Mac', cred: 'self', title: 'This machine runs its own sessions', action: '' };
|
|
62
|
+
if (credential === 'pool') return { text: 'Pool', cred: 'pool', title: 'Dialled with the shared pool secret', action: '' };
|
|
63
|
+
if (credential === 'env') return { text: 'Env token', cred: 'env', title: 'Legacy env token — a one-way member', action: 'join-pool' };
|
|
64
|
+
return { text: 'No credential', cred: 'missing', title: 'No credential this hub can dial it with', action: '' };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
// ── D-1 picker: <select> at N <= 4, search sheet above it ──────────────────────────────────────
|
|
69
|
+
// D-1 counts N = 1 (This Mac) + the visible registry rows. At or below MACHINE_PICKER_CARD_LIMIT the
|
|
70
|
+
// phone keeps the one-tap <select>; above it the picker is the search sheet.
|
|
71
|
+
function pickerMode(machines, selfIncluded) {
|
|
72
|
+
var visible = (Array.isArray(machines) ? machines : []).filter(function (m) {
|
|
73
|
+
return m && m.instanceId && !machineIsHidden(m);
|
|
74
|
+
});
|
|
75
|
+
var n = visible.length + (selfIncluded === false ? 0 : 1);
|
|
76
|
+
return n > MACHINE_PICKER_CARD_LIMIT ? 'search' : 'select';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// D-2/D-3 for the search sheet: the grouped rows a query leaves, in the shared order, with `hidden`
|
|
80
|
+
// rows already dropped by machinePickerGroupRows (so a hidden box can never surface here).
|
|
81
|
+
function sheetGroups(machines, query, now) {
|
|
82
|
+
var needle = String(query == null ? '' : query).trim().toLowerCase();
|
|
83
|
+
var groups = machinePickerGroupRows(machines, now);
|
|
84
|
+
if (!needle) return groups;
|
|
85
|
+
return groups.map(function (group) {
|
|
86
|
+
return {
|
|
87
|
+
id: group.id,
|
|
88
|
+
label: group.label,
|
|
89
|
+
rows: group.rows.filter(function (machine) { return machineSearchText(machine).indexOf(needle) !== -1; }),
|
|
90
|
+
};
|
|
91
|
+
}).filter(function (group) { return group.rows.length > 0; });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// A sheet row. D-3: the sheet lists EVERY row, including the unreachable ones HUB AC-5a keeps out
|
|
95
|
+
// of the picker CARD row — "where is my machine" is exactly the question a search answers, so the
|
|
96
|
+
// offline group is the point of the group set, not an afterthought. A non-reachable row is DISABLED
|
|
97
|
+
// and says so with its last-seen stamp; a reachable one is tappable and carries the D-12 load line.
|
|
98
|
+
// `now` is passed in, so staleness is never judged inside a tested path.
|
|
99
|
+
function sheetRowHtml(machine, selectedId, now) {
|
|
100
|
+
var id = normId(machine && machine.instanceId);
|
|
101
|
+
if (!id) return '';
|
|
102
|
+
var disabled = machineIsOffline(machine);
|
|
103
|
+
var meta = disabled
|
|
104
|
+
? [machineHealthLabel(machine).toLowerCase(), machineLastSeenText(machine)].join(' · ')
|
|
105
|
+
: machineLoadLine(machine, now);
|
|
106
|
+
var selected = id === selectedId;
|
|
107
|
+
return '<button type="button" class="model-picker-option machine-sheet-row' + (selected ? ' is-selected' : '') + '"'
|
|
108
|
+
+ ' role="option" aria-selected="' + (selected ? 'true' : 'false') + '"'
|
|
109
|
+
+ ' data-machine-pick="' + defaultEsc(id) + '"' + (disabled ? ' disabled aria-disabled="true"' : '') + '>'
|
|
110
|
+
+ '<span class="machine-sheet-row-name">' + defaultEsc(machineName(machine)) + '</span>'
|
|
111
|
+
+ (meta ? '<span class="machine-sheet-row-meta">' + defaultEsc(meta) + '</span>' : '')
|
|
112
|
+
+ '</button>';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function sheetListHtml(machines, query, selectedId, now) {
|
|
116
|
+
var groups = sheetGroups(machines, query, now);
|
|
117
|
+
if (!groups.length) return '<p class="machine-sheet-empty">No machine matches that search.</p>';
|
|
118
|
+
return groups.map(function (group) {
|
|
119
|
+
return '<div class="machine-sheet-group" data-group="' + defaultEsc(group.id) + '">'
|
|
120
|
+
+ '<p class="machine-sheet-group-label">' + defaultEsc(group.label) + '</p>'
|
|
121
|
+
+ group.rows.map(function (machine) { return sheetRowHtml(machine, selectedId, now); }).join('')
|
|
122
|
+
+ '</div>';
|
|
123
|
+
}).join('');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function create(deps) {
|
|
127
|
+
var d = deps || {};
|
|
128
|
+
var $ = d.$ || function () { return null; };
|
|
129
|
+
var esc = d.esc || defaultEsc;
|
|
130
|
+
var api = d.api || function () { return Promise.reject(new Error('no api')); };
|
|
131
|
+
var toast = d.toast || function () {};
|
|
132
|
+
var host = d.document || (typeof document !== 'undefined' ? document : null);
|
|
133
|
+
var openNewSession = d.openNewSessionSheet || function () {};
|
|
134
|
+
var confirmImpl = d.confirm || (typeof confirm === 'function' ? confirm : null);
|
|
135
|
+
var clock = d.now || function () { return Date.now(); };
|
|
136
|
+
var at = function () { var value = Number(clock()); return Number.isFinite(value) ? value : 0; };
|
|
137
|
+
// The ONE timer seam (WP-3 fix C). Injected like every other dependency, so a vm spec drives the
|
|
138
|
+
// sign-in re-read tick by tick instead of waiting on a real clock.
|
|
139
|
+
var timers = d.timers || {
|
|
140
|
+
set: function (fn, ms) { return setTimeout(fn, ms); },
|
|
141
|
+
clear: function (id) { if (id != null) clearTimeout(id); },
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
var state = {
|
|
145
|
+
machines: [],
|
|
146
|
+
histories: {},
|
|
147
|
+
self: null, // /api/host/stats for This Mac: { latest, samples, instanceId, urls }
|
|
148
|
+
selfSettings: null, // /api/machine-self: { displayName, favorite, hidden }
|
|
149
|
+
selfInstanceId: '',
|
|
150
|
+
origins: [], // the P-7 dial cache, read through the failover module
|
|
151
|
+
// WP-3: the last machine-scoped login-state entry per '<instanceId>-<agent>', so an open
|
|
152
|
+
// sign-in survives the app's re-renders.
|
|
153
|
+
signIn: {},
|
|
154
|
+
// WP-3 fix round 3 (G): the draft the user has typed into a block's own code box, per the same
|
|
155
|
+
// '<instanceId>-<agent>' key — kept HERE, not only in the block, because the More page rewrites
|
|
156
|
+
// #more-content wholesale BEFORE this module renders into it, so by then the old input (and its
|
|
157
|
+
// draft) is gone; this state is the only record the new card can be painted from.
|
|
158
|
+
signInDrafts: {},
|
|
159
|
+
// WP-3 fix (C): at most ONE re-read handle per '<instanceId>-<agent>'.
|
|
160
|
+
signInTimers: {},
|
|
161
|
+
loading: false,
|
|
162
|
+
loaded: false,
|
|
163
|
+
error: '',
|
|
164
|
+
};
|
|
165
|
+
var dialog = null;
|
|
166
|
+
|
|
167
|
+
function failoverModule() {
|
|
168
|
+
return root.CtmPoolFailover || null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// The P-7 dial cache is the ONLY client-side machine state. A blocked or corrupted cache reads as
|
|
172
|
+
// "this device has reached nothing", which is exactly the safe default for the sign-in row.
|
|
173
|
+
function poolOrigins() {
|
|
174
|
+
var failover = failoverModule();
|
|
175
|
+
if (!failover || typeof failover.readPoolOrigins !== 'function') return [];
|
|
176
|
+
try {
|
|
177
|
+
return failover.readPoolOrigins(d.storage);
|
|
178
|
+
} catch (error) {
|
|
179
|
+
return []; // HANDLE: a blocked dial cache is an empty roster; the page still renders
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ── Machines page markup ───────────────────────────────────────────────────────────────────
|
|
184
|
+
// P-7: a member whose public https origin this device has never reached offers the one-tap
|
|
185
|
+
// sign-in — passkeys are pinned to the exact hostname, so the member's own /m/ runs its flow.
|
|
186
|
+
function signInRowHtml(machine) {
|
|
187
|
+
var origin = machinePoolSignInOrigin(machine, state.origins);
|
|
188
|
+
if (!origin) return '';
|
|
189
|
+
return '<button type="button" class="mp-btn mp-signin-row"'
|
|
190
|
+
+ ' title="' + esc('Opens ' + origin + '/m/ — a passkey is pinned to that hostname') + '"'
|
|
191
|
+
+ ' data-machine-signin="' + esc(origin) + '">'
|
|
192
|
+
+ esc('Sign in to ' + machineName(machine) + ' too')
|
|
193
|
+
+ '<span class="mp-signin-hint">' + esc(origin + '/m/') + '</span>'
|
|
194
|
+
+ '</button>';
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function factRow(label, value) {
|
|
198
|
+
if (!value) return '';
|
|
199
|
+
return '<div class="mp-fact"><span class="mp-fact-label">' + esc(label) + '</span>'
|
|
200
|
+
+ '<span class="mp-fact-value">' + value + '</span></div>';
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function sparkBlock(cpuSeries, memSeries, cpuNow, memNow) {
|
|
204
|
+
return '<div class="mp-sparks">'
|
|
205
|
+
+ '<div class="mp-spark"><span class="mp-spark-label">CPU</span>' + sparklineSvg(cpuSeries)
|
|
206
|
+
+ '<span class="mp-spark-val">' + esc(machineFormatPct(cpuNow)) + '</span></div>'
|
|
207
|
+
+ '<div class="mp-spark"><span class="mp-spark-label">MEM</span>' + sparklineSvg(memSeries)
|
|
208
|
+
+ '<span class="mp-spark-val">' + esc(machineFormatPct(memNow)) + '</span></div>'
|
|
209
|
+
+ '</div>';
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function machineFactsHtml(sample, agents) {
|
|
213
|
+
return ''
|
|
214
|
+
+ factRow('Load', sample && sample.load1 != null ? esc(Number(sample.load1).toFixed(2)) : '')
|
|
215
|
+
+ factRow('Cores', sample && sample.cores != null ? esc(String(sample.cores)) : '')
|
|
216
|
+
+ factRow('Disk free', sample ? esc(machineFormatBytes(sample.diskFreeBytes)) : '')
|
|
217
|
+
+ factRow('GPU', esc(machineGpuText(sample && sample.gpu)))
|
|
218
|
+
+ factRow('Tasks', esc(machineTaskLine(sample && sample.tasks)))
|
|
219
|
+
+ factRow('Agents', esc(machineAgentText(agents)));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ── WP-3 sign-in: a coding agent ON a pooled machine, signed in from this phone ───────────────
|
|
223
|
+
// docs/remote-agent-sign-in-from-hub-browser-2026-10-25.html §5 D-7/D-9, §6, §9 S-13 — PHONE =
|
|
224
|
+
// DESKTOP PARITY, so the markup carries the SAME `data-*` names as the desktop card
|
|
225
|
+
// (data-machine-sign-in / data-machine-agent / data-machine-sign-in-block /
|
|
226
|
+
// data-open-sign-in-page / data-sign-in-code / data-sign-in-code-submit) and the three provider
|
|
227
|
+
// routes ride the same `?machine=<id>` proxy. Fix (C): the POST's own follow-up read is NOT
|
|
228
|
+
// enough — the box prints its link about a second later — so one bounded re-read per machine (see
|
|
229
|
+
// SIGN_IN_POLL_MS) runs until the flow closes; the entry also persists in `state.signIn` across
|
|
230
|
+
// the app's re-renders, so a repaint never loses the flow it already has.
|
|
231
|
+
var SIGN_IN_AGENTS = ['claude', 'codex'];
|
|
232
|
+
|
|
233
|
+
// The desktop's mapped copy, verbatim (models-tab.js `_modelsProviderLoginErrorText`): the phone
|
|
234
|
+
// must not print a raw refusal id where the card prints what the user can do about it.
|
|
235
|
+
function signInErrorText(errorId) {
|
|
236
|
+
var id = String(errorId === null || errorId === undefined ? '' : errorId);
|
|
237
|
+
if (id === 'code_format_invalid') return "That doesn't look like a sign-in code. Paste the code, or the page address, from the sign-in page.";
|
|
238
|
+
if (id === 'code_state_mismatch') return 'That code is from a different sign-in. Use the link above and try again.';
|
|
239
|
+
if (id === 'code_from_loopback_redirect') return 'That page came from a localhost link. Use the Open sign-in page link above.';
|
|
240
|
+
if (id === 'login_tab_gone' || id === 'provider_login_not_open') return 'The sign-in tab closed. Start Sign in again.';
|
|
241
|
+
if (id === 'code_entry_not_supported') return 'This sign-in does not take a code here.';
|
|
242
|
+
return 'Could not send the code (' + id + ').';
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// The clipboard is the browser's, and its refusal is a label, never an exception escaping to the
|
|
246
|
+
// console (the desktop card's own rule). No timer: the label holds until the next render.
|
|
247
|
+
function copySignInCode(code, btn) {
|
|
248
|
+
var label = function (text) { if (btn) btn.textContent = text; };
|
|
249
|
+
try {
|
|
250
|
+
navigator.clipboard.writeText(String(code || '')).then(
|
|
251
|
+
function () { label('Copied'); },
|
|
252
|
+
function () { label('Copy failed'); },
|
|
253
|
+
);
|
|
254
|
+
} catch (error) {
|
|
255
|
+
// HANDLE: no clipboard (or no navigator) on this surface — the button says so and stops.
|
|
256
|
+
label('Copy failed');
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// `signed_out` only — never `signed_in`, never `unknown` (P3: a box that never answered is not
|
|
261
|
+
// told it is signed out).
|
|
262
|
+
function signedOutAgents(machine) {
|
|
263
|
+
var agents = machine && machine.agents;
|
|
264
|
+
if (!agents || typeof agents !== 'object') return [];
|
|
265
|
+
return SIGN_IN_AGENTS.filter(function (agent) { return String(agents[agent] || '') === 'signed_out'; });
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// The Sign in button plus the block it fills, per signed-out agent, under the Agents fact.
|
|
269
|
+
// F.1 (fix round 2): while an entry is open, the row button renders as the disabled "Opening…"
|
|
270
|
+
// the POST set — a More-page re-render runs after every render of the container, and an enabled
|
|
271
|
+
// "Sign in" there invites a second tap that just gets the 409.
|
|
272
|
+
function signInBlockHtml(machine) {
|
|
273
|
+
var id = normId(machine && machine.instanceId);
|
|
274
|
+
if (!id) return '';
|
|
275
|
+
return signedOutAgents(machine).map(function (agent) {
|
|
276
|
+
var key = id + '-' + agent;
|
|
277
|
+
var opening = signInEntryOpen(state.signIn[key]);
|
|
278
|
+
return '<div class="mp-fact mp-signin" data-machine-signin-for="' + esc(key) + '">'
|
|
279
|
+
+ '<button type="button" class="mp-btn mp-signin-btn" data-machine-sign-in="' + esc(id) + '"'
|
|
280
|
+
+ ' data-machine-agent="' + esc(agent) + '"'
|
|
281
|
+
+ (opening ? ' disabled' : '') + '>'
|
|
282
|
+
+ (opening ? 'Opening…' : 'Sign in') + '</button>'
|
|
283
|
+
+ '<div class="mp-signin-block" data-machine-agent="' + esc(agent) + '"'
|
|
284
|
+
+ ' data-machine-sign-in-block="' + esc(key) + '">'
|
|
285
|
+
+ signInFlowHtml(agent, state.signIn[key]) + '</div>'
|
|
286
|
+
+ '</div>';
|
|
287
|
+
}).join('');
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// D-1 defence in depth, the same rule the card applies: only an https URL whose host (and whose
|
|
291
|
+
// redirect_uri host) is NOT loopback may become a link — a loopback link is the BOX's own URL,
|
|
292
|
+
// which a phone is not on.
|
|
293
|
+
function signInUrl(value) {
|
|
294
|
+
var loopback = ['localhost', '127.0.0.1', '::1', '[::1]'];
|
|
295
|
+
var parsed = null;
|
|
296
|
+
try { parsed = new URL(String(value || '')); } catch (error) { return ''; }
|
|
297
|
+
if (parsed.protocol !== 'https:') return '';
|
|
298
|
+
if (loopback.indexOf(String(parsed.hostname).toLowerCase()) >= 0) return '';
|
|
299
|
+
var redirect = parsed.searchParams.get('redirect_uri');
|
|
300
|
+
if (redirect) {
|
|
301
|
+
var host = '';
|
|
302
|
+
try { host = new URL(redirect).hostname; } catch (error) { host = ''; }
|
|
303
|
+
if (loopback.indexOf(String(host).toLowerCase()) >= 0) return '';
|
|
304
|
+
}
|
|
305
|
+
return parsed.href;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function providerIdForAgent(agent) {
|
|
309
|
+
return String(agent) === 'codex' ? 'codex' : 'claude';
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// The flow's block, in the card's own markup and labels (desktop parity). 'Show sign-in tab' is
|
|
313
|
+
// deliberately absent: the sign-in tab lives on the box, not on this phone.
|
|
314
|
+
function signInFlowHtml(agent, entry) {
|
|
315
|
+
var flow = entry && entry.flow;
|
|
316
|
+
if (!flow || ['starting', 'running', 'awaiting_user'].indexOf(String(flow.status || '')) === -1) return '';
|
|
317
|
+
var url = signInUrl(flow.verificationUrl);
|
|
318
|
+
var parts = [];
|
|
319
|
+
if (url) {
|
|
320
|
+
parts.push('<a class="mp-btn" data-open-sign-in-page href="' + esc(url) + '" target="_blank" rel="noopener">Open sign-in page</a>');
|
|
321
|
+
}
|
|
322
|
+
if (String(flow.codeEntry || '') === 'browser') {
|
|
323
|
+
parts.push('<code data-sign-in-user-code>' + esc(flow.userCode || '') + '</code>');
|
|
324
|
+
parts.push('<span data-sign-in-hint>Enter this code on the sign-in page.</span>');
|
|
325
|
+
} else if (String(flow.codeEntry || '') === 'paste') {
|
|
326
|
+
parts.push('<input data-sign-in-code type="text" inputmode="text" autocomplete="off" spellcheck="false"'
|
|
327
|
+
+ ' aria-label="Sign-in code" placeholder="Paste the code from the sign-in page" />');
|
|
328
|
+
parts.push('<button type="button" class="mp-btn" data-sign-in-code-submit>Submit code</button>');
|
|
329
|
+
parts.push('<span data-sign-in-hint>After you sign in, the page shows a code. Paste it here.</span>');
|
|
330
|
+
parts.push('<span data-sign-in-error></span>');
|
|
331
|
+
} else {
|
|
332
|
+
parts.push('<span data-sign-in-hint>Waiting for the sign-in page…</span>');
|
|
333
|
+
}
|
|
334
|
+
return parts.join('');
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function signInBlockNode(instanceId, agent) {
|
|
338
|
+
var doc = host || (typeof document !== 'undefined' ? document : null);
|
|
339
|
+
if (!doc || typeof doc.querySelector !== 'function') return null;
|
|
340
|
+
return doc.querySelector('[data-machine-sign-in-block="' + normId(instanceId) + '-' + String(agent) + '"]');
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function signInKey(instanceId, agent) {
|
|
344
|
+
return normId(instanceId) + '-' + String(agent);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function signInEntryOpen(entry) {
|
|
348
|
+
var flow = entry && entry.flow;
|
|
349
|
+
return !!flow && SIGN_IN_OPEN_STATUSES.indexOf(String(flow.status || '')) >= 0;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// The desktop's render key, over ONE machine-scoped entry: everything the block renders, with the
|
|
353
|
+
// probe's own timestamp dropped (it moves between ticks and is painted nowhere). Same defect as
|
|
354
|
+
// WP-2 on the card — a repaint on every tick replaces the code box mid-typing.
|
|
355
|
+
function signInRenderKey(entry) {
|
|
356
|
+
if (!entry || typeof entry !== 'object') return JSON.stringify(entry === undefined ? null : entry);
|
|
357
|
+
var copy = {};
|
|
358
|
+
Object.keys(entry).forEach(function (k) { copy[k] = entry[k]; });
|
|
359
|
+
if (copy.account && typeof copy.account === 'object') {
|
|
360
|
+
var account = {};
|
|
361
|
+
Object.keys(copy.account).forEach(function (k) { if (k !== 'checkedAt') account[k] = copy.account[k]; });
|
|
362
|
+
copy.account = account;
|
|
363
|
+
}
|
|
364
|
+
return JSON.stringify(copy);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// The draft a repaint must not destroy: the typed code (caret and focus included) and the refusal.
|
|
368
|
+
function captureSignInDraft(node) {
|
|
369
|
+
var code = node && typeof node.querySelector === 'function' ? node.querySelector('[data-sign-in-code]') : null;
|
|
370
|
+
var error = node && typeof node.querySelector === 'function' ? node.querySelector('[data-sign-in-error]') : null;
|
|
371
|
+
var doc = host || (typeof document !== 'undefined' ? document : null);
|
|
372
|
+
var draft = {
|
|
373
|
+
value: code && code.value !== undefined && code.value !== null ? String(code.value) : null,
|
|
374
|
+
selectionStart: code && typeof code.selectionStart === 'number' ? code.selectionStart : null,
|
|
375
|
+
selectionEnd: code && typeof code.selectionEnd === 'number' ? code.selectionEnd : null,
|
|
376
|
+
focused: !!(doc && doc.activeElement && doc.activeElement === code),
|
|
377
|
+
error: error ? String(error.textContent == null ? '' : error.textContent) : null,
|
|
378
|
+
};
|
|
379
|
+
// Fix round 3 (G): the carry (signInBlocksIn → adoptSignInBlocks) can only reach a block that is
|
|
380
|
+
// still on the page, and a More-page rebuild has already thrown that one away — so the same
|
|
381
|
+
// capture is kept in state as well, and the carry and the state can never disagree.
|
|
382
|
+
var key = node && typeof node.getAttribute === 'function'
|
|
383
|
+
? String(node.getAttribute('data-machine-sign-in-block') || '')
|
|
384
|
+
: '';
|
|
385
|
+
if (key && code) state.signInDrafts[key] = draftFromSignInInput(code, draft.focused);
|
|
386
|
+
return draft;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// The draft of ONE code input, in the shape state.signInDrafts holds it.
|
|
390
|
+
function draftFromSignInInput(code, focused) {
|
|
391
|
+
return {
|
|
392
|
+
value: code && code.value !== undefined && code.value !== null ? String(code.value) : null,
|
|
393
|
+
// Guarded like captureSignInDraft: a fake or exotic node has no numeric caret at all.
|
|
394
|
+
selectionStart: code && typeof code.selectionStart === 'number' ? code.selectionStart : null,
|
|
395
|
+
selectionEnd: code && typeof code.selectionEnd === 'number' ? code.selectionEnd : null,
|
|
396
|
+
focused: !!focused,
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function restoreSignInDraft(node, draft) {
|
|
401
|
+
if (!node || !draft || typeof node.querySelector !== 'function') return false;
|
|
402
|
+
var code = node.querySelector('[data-sign-in-code]');
|
|
403
|
+
if (code) {
|
|
404
|
+
if (draft.value !== null && code.value !== undefined) code.value = draft.value;
|
|
405
|
+
if (draft.focused) {
|
|
406
|
+
if (typeof code.focus === 'function') code.focus();
|
|
407
|
+
if (typeof code.setSelectionRange === 'function' && draft.selectionStart !== null && draft.selectionEnd !== null) {
|
|
408
|
+
code.setSelectionRange(draft.selectionStart, draft.selectionEnd);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
// `undefined` is "this draft says nothing about the refusal" — a state draft carries no copy, and
|
|
413
|
+
// writing undefined into the node would print the word "undefined" in the block.
|
|
414
|
+
if (draft.error !== null && draft.error !== undefined) {
|
|
415
|
+
var error = node.querySelector('[data-sign-in-error]');
|
|
416
|
+
if (error) error.textContent = draft.error;
|
|
417
|
+
}
|
|
418
|
+
return true;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// F.2 (fix round 2): render() replaces the card's markup wholesale on every More-page render, so
|
|
422
|
+
// an open block would lose its draft (the value being typed, the caret, the focus, the refusal
|
|
423
|
+
// copy) and its renderKey (one wasted write on the next tick). Capture before the write, adopt the
|
|
424
|
+
// same-keyed new node after it. No timer: the phone's ONE bounded re-read is unaffected.
|
|
425
|
+
function signInBlocksIn(root) {
|
|
426
|
+
var out = [];
|
|
427
|
+
var walk = function (node) {
|
|
428
|
+
var kids = node && node.children;
|
|
429
|
+
if (!kids) return;
|
|
430
|
+
for (var i = 0; i < kids.length; i += 1) {
|
|
431
|
+
var child = kids[i];
|
|
432
|
+
var key = child && typeof child.getAttribute === 'function'
|
|
433
|
+
? String(child.getAttribute('data-machine-sign-in-block') || '')
|
|
434
|
+
: '';
|
|
435
|
+
if (key) out.push({ key: key, draft: captureSignInDraft(child) });
|
|
436
|
+
walk(child);
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
walk(root);
|
|
440
|
+
return out;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function adoptSignInBlocks(root, carried, adoptedKeys) {
|
|
444
|
+
if (!carried || !carried.length) return 0;
|
|
445
|
+
var byKey = {};
|
|
446
|
+
carried.forEach(function (item) { byKey[item.key] = item; });
|
|
447
|
+
var adopted = 0;
|
|
448
|
+
var walk = function (node) {
|
|
449
|
+
var kids = node && node.children;
|
|
450
|
+
if (!kids) return;
|
|
451
|
+
for (var i = 0; i < kids.length; i += 1) {
|
|
452
|
+
var child = kids[i];
|
|
453
|
+
var key = child && typeof child.getAttribute === 'function'
|
|
454
|
+
? String(child.getAttribute('data-machine-sign-in-block') || '')
|
|
455
|
+
: '';
|
|
456
|
+
var item = key ? byKey[key] : null;
|
|
457
|
+
if (item) {
|
|
458
|
+
restoreSignInDraft(child, item.draft);
|
|
459
|
+
// The content was just painted from state.signIn[key], so the next identical tick must
|
|
460
|
+
// write nothing.
|
|
461
|
+
if (child.dataset && state.signIn[key] !== undefined) {
|
|
462
|
+
child.dataset.renderKey = signInRenderKey(state.signIn[key]);
|
|
463
|
+
}
|
|
464
|
+
if (adoptedKeys) adoptedKeys[key] = true;
|
|
465
|
+
adopted += 1;
|
|
466
|
+
}
|
|
467
|
+
walk(child);
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
walk(root);
|
|
471
|
+
return adopted;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Fix round 3 (G): the More page rebuilds #more-content wholesale and only THEN calls render(),
|
|
475
|
+
// so the block that was on the page is gone before the carry can look at it — the draft the code
|
|
476
|
+
// box reported into state.signInDrafts is the only record left. Every block the new card paints
|
|
477
|
+
// gets that draft back here; a block that WAS carried keeps its own (never overwritten).
|
|
478
|
+
function restoreSignInDraftsFromState(card, adoptedKeys) {
|
|
479
|
+
var restored = 0;
|
|
480
|
+
var walk = function (node) {
|
|
481
|
+
var kids = node && node.children;
|
|
482
|
+
if (!kids) return;
|
|
483
|
+
for (var i = 0; i < kids.length; i += 1) {
|
|
484
|
+
var child = kids[i];
|
|
485
|
+
var key = child && typeof child.getAttribute === 'function'
|
|
486
|
+
? String(child.getAttribute('data-machine-sign-in-block') || '')
|
|
487
|
+
: '';
|
|
488
|
+
if (key && !(adoptedKeys && adoptedKeys[key]) && state.signInDrafts[key]) {
|
|
489
|
+
restoreSignInDraft(child, state.signInDrafts[key]);
|
|
490
|
+
restored += 1;
|
|
491
|
+
}
|
|
492
|
+
walk(child);
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
walk(card);
|
|
496
|
+
return restored;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Fill ONE machine's block from a machine-scoped login-state payload, and remember the entry so
|
|
500
|
+
// the next page render paints the same flow. Gated on the fingerprint (see signInRenderKey), so a
|
|
501
|
+
// checkedAt-only tick leaves the DOM — and the code being typed into it — untouched.
|
|
502
|
+
// false = the block is not on the page (the caller stops that machine's re-read).
|
|
503
|
+
function renderSignInBlock(instanceId, agent, payload) {
|
|
504
|
+
var id = normId(instanceId);
|
|
505
|
+
var key = signInKey(id, agent);
|
|
506
|
+
var entry = ((payload && payload.providers) || {})[providerIdForAgent(agent)] || null;
|
|
507
|
+
state.signIn[key] = entry;
|
|
508
|
+
var node = signInBlockNode(id, agent);
|
|
509
|
+
if (!node) return false;
|
|
510
|
+
var nextKey = signInRenderKey(entry);
|
|
511
|
+
if (node.dataset && node.dataset.renderKey === nextKey) return true;
|
|
512
|
+
var draft = captureSignInDraft(node);
|
|
513
|
+
node.innerHTML = signInFlowHtml(agent, entry);
|
|
514
|
+
restoreSignInDraft(node, draft);
|
|
515
|
+
if (node.dataset) node.dataset.renderKey = nextKey;
|
|
516
|
+
// Fix round 3 (G): a closed entry has no box to type into, so the draft kept for it is stale —
|
|
517
|
+
// dropped AFTER the capture above, which would otherwise just have written it back.
|
|
518
|
+
if (!signInEntryOpen(entry)) delete state.signInDrafts[key];
|
|
519
|
+
return true;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// ── the ONE bounded re-read (fix C) ─────────────────────────────────────────────────────────
|
|
523
|
+
// One handle per '<id>-<agent>', never two. It stops when the flow is no longer open (then it
|
|
524
|
+
// re-reads the page once, silently), when the block has left the page, or at the tick cap.
|
|
525
|
+
function stopSignInPoll(key) {
|
|
526
|
+
var handle = state.signInTimers[key];
|
|
527
|
+
if (!handle) return false;
|
|
528
|
+
if (handle.id != null) timers.clear(handle.id);
|
|
529
|
+
delete state.signInTimers[key];
|
|
530
|
+
return true;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function stopSignInPolls() {
|
|
534
|
+
Object.keys(state.signInTimers).forEach(stopSignInPoll);
|
|
535
|
+
return true;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function armSignInPoll(key, id, agent) {
|
|
539
|
+
var handle = state.signInTimers[key];
|
|
540
|
+
if (!handle) return false;
|
|
541
|
+
handle.id = timers.set(function () {
|
|
542
|
+
handle.id = null;
|
|
543
|
+
if (!state.signInTimers[key]) return; // stopped while we waited
|
|
544
|
+
handle.ticks += 1;
|
|
545
|
+
if (handle.ticks > SIGN_IN_POLL_MAX_TICKS) { stopSignInPoll(key); return; }
|
|
546
|
+
readSignInState(id, agent).then(function () {
|
|
547
|
+
if (!state.signInTimers[key]) return;
|
|
548
|
+
if (!signInEntryOpen(state.signIn[key])) { stopSignInPoll(key); load({ silent: true }); return; }
|
|
549
|
+
armSignInPoll(key, id, agent);
|
|
550
|
+
});
|
|
551
|
+
}, SIGN_IN_POLL_MS);
|
|
552
|
+
return true;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function startSignInPoll(instanceId, agent) {
|
|
556
|
+
var id = normId(instanceId);
|
|
557
|
+
if (!id) return false;
|
|
558
|
+
var key = signInKey(id, agent);
|
|
559
|
+
if (state.signInTimers[key]) return false; // one handle per machine+agent
|
|
560
|
+
state.signInTimers[key] = { id: null, ticks: 0 };
|
|
561
|
+
return armSignInPoll(key, id, agent);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function readSignInState(instanceId, agent) {
|
|
565
|
+
var id = normId(instanceId);
|
|
566
|
+
return api('/api/providers/login-state?machine=' + encodeURIComponent(id))
|
|
567
|
+
.then(function (payload) {
|
|
568
|
+
// No block on the page any more: the row is gone, so is the reason to re-read.
|
|
569
|
+
if (!renderSignInBlock(id, agent, payload)) stopSignInPoll(signInKey(id, agent));
|
|
570
|
+
return payload;
|
|
571
|
+
})
|
|
572
|
+
.catch(function () {
|
|
573
|
+
// HANDLE: a box that does not answer leaves the block empty; the button stays tappable.
|
|
574
|
+
return null;
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// The server's refusal id, read the way serverError reads it.
|
|
579
|
+
function errorIdOf(failure) {
|
|
580
|
+
var payload = failure && failure.payload;
|
|
581
|
+
if (payload && typeof payload === 'object') {
|
|
582
|
+
if (payload.error) return String(payload.error);
|
|
583
|
+
if (payload.code) return String(payload.code);
|
|
584
|
+
}
|
|
585
|
+
return String((failure && (failure.code || failure.message)) || '');
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// The click: POST the box's login route through the proxy, read that box's state, then keep
|
|
589
|
+
// re-reading until the flow closes (see the block's header).
|
|
590
|
+
function machineSignIn(instanceId, agent, btn) {
|
|
591
|
+
var id = normId(instanceId);
|
|
592
|
+
if (!id) return Promise.resolve(false);
|
|
593
|
+
if (btn) { btn.disabled = true; btn.textContent = 'Opening…'; }
|
|
594
|
+
return api('/api/providers/' + providerIdForAgent(agent) + '/login?machine=' + encodeURIComponent(id), { method: 'POST', body: {} })
|
|
595
|
+
.then(function () { return readSignInState(id, agent); })
|
|
596
|
+
.then(function (payload) { startSignInPoll(id, agent); return payload; })
|
|
597
|
+
.catch(function (error) {
|
|
598
|
+
// A second tap while the box is already opening this flow is NOT an error: adopt the flow
|
|
599
|
+
// the box already has and keep reading it (the alternative is a dead-end 409 on the row).
|
|
600
|
+
if (errorIdOf(error) === 'provider_login_already_running') {
|
|
601
|
+
return readSignInState(id, agent).then(function (payload) { startSignInPoll(id, agent); return payload; });
|
|
602
|
+
}
|
|
603
|
+
// REPORT: the server's own code is the message; the button is restored for a retry.
|
|
604
|
+
if (btn) { btn.disabled = false; btn.textContent = 'Sign in'; }
|
|
605
|
+
setStatus(serverError(error));
|
|
606
|
+
return null;
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// The code the user pasted goes to the flow's own tab on the box (one POST, one meaning), then
|
|
611
|
+
// the block re-reads that box's state and keeps re-reading while the flow is open.
|
|
612
|
+
function submitSignInCode(instanceId, agent, node) {
|
|
613
|
+
var id = normId(instanceId);
|
|
614
|
+
var block = node && typeof node.closest === 'function' ? node.closest('[data-machine-sign-in-block]') : null;
|
|
615
|
+
var input = node && node.hasAttribute && node.hasAttribute('data-sign-in-code')
|
|
616
|
+
? node
|
|
617
|
+
: (block && typeof block.querySelector === 'function' ? block.querySelector('[data-sign-in-code]') : null);
|
|
618
|
+
var error = block && typeof block.querySelector === 'function' ? block.querySelector('[data-sign-in-error]') : null;
|
|
619
|
+
var submitBtn = node && node.hasAttribute && node.hasAttribute('data-sign-in-code-submit')
|
|
620
|
+
? node
|
|
621
|
+
: (block && typeof block.querySelector === 'function' ? block.querySelector('[data-sign-in-code-submit]') : null);
|
|
622
|
+
if (!id || !input) return Promise.resolve(false);
|
|
623
|
+
return api('/api/providers/' + providerIdForAgent(agent) + '/login/code?machine=' + encodeURIComponent(id), {
|
|
624
|
+
method: 'POST',
|
|
625
|
+
body: { code: String(input.value || '') },
|
|
626
|
+
})
|
|
627
|
+
.then(function () {
|
|
628
|
+
if (error) error.textContent = '';
|
|
629
|
+
input.value = '';
|
|
630
|
+
// The code is the box's now: the draft is spent, so a later re-render must not paint it back
|
|
631
|
+
// into an empty box. A refused send never reaches here — its typed code stays (see catch).
|
|
632
|
+
delete state.signInDrafts[signInKey(id, agent)];
|
|
633
|
+
setStatus('Code sent to the sign-in tab');
|
|
634
|
+
return readSignInState(id, agent).then(function (payload) { startSignInPoll(id, agent); return payload; });
|
|
635
|
+
})
|
|
636
|
+
.catch(function (failure) {
|
|
637
|
+
// REPORT: the server classifies its own refusal — shown in the card's own words, with the
|
|
638
|
+
// typed code kept and the button re-armed, so the user can fix it and send again.
|
|
639
|
+
if (error) error.textContent = signInErrorText(errorIdOf(failure) || 'unknown_error');
|
|
640
|
+
if (submitBtn) submitBtn.disabled = false;
|
|
641
|
+
return null;
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function machineCardHtml(machine, history) {
|
|
646
|
+
var m = machine || {};
|
|
647
|
+
var id = normId(m.instanceId);
|
|
648
|
+
var health = String(m.health || 'unknown').toLowerCase();
|
|
649
|
+
var sample = (m.stats && m.stats.sample) || null;
|
|
650
|
+
var statsAt = Number((m.stats && m.stats.checkedAt) || 0);
|
|
651
|
+
var stale = !statsAt || (at() - statsAt) > MACHINE_STATS_STALE_MS;
|
|
652
|
+
var cpuSeries = machineCpuSeries(history);
|
|
653
|
+
var memSeries = machineMemSeries(history);
|
|
654
|
+
var cpuNow = sample && sample.cpuPct != null ? Number(sample.cpuPct) : machineLastValue(cpuSeries);
|
|
655
|
+
var memNow = machineLastValue(memSeries);
|
|
656
|
+
var badge = credentialBadge(m, false);
|
|
657
|
+
var staleNote = stale && (sample || statsAt)
|
|
658
|
+
? '<span class="mp-stale" title="The hub re-reads a box at most once a minute; this sample is older than three">'
|
|
659
|
+
+ esc(STALE_NOTE) + '</span>'
|
|
660
|
+
: '';
|
|
661
|
+
var meta = [machineHealthLabel(m), machineLastSeenText(m), [m.os, m.arch].filter(Boolean).join('/')]
|
|
662
|
+
.filter(Boolean).join(' · ');
|
|
663
|
+
return '<article class="mp-card" data-machine-id="' + esc(id) + '" data-health="' + esc(health) + '">'
|
|
664
|
+
+ '<div class="mp-head">'
|
|
665
|
+
+ '<span class="mp-dot" data-health="' + esc(health) + '" title="' + esc(health) + '"></span>'
|
|
666
|
+
+ '<input class="mp-name" type="text" maxlength="80" spellcheck="false" value="' + esc(machineName(m)) + '"'
|
|
667
|
+
+ ' title="' + esc(machineName(m)) + '" aria-label="Machine name" data-machine-rename="' + esc(id) + '">'
|
|
668
|
+
+ '<span class="mp-cred" data-cred="' + esc(badge.cred) + '" title="' + esc(badge.title) + '">' + esc(badge.text) + '</span>'
|
|
669
|
+
+ '</div>'
|
|
670
|
+
+ '<div class="mp-meta">' + esc(meta) + ' ' + staleNote + '</div>'
|
|
671
|
+
+ sparkBlock(cpuSeries, memSeries, cpuNow, memNow)
|
|
672
|
+
+ '<div class="mp-facts">' + machineFactsHtml(sample, m.agents) + '</div>'
|
|
673
|
+
+ signInBlockHtml(m)
|
|
674
|
+
+ '<div class="mp-actions">'
|
|
675
|
+
+ '<button type="button" class="mp-btn mp-fav" aria-pressed="' + (m.favorite ? 'true' : 'false') + '"'
|
|
676
|
+
+ ' title="Favorite — shown first in the picker"'
|
|
677
|
+
+ ' data-machine-toggle="' + esc(id) + '" data-machine-field="favorite">'
|
|
678
|
+
+ esc(m.favorite ? '★ Favorite' : '☆ Favorite') + '</button>'
|
|
679
|
+
+ '<button type="button" class="mp-btn" data-machine-new-session="' + esc(id) + '">New session here</button>'
|
|
680
|
+
+ '<button type="button" class="mp-btn" data-machine-toggle="' + esc(id) + '" data-machine-field="hidden">'
|
|
681
|
+
+ esc(m.hidden ? 'Show in picker' : 'Hide from picker') + '</button>'
|
|
682
|
+
+ (badge.action === 'join-pool'
|
|
683
|
+
? '<button type="button" class="mp-btn" data-machine-join="' + esc(id) + '"'
|
|
684
|
+
+ ' data-machine-url="' + esc(m.url || '') + '">Join pool</button>'
|
|
685
|
+
: '')
|
|
686
|
+
+ '<button type="button" class="mp-btn mp-danger" data-machine-remove="' + esc(id) + '">Remove</button>'
|
|
687
|
+
+ '</div>'
|
|
688
|
+
+ signInRowHtml(m)
|
|
689
|
+
+ '</article>';
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// This Mac is not a registry row: its settings are /api/machine-self and its numbers are its own
|
|
693
|
+
// host samples. Same three settings writes as a member, with an empty machine id.
|
|
694
|
+
function selfCardHtml() {
|
|
695
|
+
var settings = state.selfSettings || {};
|
|
696
|
+
var latest = (state.self && state.self.latest) || null;
|
|
697
|
+
var history = state.self || null;
|
|
698
|
+
var id = normId(state.selfInstanceId);
|
|
699
|
+
var name = String(settings.displayName || 'This Mac');
|
|
700
|
+
var cpuSeries = machineCpuSeries(history);
|
|
701
|
+
var memSeries = machineMemSeries(history);
|
|
702
|
+
var cpuNow = latest && latest.cpuPct != null ? Number(latest.cpuPct) : machineLastValue(cpuSeries);
|
|
703
|
+
var memNow = machineLastValue(memSeries);
|
|
704
|
+
var poolNote = state.machines.length
|
|
705
|
+
? 'In a pool with ' + state.machines.length + ' other machine' + (state.machines.length === 1 ? '' : 's')
|
|
706
|
+
: 'Not in a pool yet';
|
|
707
|
+
var facts = machineFactsHtml(latest, null)
|
|
708
|
+
+ factRow('Memory', latest ? esc(machineFormatBytes(latest.memUsedBytes) + ' / ' + machineFormatBytes(latest.memTotalBytes)) : '');
|
|
709
|
+
return '<article class="mp-card mp-self" data-machine-id="' + esc(id) + '" data-health="reachable">'
|
|
710
|
+
+ '<div class="mp-head">'
|
|
711
|
+
+ '<span class="mp-dot" data-health="reachable" title="reachable"></span>'
|
|
712
|
+
+ '<input class="mp-name" type="text" maxlength="80" spellcheck="false" value="' + esc(name) + '"'
|
|
713
|
+
+ ' title="' + esc(name) + '" aria-label="This Mac name" data-machine-rename="">'
|
|
714
|
+
+ '<span class="mp-cred" data-cred="self" title="This machine runs its own sessions">This Mac</span>'
|
|
715
|
+
+ '</div>'
|
|
716
|
+
+ '<div class="mp-meta">' + esc(poolNote + (id ? ' · ' + id : '')) + '</div>'
|
|
717
|
+
+ sparkBlock(cpuSeries, memSeries, cpuNow, memNow)
|
|
718
|
+
+ '<div class="mp-facts">' + facts + '</div>'
|
|
719
|
+
+ '<div class="mp-actions">'
|
|
720
|
+
+ '<button type="button" class="mp-btn mp-fav" aria-pressed="' + (settings.favorite ? 'true' : 'false') + '"'
|
|
721
|
+
+ ' title="Favorite — shown first in the picker" data-machine-toggle="" data-machine-field="favorite">'
|
|
722
|
+
+ esc(settings.favorite ? '★ Favorite' : '☆ Favorite') + '</button>'
|
|
723
|
+
+ '<button type="button" class="mp-btn" data-machine-new-session="">New session here</button>'
|
|
724
|
+
+ '<button type="button" class="mp-btn" data-machine-toggle="" data-machine-field="hidden">'
|
|
725
|
+
+ esc(settings.hidden ? 'Show in picker' : 'Hide from picker') + '</button>'
|
|
726
|
+
+ '</div>'
|
|
727
|
+
+ '</article>';
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
function headerHtml() {
|
|
731
|
+
var inPool = state.machines.length > 0;
|
|
732
|
+
return '<div class="more-section-head">'
|
|
733
|
+
+ '<span>'
|
|
734
|
+
+ '<span class="more-kicker">Machines</span>'
|
|
735
|
+
+ '<h3 id="phone-machines-title">Other CTM instances</h3>'
|
|
736
|
+
+ '</span>'
|
|
737
|
+
+ '<button class="row-action" type="button" data-machines-refresh>Refresh</button>'
|
|
738
|
+
+ '</div>'
|
|
739
|
+
+ '<div class="mp-toolbar">'
|
|
740
|
+
+ '<button type="button" class="mp-btn mp-primary" data-machine-add>+ Add machine</button>'
|
|
741
|
+
+ '<button type="button" class="mp-btn" data-machine-show-code>Show a code for this machine</button>'
|
|
742
|
+
+ (inPool ? '<button type="button" class="mp-btn mp-danger" data-pool-leave>Leave pool</button>' : '')
|
|
743
|
+
+ '</div>';
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function listHtml() {
|
|
747
|
+
if (state.error) return '<p class="mp-error" role="status">' + esc(state.error) + '</p>';
|
|
748
|
+
var cards = selfCardHtml();
|
|
749
|
+
var rows = state.machines.map(function (machine) {
|
|
750
|
+
return machineCardHtml(machine, state.histories[normId(machine && machine.instanceId)]);
|
|
751
|
+
}).join('');
|
|
752
|
+
var note = rows ? '' : '<p class="more-section-copy">No other machines registered yet — use “Add machine”.</p>';
|
|
753
|
+
return cards + rows + note;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
function bodyHtml() {
|
|
757
|
+
return headerHtml() + '<div class="mp-body">' + listHtml() + '</div>';
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// Renders inside #more-content. more.js rewrites that container on every More render, so app.js
|
|
761
|
+
// calls this again after each of its renders and the section node is reused (never duplicated).
|
|
762
|
+
function render(container) {
|
|
763
|
+
var target = container || $('more-content');
|
|
764
|
+
if (!target) return null;
|
|
765
|
+
var card = typeof target.querySelector === 'function' ? target.querySelector('#phone-machines-card') : null;
|
|
766
|
+
var doc = host || (typeof document !== 'undefined' ? document : null);
|
|
767
|
+
if (!card) {
|
|
768
|
+
if (!doc || typeof doc.createElement !== 'function') return null;
|
|
769
|
+
card = doc.createElement('section');
|
|
770
|
+
card.id = 'phone-machines-card';
|
|
771
|
+
card.className = 'more-section-card';
|
|
772
|
+
card.setAttribute('aria-labelledby', 'phone-machines-title');
|
|
773
|
+
target.appendChild(card);
|
|
774
|
+
}
|
|
775
|
+
var carried = signInBlocksIn(card);
|
|
776
|
+
card.innerHTML = bodyHtml() + '<p class="mp-status" id="' + STATUS_ID + '" role="status" aria-live="polite"></p>';
|
|
777
|
+
var adoptedKeys = {};
|
|
778
|
+
adoptSignInBlocks(card, carried, adoptedKeys);
|
|
779
|
+
// The carried blocks are already whole; a block the write just REBUILT (a More-page render threw
|
|
780
|
+
// the old card away) has only state.signInDrafts to be painted from.
|
|
781
|
+
restoreSignInDraftsFromState(card, adoptedKeys);
|
|
782
|
+
bind(card);
|
|
783
|
+
return card;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
function setStatus(text) {
|
|
787
|
+
var message = String(text || '');
|
|
788
|
+
var node = $(STATUS_ID);
|
|
789
|
+
if (node) node.textContent = message;
|
|
790
|
+
if (message) toast(message);
|
|
791
|
+
return message;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// The server's error code is shown VERBATIM (machine_identity_mismatch, machine_unreachable, …).
|
|
795
|
+
function serverError(err) {
|
|
796
|
+
var payload = err && err.payload;
|
|
797
|
+
if (payload && typeof payload === 'object') {
|
|
798
|
+
if (payload.error) return String(payload.error);
|
|
799
|
+
if (payload.code) return String(payload.code);
|
|
800
|
+
}
|
|
801
|
+
return (err && (err.code || err.message)) || 'machine_request_failed';
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
// ── loading ────────────────────────────────────────────────────────────────────────────────
|
|
805
|
+
function historyQuery(machineId, now) {
|
|
806
|
+
return 'since=' + Math.trunc(Math.max(0, Number(now) - HISTORY_MS))
|
|
807
|
+
+ (machineId ? '&machine=' + encodeURIComponent(machineId) : '');
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
function loadHistories(now) {
|
|
811
|
+
var reachable = state.machines.filter(function (machine) {
|
|
812
|
+
return machine && machine.instanceId && String(machine.health || '').toLowerCase() === 'reachable';
|
|
813
|
+
});
|
|
814
|
+
return Promise.all(reachable.map(function (machine) {
|
|
815
|
+
var id = normId(machine.instanceId);
|
|
816
|
+
return api('/api/host/stats?' + historyQuery(id, now))
|
|
817
|
+
.then(function (payload) { state.histories[id] = payload || null; return null; })
|
|
818
|
+
.catch(function () {
|
|
819
|
+
// HANDLE: one box's sparkline is decoration; its card still renders from the hub's sample.
|
|
820
|
+
state.histories[id] = null;
|
|
821
|
+
return null;
|
|
822
|
+
});
|
|
823
|
+
}));
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// P-7: after every successful /api/machines load, remember who this phone has reached. `self` is
|
|
827
|
+
// this origin; members keep whatever lastSeenAt they already had.
|
|
828
|
+
function rememberOrigins() {
|
|
829
|
+
var failover = failoverModule();
|
|
830
|
+
if (!failover || typeof failover.rememberPoolOrigins !== 'function') return null;
|
|
831
|
+
var selfOrigin = String((root.location && root.location.origin) || '');
|
|
832
|
+
var self = {
|
|
833
|
+
instanceId: state.selfInstanceId,
|
|
834
|
+
displayName: (state.selfSettings && state.selfSettings.displayName) || '',
|
|
835
|
+
urls: (state.self && state.self.urls) || (selfOrigin ? [selfOrigin] : []),
|
|
836
|
+
health: 'reachable',
|
|
837
|
+
};
|
|
838
|
+
try {
|
|
839
|
+
return failover.rememberPoolOrigins(state.machines, self, { storage: d.storage, now: clock });
|
|
840
|
+
} catch (error) {
|
|
841
|
+
return null; // HANDLE: a blocked dial cache must never break the Machines page
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
function load(options) {
|
|
846
|
+
var opts = options || {};
|
|
847
|
+
if (!opts.silent) state.loading = true;
|
|
848
|
+
var now = at();
|
|
849
|
+
return Promise.all([
|
|
850
|
+
api('/api/machines'),
|
|
851
|
+
api('/api/machine-self'),
|
|
852
|
+
api('/api/host/stats?' + historyQuery('', now)),
|
|
853
|
+
]).then(function (results) {
|
|
854
|
+
var list = results[0] || {};
|
|
855
|
+
var self = results[1] || {};
|
|
856
|
+
state.loading = false;
|
|
857
|
+
state.error = '';
|
|
858
|
+
state.machines = Array.isArray(list.machines) ? list.machines.filter(function (m) { return m && m.instanceId; }) : [];
|
|
859
|
+
state.selfSettings = self.self || null;
|
|
860
|
+
state.self = results[2] || null;
|
|
861
|
+
state.selfInstanceId = normId((state.self && state.self.instanceId) || self.instanceId);
|
|
862
|
+
state.loaded = true;
|
|
863
|
+
rememberOrigins();
|
|
864
|
+
state.origins = poolOrigins();
|
|
865
|
+
return loadHistories(now).then(function () {
|
|
866
|
+
render();
|
|
867
|
+
return true;
|
|
868
|
+
});
|
|
869
|
+
}).catch(function (error) {
|
|
870
|
+
// REPORT: a transport failure is shown on the page, never swallowed into an empty list.
|
|
871
|
+
state.loading = false;
|
|
872
|
+
state.error = 'machines_unavailable: ' + ((error && error.message) || 'fetch failed');
|
|
873
|
+
render();
|
|
874
|
+
return false;
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
// ── writes (machine-owned settings, D-4 rev 2) ─────────────────────────────────────────────
|
|
879
|
+
function selfPath(instanceId) {
|
|
880
|
+
var id = normId(instanceId);
|
|
881
|
+
return '/api/machine-self' + (id ? '?machine=' + encodeURIComponent(id) : '');
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
function applySelfPatch(instanceId, patch) {
|
|
885
|
+
return api(selfPath(instanceId), { method: 'POST', body: patch || {} })
|
|
886
|
+
.then(function () { setStatus(''); return load({ silent: true }); })
|
|
887
|
+
.catch(function (error) {
|
|
888
|
+
// REPORT: the server's refusal (or the transport fault) is what the user needs to see.
|
|
889
|
+
setStatus(serverError(error));
|
|
890
|
+
return false;
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
function toggle(instanceId, field) {
|
|
895
|
+
if (field !== 'favorite' && field !== 'hidden') return Promise.resolve(false);
|
|
896
|
+
var id = normId(instanceId);
|
|
897
|
+
var current = id
|
|
898
|
+
? state.machines.filter(function (m) { return normId(m && m.instanceId) === id; })[0]
|
|
899
|
+
: state.selfSettings;
|
|
900
|
+
var patch = {};
|
|
901
|
+
patch[field] = (current && (current[field] === 1 || current[field] === true)) ? 0 : 1;
|
|
902
|
+
return applySelfPatch(id, patch);
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
function rename(instanceId, value) {
|
|
906
|
+
var name = String(value == null ? '' : value).trim();
|
|
907
|
+
if (name.length > 80) {
|
|
908
|
+
setStatus('display_name_too_long');
|
|
909
|
+
return Promise.resolve(false);
|
|
910
|
+
}
|
|
911
|
+
return applySelfPatch(normId(instanceId), { displayName: name });
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
function removeMachine(instanceId) {
|
|
915
|
+
var id = normId(instanceId);
|
|
916
|
+
if (!id) return Promise.resolve(false);
|
|
917
|
+
var machine = state.machines.filter(function (row) { return normId(row && row.instanceId) === id; })[0];
|
|
918
|
+
var name = machineName(machine) || id;
|
|
919
|
+
if (confirmImpl && !confirmImpl('Remove ' + name + ' from this machine?')) return Promise.resolve(false);
|
|
920
|
+
return api('/api/machines/' + encodeURIComponent(id) + '/remove', { method: 'POST' })
|
|
921
|
+
.then(function (data) {
|
|
922
|
+
var payload = data || {};
|
|
923
|
+
var rotated = Array.isArray(payload.rotated) ? payload.rotated : [];
|
|
924
|
+
var unreachable = Array.isArray(payload.unreachable) ? payload.unreachable : [];
|
|
925
|
+
var note = 'Removed ' + name + '.';
|
|
926
|
+
if (rotated.length) note += ' Re-keyed: ' + rotated.join(', ') + '.';
|
|
927
|
+
if (unreachable.length) note += ' Unreachable (re-pair after the grace): ' + unreachable.join(', ') + '.';
|
|
928
|
+
setStatus(note);
|
|
929
|
+
return load({ silent: true });
|
|
930
|
+
})
|
|
931
|
+
.catch(function (error) {
|
|
932
|
+
// REPORT: a failed removal must never look like a successful one.
|
|
933
|
+
setStatus(serverError(error));
|
|
934
|
+
return false;
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
function leavePool() {
|
|
939
|
+
if (confirmImpl && !confirmImpl('Leave the machine pool? This phone drops the shared secret and its machine list.')) {
|
|
940
|
+
return Promise.resolve(false);
|
|
941
|
+
}
|
|
942
|
+
return api('/api/pool/leave', { method: 'POST', body: {} })
|
|
943
|
+
.then(function (data) {
|
|
944
|
+
setStatus('Left the pool (' + (Number((data || {}).removed) || 0) + ' rows dropped).');
|
|
945
|
+
return load({ silent: true });
|
|
946
|
+
})
|
|
947
|
+
.catch(function (error) {
|
|
948
|
+
// REPORT: an unreachable hub must not read as "you left the pool".
|
|
949
|
+
setStatus(serverError(error));
|
|
950
|
+
return false;
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// "New session here": the EXISTING new-session sheet with this machine selected. app.js owns that
|
|
955
|
+
// modal, so there is no second create path.
|
|
956
|
+
function newSessionHere(instanceId) {
|
|
957
|
+
return Promise.resolve(openNewSession(normId(instanceId)));
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
function signInToo(origin) {
|
|
961
|
+
var target = String(origin || '').trim().replace(/\/+$/, '');
|
|
962
|
+
if (!/^https:\/\/[^/?#]+$/i.test(target)) {
|
|
963
|
+
setStatus('sign_in_origin_unavailable');
|
|
964
|
+
return false;
|
|
965
|
+
}
|
|
966
|
+
if (root.location && typeof root.location.assign === 'function') {
|
|
967
|
+
root.location.assign(target + '/m/');
|
|
968
|
+
return true;
|
|
969
|
+
}
|
|
970
|
+
return false;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
// ── dialogs (Add machine, Show a code) ─────────────────────────────────────────────────────
|
|
974
|
+
function mountDialog(html) {
|
|
975
|
+
var doc = host || (typeof document !== 'undefined' ? document : null);
|
|
976
|
+
if (!doc || typeof doc.createElement !== 'function') return null;
|
|
977
|
+
var overlay = doc.createElement('div');
|
|
978
|
+
overlay.className = 'modal-backdrop';
|
|
979
|
+
overlay.innerHTML = html;
|
|
980
|
+
var parent = doc.body || doc.documentElement;
|
|
981
|
+
if (parent && typeof parent.appendChild === 'function') parent.appendChild(overlay);
|
|
982
|
+
return overlay;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
function closeDialog() {
|
|
986
|
+
if (dialog && dialog.overlay && dialog.overlay.parentNode) dialog.overlay.parentNode.removeChild(dialog.overlay);
|
|
987
|
+
dialog = null;
|
|
988
|
+
return true;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
function previewFields() {
|
|
992
|
+
return machinesAddPreview(
|
|
993
|
+
dialog && dialog.pairInput ? dialog.pairInput.value : '',
|
|
994
|
+
dialog && dialog.codeInput ? dialog.codeInput.value : '',
|
|
995
|
+
dialog && dialog.urlInput ? dialog.urlInput.value : '',
|
|
996
|
+
);
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
// RFC 8628 §5.4 parity with the desktop dialog: the box's OWN name is shown BEFORE Confirm is
|
|
1000
|
+
// enabled, and a string that does not decode is refused with the server's own wording.
|
|
1001
|
+
function paintPreview() {
|
|
1002
|
+
if (!dialog) return null;
|
|
1003
|
+
var preview = previewFields();
|
|
1004
|
+
if (dialog.preview) {
|
|
1005
|
+
var text = '';
|
|
1006
|
+
if (preview.message === 'pairing_string_invalid') text = 'pairing_string_invalid — that is not a ctmpair1: string.';
|
|
1007
|
+
else if (preview.message === 'pairing_code_invalid') text = 'pairing_code_invalid — a code is 8 letters (XXXX-XXXX).';
|
|
1008
|
+
else if (preview.message === 'address_required') text = 'Enter the machine address (https://host:port).';
|
|
1009
|
+
else if (preview.ok && preview.name) text = 'Box: ' + preview.name + (preview.url ? ' — ' + preview.url : '');
|
|
1010
|
+
else if (preview.ok) text = 'Box: ' + preview.url;
|
|
1011
|
+
else if (preview.addressOnly) text = preview.url + ' — the name appears after pairing';
|
|
1012
|
+
dialog.preview.textContent = text;
|
|
1013
|
+
}
|
|
1014
|
+
if (dialog.confirm) dialog.confirm.disabled = !preview.ok;
|
|
1015
|
+
return preview;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
function openAddDialog(options) {
|
|
1019
|
+
var opts = options || {};
|
|
1020
|
+
closeDialog();
|
|
1021
|
+
var overlay = mountDialog('<section class="model-picker-sheet mp-modal" role="dialog" aria-modal="true" aria-labelledby="mp-add-title">'
|
|
1022
|
+
+ '<div class="model-picker-head"><div><h2 id="mp-add-title">Add a machine</h2>'
|
|
1023
|
+
+ '<p>Paste a pairing string from the other machine, or type its code and address.</p></div>'
|
|
1024
|
+
+ '<button type="button" class="model-picker-close" id="mp-add-cancel" aria-label="Close">×</button></div>'
|
|
1025
|
+
+ '<label class="mp-modal-label" for="mp-pair-input">Pairing string</label>'
|
|
1026
|
+
+ '<textarea id="mp-pair-input" class="mp-modal-input" rows="3" spellcheck="false" placeholder="ctmpair1:…"></textarea>'
|
|
1027
|
+
+ '<div id="mp-pair-preview" class="mp-pair-preview" role="status" aria-live="polite"></div>'
|
|
1028
|
+
+ '<p class="mp-modal-or">or</p>'
|
|
1029
|
+
+ '<input id="mp-pair-code" class="mp-modal-input" type="text" autocomplete="off" spellcheck="false" placeholder="XXXX-XXXX" aria-label="Pairing code">'
|
|
1030
|
+
+ '<input id="mp-pair-url" class="mp-modal-input" type="text" autocapitalize="off" autocomplete="off" spellcheck="false" placeholder="https://spark.r.walle.sh" aria-label="Machine address">'
|
|
1031
|
+
+ '<p id="mp-add-error" class="mp-modal-error" role="status" aria-live="polite"></p>'
|
|
1032
|
+
+ '<div class="mp-modal-actions">'
|
|
1033
|
+
+ '<button type="button" class="mp-btn mp-primary" id="mp-add-confirm" disabled>Add machine</button>'
|
|
1034
|
+
+ '</div>'
|
|
1035
|
+
+ '<details class="mp-modal-legacy"><summary>Register by URL (legacy)</summary>'
|
|
1036
|
+
+ '<p class="mp-modal-sub">An env-token box (P-8) that has not joined the pool. Its identity is pinned by the manifest it answers.</p>'
|
|
1037
|
+
+ '<input id="mp-legacy-url" class="mp-modal-input" type="text" autocomplete="off" spellcheck="false" placeholder="http://192.168.1.20:3456" aria-label="Legacy machine URL">'
|
|
1038
|
+
+ '<input id="mp-legacy-name" class="mp-modal-input" type="text" autocomplete="off" spellcheck="false" placeholder="Display name" aria-label="Legacy machine display name">'
|
|
1039
|
+
+ '<div class="mp-modal-actions"><button type="button" class="mp-btn" id="mp-legacy-submit">Register by URL</button></div>'
|
|
1040
|
+
+ '</details></section>');
|
|
1041
|
+
if (!overlay) return null;
|
|
1042
|
+
dialog = {
|
|
1043
|
+
overlay: overlay,
|
|
1044
|
+
pairInput: $('mp-pair-input'),
|
|
1045
|
+
codeInput: $('mp-pair-code'),
|
|
1046
|
+
urlInput: $('mp-pair-url'),
|
|
1047
|
+
preview: $('mp-pair-preview'),
|
|
1048
|
+
error: $('mp-add-error'),
|
|
1049
|
+
confirm: $('mp-add-confirm'),
|
|
1050
|
+
};
|
|
1051
|
+
// P-8: "Join pool" on an env-token row opens this dialog with the row's address already filled.
|
|
1052
|
+
if (opts.url && dialog.urlInput) dialog.urlInput.value = String(opts.url);
|
|
1053
|
+
['pairInput', 'codeInput', 'urlInput'].forEach(function (key) {
|
|
1054
|
+
var node = dialog[key];
|
|
1055
|
+
if (node && typeof node.addEventListener === 'function') node.addEventListener('input', paintPreview);
|
|
1056
|
+
});
|
|
1057
|
+
var cancel = $('mp-add-cancel');
|
|
1058
|
+
if (cancel && typeof cancel.addEventListener === 'function') cancel.addEventListener('click', closeDialog);
|
|
1059
|
+
if (dialog.confirm && typeof dialog.confirm.addEventListener === 'function') dialog.confirm.addEventListener('click', submitAddDialog);
|
|
1060
|
+
var legacy = $('mp-legacy-submit');
|
|
1061
|
+
if (legacy && typeof legacy.addEventListener === 'function') legacy.addEventListener('click', registerLegacy);
|
|
1062
|
+
paintPreview();
|
|
1063
|
+
return dialog;
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
function submitAddDialog() {
|
|
1067
|
+
if (!dialog) return Promise.resolve(false);
|
|
1068
|
+
var preview = previewFields();
|
|
1069
|
+
if (!preview.ok) return Promise.resolve(false);
|
|
1070
|
+
var body = preview.mode === 'string'
|
|
1071
|
+
? { pairingString: preview.pairingString }
|
|
1072
|
+
: { code: machinesFormatCode(preview.code), url: preview.url };
|
|
1073
|
+
if (dialog.error) dialog.error.textContent = '';
|
|
1074
|
+
return api('/api/machines/pair', { method: 'POST', body: body })
|
|
1075
|
+
.then(function () { closeDialog(); setStatus('Machine added.'); return load({ silent: true }); })
|
|
1076
|
+
.catch(function (error) {
|
|
1077
|
+
// REPORT: the server's code is relayed VERBATIM and the dialog stays open for a retry.
|
|
1078
|
+
if (dialog && dialog.error) dialog.error.textContent = serverError(error);
|
|
1079
|
+
return false;
|
|
1080
|
+
});
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
function registerLegacy() {
|
|
1084
|
+
var urlNode = $('mp-legacy-url');
|
|
1085
|
+
var nameNode = $('mp-legacy-name');
|
|
1086
|
+
var url = String((urlNode && urlNode.value) || '').trim();
|
|
1087
|
+
var displayName = String((nameNode && nameNode.value) || '').trim();
|
|
1088
|
+
var errorNode = (dialog && dialog.error) || $('mp-add-error');
|
|
1089
|
+
if (!url) {
|
|
1090
|
+
if (errorNode) errorNode.textContent = 'Enter the machine URL.';
|
|
1091
|
+
return Promise.resolve(false);
|
|
1092
|
+
}
|
|
1093
|
+
return api('/api/machines', { method: 'POST', body: { url: url, displayName: displayName } })
|
|
1094
|
+
.then(function () { closeDialog(); setStatus('Machine registered.'); return load({ silent: true }); })
|
|
1095
|
+
.catch(function (error) {
|
|
1096
|
+
// REPORT: the register refusal is shown verbatim in the dialog.
|
|
1097
|
+
if (errorNode) errorNode.textContent = serverError(error);
|
|
1098
|
+
return false;
|
|
1099
|
+
});
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
function openShowCodeDialog() {
|
|
1103
|
+
closeDialog();
|
|
1104
|
+
var overlay = mountDialog('<section class="model-picker-sheet mp-modal" role="dialog" aria-modal="true" aria-labelledby="mp-code-title">'
|
|
1105
|
+
+ '<div class="model-picker-head"><div><h2 id="mp-code-title">Show a code for this machine</h2>'
|
|
1106
|
+
+ '<p>On the other machine, choose “Add machine” and paste this string — or type the code.</p></div>'
|
|
1107
|
+
+ '<button type="button" class="model-picker-close" id="mp-code-close" aria-label="Close">×</button></div>'
|
|
1108
|
+
+ '<div id="mp-code-value" class="mp-code-value" role="status" aria-live="polite">…</div>'
|
|
1109
|
+
+ '<textarea id="mp-pair-string" class="mp-modal-input" rows="4" readonly spellcheck="false" aria-label="Pairing string"></textarea>'
|
|
1110
|
+
+ '<p id="mp-code-error" class="mp-modal-error" role="status" aria-live="polite"></p>'
|
|
1111
|
+
+ '</section>');
|
|
1112
|
+
if (!overlay) return null;
|
|
1113
|
+
dialog = { overlay: overlay, codeValue: $('mp-code-value'), pairString: $('mp-pair-string'), error: $('mp-code-error') };
|
|
1114
|
+
var close = $('mp-code-close');
|
|
1115
|
+
if (close && typeof close.addEventListener === 'function') close.addEventListener('click', closeDialog);
|
|
1116
|
+
return api('/api/machine-pairing/offers', { method: 'POST', body: {} })
|
|
1117
|
+
.then(function (data) {
|
|
1118
|
+
var payload = data || {};
|
|
1119
|
+
if (dialog && dialog.codeValue) dialog.codeValue.textContent = String(payload.code || '');
|
|
1120
|
+
if (dialog && dialog.pairString) dialog.pairString.value = String(payload.pairingString || '');
|
|
1121
|
+
return true;
|
|
1122
|
+
})
|
|
1123
|
+
.catch(function (error) {
|
|
1124
|
+
// REPORT: the offer failure stays in the dialog so the user can retry.
|
|
1125
|
+
if (dialog && dialog.error) dialog.error.textContent = serverError(error);
|
|
1126
|
+
return false;
|
|
1127
|
+
});
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
// ── wiring ─────────────────────────────────────────────────────────────────────────────────
|
|
1131
|
+
// ONE delegated click listener plus one change listener on the page, so a re-render never loses a
|
|
1132
|
+
// binding and a rename input only writes on commit (desktop parity: rename on `change`).
|
|
1133
|
+
// Fix round 3 (G): the code box reports itself the same way — the draft is kept in
|
|
1134
|
+
// state.signInDrafts as the user types (and as focus moves), because the More page may replace the
|
|
1135
|
+
// whole card between two keystrokes and the input's own value goes with it.
|
|
1136
|
+
function bind(scope) {
|
|
1137
|
+
if (!scope || typeof scope.addEventListener !== 'function' || scope.dataset.mpBound === '1') return false;
|
|
1138
|
+
scope.dataset.mpBound = '1';
|
|
1139
|
+
// The event's code input and its block key. The key is READ from the block's own attribute,
|
|
1140
|
+
// never rebuilt by splitting: an instance id is uuid-like and contains hyphens itself.
|
|
1141
|
+
function draftTargetOf(event) {
|
|
1142
|
+
var target = event && event.target;
|
|
1143
|
+
var code = target && typeof target.closest === 'function' ? target.closest('[data-sign-in-code]') : null;
|
|
1144
|
+
if (!code) return null;
|
|
1145
|
+
var block = typeof code.closest === 'function' ? code.closest('[data-machine-sign-in-block]') : null;
|
|
1146
|
+
var key = block && typeof block.getAttribute === 'function'
|
|
1147
|
+
? String(block.getAttribute('data-machine-sign-in-block') || '')
|
|
1148
|
+
: '';
|
|
1149
|
+
return key ? { code: code, key: key } : null;
|
|
1150
|
+
}
|
|
1151
|
+
scope.addEventListener('input', function (event) {
|
|
1152
|
+
var hit = draftTargetOf(event);
|
|
1153
|
+
if (!hit) return;
|
|
1154
|
+
state.signInDrafts[hit.key] = draftFromSignInInput(hit.code, true);
|
|
1155
|
+
});
|
|
1156
|
+
scope.addEventListener('focusin', function (event) {
|
|
1157
|
+
var hit = draftTargetOf(event);
|
|
1158
|
+
if (!hit) return;
|
|
1159
|
+
state.signInDrafts[hit.key] = draftFromSignInInput(hit.code, true);
|
|
1160
|
+
});
|
|
1161
|
+
scope.addEventListener('focusout', function (event) {
|
|
1162
|
+
var hit = draftTargetOf(event);
|
|
1163
|
+
if (!hit) return;
|
|
1164
|
+
// Fix round 3b (H): NOT `focused: false` outright. Chrome dispatches focusout on the input
|
|
1165
|
+
// DURING the removal the More page performs, and at dispatch time a removal blur and a genuine
|
|
1166
|
+
// one are indistinguishable — the user's next keystrokes would go to <body>. The value and the
|
|
1167
|
+
// caret are stored now; the flag is decided one microtask later, when "was this input thrown
|
|
1168
|
+
// away?" has an answer. A microtask, not a timer: this module still owns exactly one timer.
|
|
1169
|
+
var entry = draftFromSignInInput(hit.code, true);
|
|
1170
|
+
state.signInDrafts[hit.key] = entry;
|
|
1171
|
+
Promise.resolve().then(function () {
|
|
1172
|
+
// The input is gone, so the blur was the removal itself — keep `focused`, and the card the
|
|
1173
|
+
// rebuild paints takes the focus back. (`=== false` strictly: a node without the property is
|
|
1174
|
+
// treated as connected, i.e. as a real blur.)
|
|
1175
|
+
if (hit.code.isConnected === false) return;
|
|
1176
|
+
// Only the flag of THIS write is settled: a later input/focusin write has already replaced
|
|
1177
|
+
// the entry and owns the truth about where the user is typing.
|
|
1178
|
+
if (state.signInDrafts[hit.key] === entry) entry.focused = false;
|
|
1179
|
+
});
|
|
1180
|
+
});
|
|
1181
|
+
scope.addEventListener('click', function (event) {
|
|
1182
|
+
var target = event.target;
|
|
1183
|
+
var closest = target && typeof target.closest === 'function' ? target.closest.bind(target) : null;
|
|
1184
|
+
if (!closest) return;
|
|
1185
|
+
var toggleNode = closest('[data-machine-toggle]');
|
|
1186
|
+
if (toggleNode) {
|
|
1187
|
+
event.preventDefault();
|
|
1188
|
+
toggle(toggleNode.getAttribute('data-machine-toggle'), toggleNode.getAttribute('data-machine-field'));
|
|
1189
|
+
return;
|
|
1190
|
+
}
|
|
1191
|
+
var newSession = closest('[data-machine-new-session]');
|
|
1192
|
+
if (newSession) { event.preventDefault(); newSessionHere(newSession.getAttribute('data-machine-new-session')); return; }
|
|
1193
|
+
var remove = closest('[data-machine-remove]');
|
|
1194
|
+
if (remove) { event.preventDefault(); removeMachine(remove.getAttribute('data-machine-remove')); return; }
|
|
1195
|
+
var join = closest('[data-machine-join]');
|
|
1196
|
+
if (join) { event.preventDefault(); openAddDialog({ url: join.getAttribute('data-machine-url') || '' }); return; }
|
|
1197
|
+
var signIn = closest('[data-machine-signin]');
|
|
1198
|
+
if (signIn) { event.preventDefault(); signInToo(signIn.getAttribute('data-machine-signin')); return; }
|
|
1199
|
+
// WP-3: sign a coding agent in ON another machine (the passkey row above is the CTM LOGIN).
|
|
1200
|
+
var agentSignIn = closest('[data-machine-sign-in]');
|
|
1201
|
+
if (agentSignIn) {
|
|
1202
|
+
event.preventDefault();
|
|
1203
|
+
machineSignIn(agentSignIn.getAttribute('data-machine-sign-in'), agentSignIn.getAttribute('data-machine-agent'), agentSignIn);
|
|
1204
|
+
return;
|
|
1205
|
+
}
|
|
1206
|
+
var codeSubmit = closest('[data-sign-in-code-submit]');
|
|
1207
|
+
var copyCode = closest('[data-sign-in-copy-code]');
|
|
1208
|
+
if (codeSubmit || copyCode) {
|
|
1209
|
+
event.preventDefault();
|
|
1210
|
+
var block = closest('[data-machine-sign-in-block]');
|
|
1211
|
+
// The instance id is a uuid-like string that itself contains hyphens, so it is read from
|
|
1212
|
+
// the card (never by splitting the block's own key), and the agent from the block.
|
|
1213
|
+
var card = block && typeof block.closest === 'function' ? block.closest('[data-machine-id]') : null;
|
|
1214
|
+
var machineId = card ? card.getAttribute('data-machine-id') : '';
|
|
1215
|
+
if (copyCode) {
|
|
1216
|
+
var codeEl = block && typeof block.querySelector === 'function' ? block.querySelector('[data-sign-in-user-code]') : null;
|
|
1217
|
+
copySignInCode(codeEl ? codeEl.textContent : '', copyCode);
|
|
1218
|
+
return;
|
|
1219
|
+
}
|
|
1220
|
+
submitSignInCode(machineId, block ? block.getAttribute('data-machine-agent') : '', codeSubmit);
|
|
1221
|
+
return;
|
|
1222
|
+
}
|
|
1223
|
+
if (closest('[data-machine-add]')) { event.preventDefault(); openAddDialog(); return; }
|
|
1224
|
+
if (closest('[data-machine-show-code]')) { event.preventDefault(); openShowCodeDialog(); return; }
|
|
1225
|
+
if (closest('[data-pool-leave]')) { event.preventDefault(); leavePool(); return; }
|
|
1226
|
+
if (closest('[data-machines-refresh]')) { event.preventDefault(); load({ silent: true }); }
|
|
1227
|
+
});
|
|
1228
|
+
scope.addEventListener('change', function (event) {
|
|
1229
|
+
var target = event.target;
|
|
1230
|
+
var closest = target && typeof target.closest === 'function' ? target.closest.bind(target) : null;
|
|
1231
|
+
var input = closest ? closest('[data-machine-rename]') : null;
|
|
1232
|
+
if (!input) return;
|
|
1233
|
+
rename(input.getAttribute('data-machine-rename'), input.value);
|
|
1234
|
+
});
|
|
1235
|
+
// Enter in the code box means the same thing as tapping Submit (desktop parity).
|
|
1236
|
+
scope.addEventListener('keydown', function (event) {
|
|
1237
|
+
if (!event || event.key !== 'Enter') return;
|
|
1238
|
+
var target = event.target;
|
|
1239
|
+
var closest = target && typeof target.closest === 'function' ? target.closest.bind(target) : null;
|
|
1240
|
+
var input = closest ? closest('[data-sign-in-code]') : null;
|
|
1241
|
+
if (!input) return;
|
|
1242
|
+
event.preventDefault();
|
|
1243
|
+
var block = closest('[data-machine-sign-in-block]');
|
|
1244
|
+
var card = block && typeof block.closest === 'function' ? block.closest('[data-machine-id]') : null;
|
|
1245
|
+
submitSignInCode(
|
|
1246
|
+
card ? card.getAttribute('data-machine-id') : '',
|
|
1247
|
+
block ? block.getAttribute('data-machine-agent') : '',
|
|
1248
|
+
input,
|
|
1249
|
+
);
|
|
1250
|
+
});
|
|
1251
|
+
return true;
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
return {
|
|
1255
|
+
state: state,
|
|
1256
|
+
render: render,
|
|
1257
|
+
bind: bind,
|
|
1258
|
+
load: load,
|
|
1259
|
+
loadHistories: loadHistories,
|
|
1260
|
+
headerHtml: headerHtml,
|
|
1261
|
+
listHtml: listHtml,
|
|
1262
|
+
bodyHtml: bodyHtml,
|
|
1263
|
+
machineCardHtml: machineCardHtml,
|
|
1264
|
+
selfCardHtml: selfCardHtml,
|
|
1265
|
+
signInRowHtml: signInRowHtml,
|
|
1266
|
+
// WP-3: the agent sign-in block (desktop parity) — presenters plus the two writes.
|
|
1267
|
+
signInBlockHtml: signInBlockHtml,
|
|
1268
|
+
signedOutAgents: signedOutAgents,
|
|
1269
|
+
renderSignInBlock: renderSignInBlock,
|
|
1270
|
+
machineSignIn: machineSignIn,
|
|
1271
|
+
submitSignInCode: submitSignInCode,
|
|
1272
|
+
// WP-3 fix (C)/(D): the bounded re-read and the card's own refusal copy.
|
|
1273
|
+
signInErrorText: signInErrorText,
|
|
1274
|
+
startSignInPoll: startSignInPoll,
|
|
1275
|
+
stopSignInPoll: stopSignInPoll,
|
|
1276
|
+
stopSignInPolls: stopSignInPolls,
|
|
1277
|
+
credentialBadge: credentialBadge,
|
|
1278
|
+
toggle: toggle,
|
|
1279
|
+
rename: rename,
|
|
1280
|
+
removeMachine: removeMachine,
|
|
1281
|
+
leavePool: leavePool,
|
|
1282
|
+
newSessionHere: newSessionHere,
|
|
1283
|
+
signInToo: signInToo,
|
|
1284
|
+
openAddDialog: openAddDialog,
|
|
1285
|
+
submitAddDialog: submitAddDialog,
|
|
1286
|
+
registerLegacy: registerLegacy,
|
|
1287
|
+
openShowCodeDialog: openShowCodeDialog,
|
|
1288
|
+
closeDialog: closeDialog,
|
|
1289
|
+
preview: function () { return dialog ? previewFields() : null; },
|
|
1290
|
+
dialog: function () { return dialog; },
|
|
1291
|
+
readOrigins: poolOrigins,
|
|
1292
|
+
rememberOrigins: rememberOrigins,
|
|
1293
|
+
};
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
var out = {
|
|
1297
|
+
create: create,
|
|
1298
|
+
STALE_NOTE: STALE_NOTE,
|
|
1299
|
+
HISTORY_MS: HISTORY_MS,
|
|
1300
|
+
STATUS_ID: STATUS_ID,
|
|
1301
|
+
// WP-3 fix (C): the bounded re-read's cadence and cap, so a spec asserts them instead of guessing.
|
|
1302
|
+
SIGN_IN_POLL_MS: SIGN_IN_POLL_MS,
|
|
1303
|
+
SIGN_IN_POLL_MAX_TICKS: SIGN_IN_POLL_MAX_TICKS,
|
|
1304
|
+
// picker helpers (D-1/D-2/D-3), used by the phone new-session picker and provable on their own.
|
|
1305
|
+
pickerMode: pickerMode,
|
|
1306
|
+
sheetGroups: sheetGroups,
|
|
1307
|
+
sheetRowHtml: sheetRowHtml,
|
|
1308
|
+
sheetListHtml: sheetListHtml,
|
|
1309
|
+
credentialBadge: credentialBadge,
|
|
1310
|
+
};
|
|
1311
|
+
if (typeof module === 'object' && module.exports) module.exports = out;
|
|
1312
|
+
else root.CtmPhoneMachines = out;
|
|
1313
|
+
})(typeof window !== 'undefined' ? window : this);
|