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,1252 @@
|
|
|
1
|
+
// public/js/app/machines-page.js — WP-5 (docs/machine-pool-picker-pairing-dashboard-2026-10-24.html
|
|
2
|
+
// §5 D-4/D-9/D-12/D-14, P-8, §6.4 client block, §8 WP-5, §9 S-18/S-19).
|
|
3
|
+
//
|
|
4
|
+
// The Machines page replaces Settings → Machines in place (D-14): nav target `machines`, hash
|
|
5
|
+
// `#machines`, and index.html's hash router aliases `#setup&tab=machines` onto it. This Mac is the
|
|
6
|
+
// first card; every registry member follows.
|
|
7
|
+
//
|
|
8
|
+
// * Every setting on a card is MACHINE-OWNED (D-4 rev 2): rename / favorite / hide POST
|
|
9
|
+
// /api/machine-self on the machine they describe (`?machine=<id>` for a member), never a local
|
|
10
|
+
// pref — so two primaries and a phone agree. Nothing in this file writes localStorage.
|
|
11
|
+
// * Live numbers come from the box's own host samples (D-12): one `GET /api/host/stats?since=…`
|
|
12
|
+
// per reachable row (the response carries `latest` AND the retained history) and one for This
|
|
13
|
+
// Mac. Each metric is ONE `<path>` whose gaps are `M` moves (AC-5b).
|
|
14
|
+
// * ONE poll timer (60 s) runs only while the panel is visible. It is cleared when the panel hides
|
|
15
|
+
// and when document.hidden turns true (AC-5f). `_machinesPageTimers` is the seam a test injects.
|
|
16
|
+
//
|
|
17
|
+
// Plain classic script, like public/js/app/worktrees-panel.js: the top-level statements are function
|
|
18
|
+
// declarations and literal `var`s, and it is deliberately NOT strict mode.
|
|
19
|
+
|
|
20
|
+
var MACHINES_PAGE_POLL_MS = 60000;
|
|
21
|
+
var MACHINES_PAGE_HISTORY_MS = 24 * 60 * 60 * 1000;
|
|
22
|
+
// D-12: the hub re-reads a box's sample at most once per 60 s, so a sample older than three
|
|
23
|
+
// refreshes is LABELLED, never shown as the box's current number. WP-6: the window itself is the
|
|
24
|
+
// shared MACHINE_STATS_STALE_MS (public/js/machine-pool-shared.js) — one number, every reader.
|
|
25
|
+
// The agent labels ("Claude"/"Codex") are the shared MACHINE_AGENT_LABELS in the same file.
|
|
26
|
+
|
|
27
|
+
var _machinesPageState = {
|
|
28
|
+
machines: [],
|
|
29
|
+
hubNetworkSuspect: false,
|
|
30
|
+
self: null, // /api/host/stats for This Mac: { latest, samples }
|
|
31
|
+
selfSettings: null, // /api/machine-self: { displayName, favorite, hidden }
|
|
32
|
+
selfInstanceId: '',
|
|
33
|
+
histories: Object.create(null),
|
|
34
|
+
origins: [], // the P-7 dial cache (ctm.pool.origins) — which members this device reached
|
|
35
|
+
loading: false,
|
|
36
|
+
loaded: false,
|
|
37
|
+
error: '',
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// AC-5f's seam: the panel's timer and clock are injectable so a test can prove the timer stops
|
|
41
|
+
// without sleeping. `now` is never called from a hidden path — every caller passes it in or reads it
|
|
42
|
+
// here, so the tested paths are clock-free.
|
|
43
|
+
var _machinesPageTimers = {
|
|
44
|
+
set: function (fn, ms) { return setTimeout(fn, ms); },
|
|
45
|
+
clear: function (id) { if (id != null) clearTimeout(id); },
|
|
46
|
+
now: function () { return Date.now(); },
|
|
47
|
+
};
|
|
48
|
+
var _machinesPagePollTimer = null;
|
|
49
|
+
var _machinesPageVisibilityBound = false;
|
|
50
|
+
// The nodes openAddMachineDialog() built, so the dialog is testable without a real event loop.
|
|
51
|
+
var _machinesPageDialog = null;
|
|
52
|
+
|
|
53
|
+
function machinesPageSetTimers(next) {
|
|
54
|
+
if (!next) return _machinesPageTimers;
|
|
55
|
+
if (typeof next.set === 'function') _machinesPageTimers.set = next.set;
|
|
56
|
+
if (typeof next.clear === 'function') _machinesPageTimers.clear = next.clear;
|
|
57
|
+
if (typeof next.now === 'function') _machinesPageTimers.now = next.now;
|
|
58
|
+
return _machinesPageTimers;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function machinesPageNow() {
|
|
62
|
+
var value = Number(_machinesPageTimers.now());
|
|
63
|
+
return Number.isFinite(value) ? value : 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// The one escape helper the app ships (public/js/core/01-util.js, loaded before this file). Never a
|
|
67
|
+
// raw innerHTML of server data.
|
|
68
|
+
function mEsc(value) {
|
|
69
|
+
if (typeof escHtml === 'function') return escHtml(value);
|
|
70
|
+
return String(value == null ? '' : value)
|
|
71
|
+
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ── request plumbing ────────────────────────────────────────────────────────────────────────────
|
|
75
|
+
// Moved from public/js/core/session-commands.js with the Settings card (S-18): ONE definition of the
|
|
76
|
+
// machine-write headers, owned by the page that makes the writes.
|
|
77
|
+
function _machinesHeaders() {
|
|
78
|
+
var headers = { 'Content-Type': 'application/json' };
|
|
79
|
+
if (typeof state !== 'undefined' && state && state.clientId) headers['X-CTM-Client-Id'] = state.clientId;
|
|
80
|
+
return headers;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function machinesPageToken() {
|
|
84
|
+
return (typeof state !== 'undefined' && state && state.token) ? String(state.token) : '';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// The app's convention for a mutating call (see public/js/app/worktrees-panel.js): the token rides
|
|
88
|
+
// the query string, the content type rides the headers.
|
|
89
|
+
function machinesPageUrl(path, extra) {
|
|
90
|
+
var query = 'token=' + encodeURIComponent(machinesPageToken()) + (extra ? '&' + extra : '');
|
|
91
|
+
return path + (path.indexOf('?') === -1 ? '?' : '&') + query;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function machinesPageServerError(payload, status) {
|
|
95
|
+
if (payload && typeof payload === 'object') {
|
|
96
|
+
if (payload.error) return String(payload.error);
|
|
97
|
+
if (payload.code) return String(payload.code);
|
|
98
|
+
}
|
|
99
|
+
return 'HTTP ' + status;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function machinesPageJson(path, options) {
|
|
103
|
+
var opts = options || {};
|
|
104
|
+
return fetch(path, {
|
|
105
|
+
method: opts.method || 'GET',
|
|
106
|
+
headers: opts.body ? _machinesHeaders() : undefined,
|
|
107
|
+
body: opts.body ? JSON.stringify(opts.body) : undefined,
|
|
108
|
+
cache: 'no-store',
|
|
109
|
+
}).then(function (res) {
|
|
110
|
+
return res.json().catch(function () { return null; }).then(function (data) {
|
|
111
|
+
return { ok: res.ok, status: res.status, data: data };
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function machinesPageToast(text) {
|
|
117
|
+
var message = String(text || '');
|
|
118
|
+
var panel = document.getElementById('machines-panel');
|
|
119
|
+
var node = panel ? panel.querySelector('.machines-page-toast') : null;
|
|
120
|
+
if (node) { node.textContent = message; node.classList.toggle('show', !!message); }
|
|
121
|
+
if (typeof toast === 'function') toast(message);
|
|
122
|
+
return message;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ── sparkline (AC-5b) ───────────────────────────────────────────────────────────────────────────
|
|
126
|
+
// WP-6: sparklineSvg and the series/fact formatters below are the ONE shared copy in
|
|
127
|
+
// public/js/machine-pool-shared.js (the phone renders the same sparks and the same facts). Each name
|
|
128
|
+
// stays here as a thin delegate so the page's own call sites, and any spec that names them, keep
|
|
129
|
+
// working.
|
|
130
|
+
function machinesCpuSeries(history) {
|
|
131
|
+
return machineCpuSeries(history);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function machinesMemSeries(history) {
|
|
135
|
+
return machineMemSeries(history);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function machinesHistoryRows(history) {
|
|
139
|
+
return machineHistoryRows(history);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function machinesLastValue(series) {
|
|
143
|
+
return machineLastValue(series);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ── formatting ──────────────────────────────────────────────────────────────────────────────────
|
|
147
|
+
// WP-6: the card's "how long ago" line is the shared pair machineHealthLabel / machineLastSeenText
|
|
148
|
+
// (public/js/machine-pool-shared.js), the SAME two functions the phone card calls, so a phone card
|
|
149
|
+
// and a desktop card can never disagree about a box's state or when it was last seen.
|
|
150
|
+
|
|
151
|
+
function machinesFormatBytes(value) {
|
|
152
|
+
return machineFormatBytes(value);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function machinesFormatPct(value) {
|
|
156
|
+
return machineFormatPct(value);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function machinesAgentText(agents) {
|
|
160
|
+
return machineAgentText(agents);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function machinesTaskLine(tasks) {
|
|
164
|
+
return machineTaskLine(tasks);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function machinesGpuText(gpu) {
|
|
168
|
+
return machineGpuText(gpu);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function machinesCredentialBadge(machine, isSelf) {
|
|
172
|
+
var credential = String((machine && machine.peerCredential) || (isSelf ? 'self' : 'missing'));
|
|
173
|
+
if (isSelf) return { html: '<span class="mpc-cred" data-cred="self" title="This Mac runs the sessions itself">This Mac</span>', action: '' };
|
|
174
|
+
if (credential === 'pool') {
|
|
175
|
+
return { html: '<span class="mpc-cred" data-cred="pool" title="Dialled with the shared pool secret">Pool</span>', action: '' };
|
|
176
|
+
}
|
|
177
|
+
if (credential === 'env') {
|
|
178
|
+
// P-8: an env-token box is a one-way member. "Join pool" runs D-9 against it.
|
|
179
|
+
return {
|
|
180
|
+
html: '<span class="mpc-cred" data-cred="env" title="Legacy env token — a one-way member">Env token</span>',
|
|
181
|
+
action: '<button type="button" class="mpc-btn" onclick="machinesPageJoinPool('
|
|
182
|
+
+ mEsc(JSON.stringify(String((machine && machine.instanceId) || ''))) + ','
|
|
183
|
+
+ mEsc(JSON.stringify(String((machine && machine.url) || ''))) + ')">Join pool</button>',
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
return { html: '<span class="mpc-cred" data-cred="missing" title="No credential this hub can dial it with">No credential</span>', action: '' };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function machinesStatBlock(label, value) {
|
|
190
|
+
if (!value) return '';
|
|
191
|
+
return '<div class="mpc-fact"><dt>' + mEsc(label) + '</dt><dd>' + value + '</dd></div>';
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ── P-7 "Sign in to <member> too" ───────────────────────────────────────────────────────────────
|
|
195
|
+
// The phone's twin of this row is public/m/machines.js `signInRowHtml`. Passkeys are pinned to an
|
|
196
|
+
// exact hostname (rpId = hostname), so a member can only be signed in from ITS OWN origin; this row
|
|
197
|
+
// is the one-tap way there. `ctm.pool.origins` is the same dial cache public/m/pool-failover.js
|
|
198
|
+
// keeps — the only client-side machine state, never a credential, never a machine setting (D-4).
|
|
199
|
+
function machinesPageReadPoolOrigins() {
|
|
200
|
+
var raw = null;
|
|
201
|
+
try {
|
|
202
|
+
raw = window.localStorage.getItem('ctm.pool.origins');
|
|
203
|
+
} catch (error) {
|
|
204
|
+
return []; // HANDLE: private mode / blocked storage — no dial cache means "never reached", which is the safe default
|
|
205
|
+
}
|
|
206
|
+
if (!raw) return [];
|
|
207
|
+
var parsed = null;
|
|
208
|
+
try {
|
|
209
|
+
parsed = JSON.parse(raw);
|
|
210
|
+
} catch (error) {
|
|
211
|
+
return []; // HANDLE: a corrupted cache is dropped — the page still renders, the row just shows
|
|
212
|
+
}
|
|
213
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// '' when the member has no public https origin, or when this device has already reached it.
|
|
217
|
+
function machinesPageSignInRowHtml(machine) {
|
|
218
|
+
var origin = machinePoolSignInOrigin(machine, _machinesPageState.origins);
|
|
219
|
+
if (!origin) return '';
|
|
220
|
+
var name = String((machine && (machine.displayName || machine.hostname || machine.instanceId)) || origin);
|
|
221
|
+
return '<button type="button" class="mpc-btn mpc-signin-row"'
|
|
222
|
+
+ ' title="Opens ' + mEsc(origin + '/m/') + ' — a passkey is pinned to that hostname, so it signs in there"'
|
|
223
|
+
+ ' onclick="machinesPageSignInToo(' + mEsc(JSON.stringify(origin)) + ')">'
|
|
224
|
+
+ mEsc('Sign in to ' + name + ' too') + '</button>';
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Tapping the row navigates the whole tab: the member's own origin runs its own passkey flow.
|
|
228
|
+
function machinesPageSignInToo(origin) {
|
|
229
|
+
var target = String(origin || '').trim().replace(/\/+$/, '');
|
|
230
|
+
if (!/^https:\/\/[^/?#]+$/i.test(target)) {
|
|
231
|
+
machinesPageToast('sign_in_origin_unavailable');
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
window.location.assign(target + '/m/');
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ── WP-3 sign-in: a coding agent ON a pooled machine, signed in from HERE ───────────────────────
|
|
239
|
+
// docs/remote-agent-sign-in-from-hub-browser-2026-10-25.html §5 D-7/D-9, §6, §9 S-13/S-14.
|
|
240
|
+
// The three provider routes are forwarded to the box through the existing `?machine=<id>` proxy, so
|
|
241
|
+
// the user never opens the box's own URL (a phone never could). The AI Providers card helper is
|
|
242
|
+
// reused verbatim with `machineId`, which is also why "Show sign-in tab" is never offered here: the
|
|
243
|
+
// sign-in tab lives on the box, and there is no local session for this page to activate.
|
|
244
|
+
var MACHINES_PAGE_AGENTS = ['claude', 'codex'];
|
|
245
|
+
var MACHINES_PAGE_AGENT_PRESETS = {
|
|
246
|
+
claude: { id: 'claude-code', cli: 'claude', name: 'Claude Code' },
|
|
247
|
+
codex: { id: 'codex', cli: 'codex', name: 'Codex' },
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
// Only a `signed_out` verdict earns a sign-in affordance — `signed_in` and `unknown` do not
|
|
251
|
+
// (P3 Unknown != Dead: a box that never answered is never told it is signed out).
|
|
252
|
+
function machinesPageSignedOutAgents(machine) {
|
|
253
|
+
var agents = machine && machine.agents;
|
|
254
|
+
if (!agents || typeof agents !== 'object') return [];
|
|
255
|
+
return MACHINES_PAGE_AGENTS.filter(function (agent) { return String(agents[agent] || '') === 'signed_out'; });
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function machinesPageAgentPreset(agent) {
|
|
259
|
+
return MACHINES_PAGE_AGENT_PRESETS[String(agent || '')] || null;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// The card's own action block for one machine+agent, machine-scoped. '' while no flow is open — the
|
|
263
|
+
// row's own Sign in button is the affordance then (never a second one).
|
|
264
|
+
function machinesPageSignInBlockHtml(instanceId, agent, payload) {
|
|
265
|
+
if (typeof _modelsProviderLoginActionsHtml !== 'function') return '';
|
|
266
|
+
var preset = machinesPageAgentPreset(agent);
|
|
267
|
+
if (!preset || !instanceId) return '';
|
|
268
|
+
var providerId = String(agent) === 'codex' ? 'codex' : 'claude';
|
|
269
|
+
var entry = ((payload && payload.providers) || {})[providerId] || null;
|
|
270
|
+
return _modelsProviderLoginActionsHtml(preset, entry ? entry.flow : null, { machineId: String(instanceId) });
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// E (fix round 2): the LAST machine-scoped login-state payload per '<instanceId>-<agent>'. The page's
|
|
274
|
+
// 60 s poll calls renderMachinesPage() twice, which rebuilds every card from scratch — and the page
|
|
275
|
+
// keeps no per-machine login state of its own, so without this cache an open sign-in (the link, the
|
|
276
|
+
// device code, the code box, its draft, and the row button's "Opening..." state) was wiped every
|
|
277
|
+
// minute, mid-sign-in. `machinesPageRenderSignInBlock` fills this on every 2 s tick; the closed-flow
|
|
278
|
+
// branch deletes the key, so the next full render paints the plain row again.
|
|
279
|
+
var _machinesPageSignInPayloads = {};
|
|
280
|
+
|
|
281
|
+
// One Sign in button plus the block it fills, per signed-out agent, directly under the Agents fact.
|
|
282
|
+
// The block carries `data-machine-agent` as well as its own `<id>-<agent>` key: the delegated
|
|
283
|
+
// handlers read the AGENT off the block and the INSTANCE ID off the card's `data-machine-id`
|
|
284
|
+
// (an instance id is uuid-like and itself contains hyphens, so the key is never split).
|
|
285
|
+
//
|
|
286
|
+
// E.2: when a tick has already delivered this machine+agent's payload, the block is painted from it
|
|
287
|
+
// here instead of staying empty, and an OPEN flow renders the row button as the disabled "Opening..."
|
|
288
|
+
// the POST set (`_modelsProviderLoginStart`'s exact label) — otherwise the 60 s re-render hands back
|
|
289
|
+
// an enabled "Sign in" and a second click races the flow that is already running.
|
|
290
|
+
function machinesPageSignInHtml(machine) {
|
|
291
|
+
var id = String((machine && machine.instanceId) || '');
|
|
292
|
+
if (!id) return '';
|
|
293
|
+
return machinesPageSignedOutAgents(machine).map(function (agent) {
|
|
294
|
+
var key = id + '-' + agent;
|
|
295
|
+
var cached = _machinesPageSignInPayloads[key] || null;
|
|
296
|
+
var entry = cached ? (((cached.providers || {})[machinesPageSignInProviderId(agent)]) || null) : null;
|
|
297
|
+
var opening = !!(entry && entry.flow && machinesPageSignInFlowIsOpen(entry.flow));
|
|
298
|
+
return '<div class="mpc-fact mpc-signin" data-machine-signin-for="' + mEsc(key) + '">'
|
|
299
|
+
+ '<button type="button" class="mpc-btn mpc-signin-btn" data-machine-sign-in="' + mEsc(id) + '"'
|
|
300
|
+
+ ' data-machine-agent="' + mEsc(agent) + '"'
|
|
301
|
+
+ (opening ? ' disabled' : '')
|
|
302
|
+
+ ' onclick="machinesPageSignIn(' + mEsc(JSON.stringify(id)) + ',' + mEsc(JSON.stringify(agent)) + ',this)">'
|
|
303
|
+
+ (opening ? 'Opening...' : 'Sign in') + '</button>'
|
|
304
|
+
+ '<div class="mpc-signin-block" data-machine-sign-in-block="' + mEsc(key) + '"'
|
|
305
|
+
+ ' data-machine-agent="' + mEsc(agent) + '">'
|
|
306
|
+
+ (cached ? machinesPageSignInBlockHtml(id, agent, cached) : '')
|
|
307
|
+
+ '</div>'
|
|
308
|
+
+ '</div>';
|
|
309
|
+
}).join('');
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// The provider id a card agent maps to. One spelling, used by the click, the submit and the
|
|
313
|
+
// fallback transport (a second inline copy is how the two drift).
|
|
314
|
+
function machinesPageSignInProviderId(agent) {
|
|
315
|
+
return String(agent) === 'codex' ? 'codex' : 'claude';
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// The statuses that mean "the box is still running this sign-in". The card helper is the authority
|
|
319
|
+
// when this bundle has it; the fallback is the same three the phone module and the broker use, so a
|
|
320
|
+
// unit slice of this page can still tell an open flow from a finished one.
|
|
321
|
+
var MACHINES_PAGE_SIGN_IN_OPEN_STATUSES = ['starting', 'running', 'awaiting_user'];
|
|
322
|
+
function machinesPageSignInFlowIsOpen(flow) {
|
|
323
|
+
if (typeof _modelsProviderLoginFlowIsOpen === 'function') return !!_modelsProviderLoginFlowIsOpen(flow);
|
|
324
|
+
return !!flow && MACHINES_PAGE_SIGN_IN_OPEN_STATUSES.indexOf(String(flow.status || '')) >= 0;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// WP-2 fix (A) on the Machines page: what a login-state payload contributes to the block. The card's
|
|
328
|
+
// own fingerprint is used when this bundle has it (it drops `account.checkedAt`, the probe's
|
|
329
|
+
// timestamp, which is not rendered anywhere); the JSON fallback keeps a unit slice honest.
|
|
330
|
+
function machinesPageSignInRenderKey(payload) {
|
|
331
|
+
if (typeof _modelsProviderLoginRenderKey === 'function') return String(_modelsProviderLoginRenderKey(payload));
|
|
332
|
+
try { return JSON.stringify(payload === undefined ? null : payload); } catch (e) { return String(payload); }
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// The draft the repaint must not destroy: the typed code (with caret and focus) and the refusal copy.
|
|
336
|
+
// This is the defect WP-2 fixed on the card — between two poll ticks only `account.checkedAt` moves,
|
|
337
|
+
// and replacing the input mid-typing drops the value, the caret and the focus.
|
|
338
|
+
function machinesPageCaptureSignInDraft(node) {
|
|
339
|
+
var code = node && typeof node.querySelector === 'function' ? node.querySelector('[data-sign-in-code]') : null;
|
|
340
|
+
var error = node && typeof node.querySelector === 'function' ? node.querySelector('[data-sign-in-error]') : null;
|
|
341
|
+
var doc = (node && node.ownerDocument) || (typeof document !== 'undefined' ? document : null);
|
|
342
|
+
return {
|
|
343
|
+
value: code && code.value !== undefined && code.value !== null ? String(code.value) : null,
|
|
344
|
+
selectionStart: code && typeof code.selectionStart === 'number' ? code.selectionStart : null,
|
|
345
|
+
selectionEnd: code && typeof code.selectionEnd === 'number' ? code.selectionEnd : null,
|
|
346
|
+
focused: !!(doc && doc.activeElement && doc.activeElement === code),
|
|
347
|
+
error: error ? String(error.textContent == null ? '' : error.textContent) : null,
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function machinesPageRestoreSignInDraft(node, draft) {
|
|
352
|
+
if (!node || !draft || typeof node.querySelector !== 'function') return false;
|
|
353
|
+
var code = node.querySelector('[data-sign-in-code]');
|
|
354
|
+
if (code) {
|
|
355
|
+
if (draft.value !== null && code.value !== undefined) code.value = draft.value;
|
|
356
|
+
if (draft.focused) {
|
|
357
|
+
if (typeof code.focus === 'function') code.focus();
|
|
358
|
+
if (typeof code.setSelectionRange === 'function' && draft.selectionStart !== null && draft.selectionEnd !== null) {
|
|
359
|
+
code.setSelectionRange(draft.selectionStart, draft.selectionEnd);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
if (draft.error !== null) {
|
|
364
|
+
var error = node.querySelector('[data-sign-in-error]');
|
|
365
|
+
if (error) error.textContent = draft.error;
|
|
366
|
+
}
|
|
367
|
+
return true;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Repaint ONE machine's sign-in block from a machine-scoped login-state payload. Never a full page
|
|
371
|
+
// render: the page's 60 s poll owns the rest of the card.
|
|
372
|
+
//
|
|
373
|
+
// Two gates, both from WP-2: (1) an unchanged fingerprint does not touch the DOM at all, so a tick
|
|
374
|
+
// that only re-stamped `account.checkedAt` leaves the block — and the code being typed into it —
|
|
375
|
+
// alone; (2) when the flow has FINISHED, the card's Agents fact and Sign in row are re-read once
|
|
376
|
+
// (remembered on the node) instead of waiting for the 60 s tick.
|
|
377
|
+
function machinesPageRenderSignInBlock(instanceId, agent, payload) {
|
|
378
|
+
var panel = document.getElementById('machines-panel');
|
|
379
|
+
if (!panel || typeof panel.querySelector !== 'function') return false;
|
|
380
|
+
var node = panel.querySelector('[data-machine-sign-in-block="' + String(instanceId) + '-' + String(agent) + '"]');
|
|
381
|
+
if (!node) return false;
|
|
382
|
+
// E.1: remember the payload BEFORE the gate, so the next full page render can paint this block from
|
|
383
|
+
// it (the page keeps no per-machine login state of its own). Every tick stores the same object.
|
|
384
|
+
var cacheKey = String(instanceId) + '-' + String(agent);
|
|
385
|
+
_machinesPageSignInPayloads[cacheKey] = payload;
|
|
386
|
+
var key = machinesPageSignInRenderKey(payload);
|
|
387
|
+
var dataset = node.dataset || null;
|
|
388
|
+
if (dataset && dataset.renderKey === key) return false;
|
|
389
|
+
var draft = machinesPageCaptureSignInDraft(node);
|
|
390
|
+
node.innerHTML = machinesPageSignInBlockHtml(instanceId, agent, payload);
|
|
391
|
+
machinesPageRestoreSignInDraft(node, draft);
|
|
392
|
+
if (dataset) dataset.renderKey = key;
|
|
393
|
+
|
|
394
|
+
var entry = ((payload && payload.providers) || {})[machinesPageSignInProviderId(agent)] || null;
|
|
395
|
+
var flow = entry && entry.flow;
|
|
396
|
+
var closedId = flow && !machinesPageSignInFlowIsOpen(flow)
|
|
397
|
+
? String(flow.id || flow.sessionId || flow.status || 'closed')
|
|
398
|
+
: '';
|
|
399
|
+
if (closedId) {
|
|
400
|
+
// E.1: the flow is over — drop the cache so the next full render paints the plain, enabled row.
|
|
401
|
+
delete _machinesPageSignInPayloads[cacheKey];
|
|
402
|
+
if (!dataset || dataset.closedFlowId !== closedId) {
|
|
403
|
+
if (dataset) dataset.closedFlowId = closedId;
|
|
404
|
+
// The row itself (plain, enabled "Sign in"; empty block) is repainted NOW from the page's own
|
|
405
|
+
// state — machinesPageRefreshSurfaces only re-reads the picker, and the 60 s tick is too late
|
|
406
|
+
// for a button that still says "Opening..." over a finished flow. The verdict (signed_in) still
|
|
407
|
+
// arrives with the hub's next agent-auth refresh; until then the row is truthfully signed_out.
|
|
408
|
+
renderMachinesPage();
|
|
409
|
+
machinesPageRefreshSurfaces();
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
return true;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// E.3: renderMachinesPage() rebuilds the panel from scratch, which would drop every open block's
|
|
416
|
+
// draft, its renderKey (one wasted rewrite on the next tick) and its closedFlowId memo. This pairs a
|
|
417
|
+
// capture-before-paint / adopt-after-paint around the ONE innerHTML write, so the page render stays
|
|
418
|
+
// one statement and no timer is involved.
|
|
419
|
+
function machinesPageCollectSignInBlocks(panel) {
|
|
420
|
+
var out = [];
|
|
421
|
+
var walk = function (node) {
|
|
422
|
+
var kids = node && node.children;
|
|
423
|
+
if (!kids) return;
|
|
424
|
+
for (var i = 0; i < kids.length; i += 1) {
|
|
425
|
+
var child = kids[i];
|
|
426
|
+
var key = child && typeof child.getAttribute === 'function'
|
|
427
|
+
? String(child.getAttribute('data-machine-sign-in-block') || '')
|
|
428
|
+
: '';
|
|
429
|
+
if (key) {
|
|
430
|
+
out.push({
|
|
431
|
+
key: key,
|
|
432
|
+
draft: machinesPageCaptureSignInDraft(child),
|
|
433
|
+
closedFlowId: (child.dataset && child.dataset.closedFlowId) || '',
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
walk(child);
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
walk(panel);
|
|
440
|
+
return out;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function machinesPageAdoptSignInBlocks(panel, carried) {
|
|
444
|
+
if (!carried || !carried.length) return 0;
|
|
445
|
+
var byKey = {};
|
|
446
|
+
carried.forEach(function (entry) { byKey[entry.key] = entry; });
|
|
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 carriedEntry = key ? byKey[key] : null;
|
|
457
|
+
if (carriedEntry) {
|
|
458
|
+
machinesPageRestoreSignInDraft(child, carriedEntry.draft);
|
|
459
|
+
if (child.dataset) {
|
|
460
|
+
if (carriedEntry.closedFlowId) child.dataset.closedFlowId = carriedEntry.closedFlowId;
|
|
461
|
+
// The content was just rendered from the cached payload, so the next identical tick must be
|
|
462
|
+
// a no-op rather than a second identical write.
|
|
463
|
+
var cached = _machinesPageSignInPayloads[key];
|
|
464
|
+
if (cached) child.dataset.renderKey = machinesPageSignInRenderKey(cached);
|
|
465
|
+
}
|
|
466
|
+
adopted += 1;
|
|
467
|
+
}
|
|
468
|
+
walk(child);
|
|
469
|
+
}
|
|
470
|
+
};
|
|
471
|
+
walk(panel);
|
|
472
|
+
return adopted;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function machinesPageCarrySignInBlocks(panel, paint) {
|
|
476
|
+
var carried = machinesPageCollectSignInBlocks(panel);
|
|
477
|
+
if (typeof paint === 'function') paint();
|
|
478
|
+
return machinesPageAdoptSignInBlocks(panel, carried);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// ── delegations: the block's own Submit / Enter / Copy ───────────────────────────────────────────
|
|
482
|
+
// The card's handlers live inside renderModelsCodingAgentRoutes and are scoped to the Coding Agents
|
|
483
|
+
// section, so the Machines page needs its own. ONE delegated listener on #machines-panel (guarded by
|
|
484
|
+
// a dataset flag, the way the phone module's `scope.dataset.mpBound` is): the panel's innerHTML is
|
|
485
|
+
// replaced on every render, the panel element itself is not, so a single binding outlives them all.
|
|
486
|
+
function machinesPageAttr(node, name) {
|
|
487
|
+
if (!node) return '';
|
|
488
|
+
if (typeof node.getAttribute === 'function') {
|
|
489
|
+
var value = node.getAttribute(name);
|
|
490
|
+
if (value !== null && value !== undefined) return String(value);
|
|
491
|
+
}
|
|
492
|
+
if (node.dataset) {
|
|
493
|
+
var camel = String(name).replace(/^data-/, '').replace(/-([a-z])/g, function (m, ch) { return ch.toUpperCase(); });
|
|
494
|
+
if (node.dataset[camel] !== null && node.dataset[camel] !== undefined) return String(node.dataset[camel]);
|
|
495
|
+
}
|
|
496
|
+
return '';
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// What the event's target is inside: the block it belongs to, the machine that owns it, the agent,
|
|
500
|
+
// and the three elements the card helper needs. null when the target is not part of a sign-in block.
|
|
501
|
+
function machinesPageSignInTarget(node) {
|
|
502
|
+
if (!node || typeof node.closest !== 'function') return null;
|
|
503
|
+
var submitBtn = node.closest('[data-sign-in-code-submit]');
|
|
504
|
+
var copyBtn = node.closest('[data-sign-in-copy-code]');
|
|
505
|
+
var codeInput = node.closest('[data-sign-in-code]');
|
|
506
|
+
if (!submitBtn && !copyBtn && !codeInput) return null;
|
|
507
|
+
var block = node.closest('[data-machine-sign-in-block]');
|
|
508
|
+
if (!block) return null;
|
|
509
|
+
var card = typeof block.closest === 'function' ? block.closest('[data-machine-id]') : null;
|
|
510
|
+
var machineId = machinesPageAttr(card, 'data-machine-id');
|
|
511
|
+
var agent = machinesPageAttr(block, 'data-machine-agent');
|
|
512
|
+
if (!machineId || !agent) return null;
|
|
513
|
+
var query = typeof block.querySelector === 'function' ? function (sel) { return block.querySelector(sel); } : function () { return null; };
|
|
514
|
+
return {
|
|
515
|
+
kind: copyBtn ? 'copy' : 'submit',
|
|
516
|
+
block: block,
|
|
517
|
+
machineId: machineId,
|
|
518
|
+
agent: agent,
|
|
519
|
+
inputEl: query('[data-sign-in-code]'),
|
|
520
|
+
errorEl: query('[data-sign-in-error]'),
|
|
521
|
+
submitBtn: submitBtn || query('[data-sign-in-code-submit]'),
|
|
522
|
+
copyBtn: copyBtn,
|
|
523
|
+
codeEl: query('[data-sign-in-user-code]'),
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// One POST, one meaning: the pasted code goes to the flow's own provider ON that machine.
|
|
528
|
+
function machinesPageSubmitSignInCode(hit) {
|
|
529
|
+
if (!hit || typeof _modelsProviderLoginSubmitCode !== 'function' || !hit.inputEl) return false;
|
|
530
|
+
_modelsProviderLoginSubmitCode(
|
|
531
|
+
machinesPageSignInProviderId(hit.agent),
|
|
532
|
+
hit.inputEl,
|
|
533
|
+
hit.errorEl,
|
|
534
|
+
hit.submitBtn,
|
|
535
|
+
{ machineId: hit.machineId },
|
|
536
|
+
);
|
|
537
|
+
return true;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function machinesPageSignInClick(ev) {
|
|
541
|
+
var hit = machinesPageSignInTarget(ev && ev.target);
|
|
542
|
+
if (!hit) return false;
|
|
543
|
+
if (ev && typeof ev.preventDefault === 'function') ev.preventDefault();
|
|
544
|
+
if (hit.kind === 'copy') {
|
|
545
|
+
if (hit.codeEl && typeof _modelsProviderLoginCopyCode === 'function') {
|
|
546
|
+
_modelsProviderLoginCopyCode(hit.codeEl.textContent, hit.copyBtn);
|
|
547
|
+
}
|
|
548
|
+
return true;
|
|
549
|
+
}
|
|
550
|
+
return machinesPageSubmitSignInCode(hit);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function machinesPageSignInKeydown(ev) {
|
|
554
|
+
if (!ev || ev.key !== 'Enter') return false;
|
|
555
|
+
var hit = machinesPageSignInTarget(ev.target);
|
|
556
|
+
if (!hit || hit.kind !== 'submit' || !hit.inputEl) return false;
|
|
557
|
+
if (typeof ev.preventDefault === 'function') ev.preventDefault();
|
|
558
|
+
return machinesPageSubmitSignInCode(hit);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function machinesPageSignInBind(panel) {
|
|
562
|
+
var node = panel || (typeof document !== 'undefined' ? document.getElementById('machines-panel') : null);
|
|
563
|
+
if (!node || typeof node.addEventListener !== 'function') return false;
|
|
564
|
+
if (node.dataset && node.dataset.mpcSignInBound === '1') return false;
|
|
565
|
+
if (node.dataset) node.dataset.mpcSignInBound = '1';
|
|
566
|
+
node.addEventListener('click', machinesPageSignInClick);
|
|
567
|
+
node.addEventListener('keydown', machinesPageSignInKeydown);
|
|
568
|
+
return true;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// The click on the row's own Sign in button. The AI Providers card's own helper does the POST and
|
|
572
|
+
// then its ONE 2 s poll, scoped to this machine (no second timer family); the page only repaints the
|
|
573
|
+
// block the payload answers for.
|
|
574
|
+
function machinesPageSignIn(instanceId, agent, btn) {
|
|
575
|
+
var id = String(instanceId || '');
|
|
576
|
+
var preset = machinesPageAgentPreset(agent);
|
|
577
|
+
if (!id || !preset) return false;
|
|
578
|
+
var repaint = function (payload) { machinesPageRenderSignInBlock(id, agent, payload); };
|
|
579
|
+
if (typeof _modelsProviderLoginStart === 'function') {
|
|
580
|
+
Promise.resolve(_modelsProviderLoginStart(preset, btn, { machineId: id, onPayload: repaint }))
|
|
581
|
+
.catch(function () {
|
|
582
|
+
// HANDLE: the card helper already surfaces its own refusal (toast); the row is left as it was.
|
|
583
|
+
return null;
|
|
584
|
+
});
|
|
585
|
+
return true;
|
|
586
|
+
}
|
|
587
|
+
// No AI Providers module on this page (a unit slice, or an older bundle): the page's own transport
|
|
588
|
+
// on the same route with the same scope, so the click still reaches the box.
|
|
589
|
+
var providerId = machinesPageSignInProviderId(agent);
|
|
590
|
+
machinesPageJson(machinesPageUrl('/api/providers/' + providerId + '/login', 'machine=' + encodeURIComponent(id)), { method: 'POST', body: {} })
|
|
591
|
+
.then(function (result) {
|
|
592
|
+
if (result.ok) machinesPageToast('Sign-in started on the machine');
|
|
593
|
+
else machinesPageToast(machinesPageServerError(result.data, result.status));
|
|
594
|
+
return result;
|
|
595
|
+
})
|
|
596
|
+
.catch(function () {
|
|
597
|
+
// HANDLE: a transport failure is reported, never swallowed; the row stays as it was.
|
|
598
|
+
machinesPageToast('sign_in_unavailable');
|
|
599
|
+
return null;
|
|
600
|
+
});
|
|
601
|
+
return true;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// ── cards ───────────────────────────────────────────────────────────────────────────────────────
|
|
605
|
+
// `machine` is a /api/machines row; `history` is the box's own host samples (array or {samples}).
|
|
606
|
+
function machineCardHtml(machine, history) {
|
|
607
|
+
var m = machine || {};
|
|
608
|
+
var id = String(m.instanceId || '');
|
|
609
|
+
var name = String(m.displayName || m.hostname || m.url || id || '');
|
|
610
|
+
var health = String(m.health || 'unknown').toLowerCase();
|
|
611
|
+
var sample = (m.stats && m.stats.sample) || null;
|
|
612
|
+
var statsAt = Number((m.stats && m.stats.checkedAt) || 0);
|
|
613
|
+
var stale = !statsAt || (machinesPageNow() - statsAt) > MACHINE_STATS_STALE_MS;
|
|
614
|
+
var cpuSeries = machinesCpuSeries(history);
|
|
615
|
+
var memSeries = machinesMemSeries(history);
|
|
616
|
+
var cpuNow = sample && sample.cpuPct != null ? Number(sample.cpuPct) : machinesLastValue(cpuSeries);
|
|
617
|
+
var memNow = machinesLastValue(memSeries);
|
|
618
|
+
var credential = machinesCredentialBadge(m, false);
|
|
619
|
+
var agents = machinesAgentText(m.agents);
|
|
620
|
+
var facts = ''
|
|
621
|
+
+ machinesStatBlock('Load', sample && sample.load1 != null ? mEsc(Number(sample.load1).toFixed(2)) : '')
|
|
622
|
+
+ machinesStatBlock('Cores', sample && sample.cores != null ? mEsc(String(sample.cores)) : '')
|
|
623
|
+
+ machinesStatBlock('Disk free', sample ? mEsc(machinesFormatBytes(sample.diskFreeBytes)) : '')
|
|
624
|
+
+ machinesStatBlock('GPU', mEsc(machinesGpuText(sample && sample.gpu)))
|
|
625
|
+
+ machinesStatBlock('Tasks', mEsc(machinesTaskLine(sample && sample.tasks)))
|
|
626
|
+
+ machinesStatBlock('Agents', mEsc(agents));
|
|
627
|
+
var staleNote = stale && (sample || statsAt)
|
|
628
|
+
? '<span class="mpc-stale" title="The hub re-reads a box at most once a minute; this sample is older than three">stats stale</span>'
|
|
629
|
+
: '';
|
|
630
|
+
return '<article class="machine-page-card" data-machine-id="' + mEsc(id) + '" data-health="' + mEsc(health) + '">'
|
|
631
|
+
+ '<div class="mpc-head">'
|
|
632
|
+
+ '<span class="mpc-dot" data-health="' + mEsc(health) + '" title="' + mEsc(health) + '"></span>'
|
|
633
|
+
+ '<input class="mpc-name" type="text" maxlength="80" spellcheck="false" value="' + mEsc(name) + '"'
|
|
634
|
+
+ ' title="' + mEsc(name) + '" aria-label="Machine name"'
|
|
635
|
+
+ ' onchange="machinesPageRename(' + mEsc(JSON.stringify(id)) + ', this.value, this)">'
|
|
636
|
+
+ credential.html
|
|
637
|
+
+ '</div>'
|
|
638
|
+
+ '<div class="mpc-meta">' + mEsc([machineHealthLabel(m), machineLastSeenText(m), [m.os, m.arch].filter(Boolean).join('/')].filter(Boolean).join(' · '))
|
|
639
|
+
+ ' ' + staleNote + '</div>'
|
|
640
|
+
+ '<div class="mpc-sparks">'
|
|
641
|
+
+ '<div class="mpc-spark"><span class="mpc-spark-label">CPU</span>' + sparklineSvg(cpuSeries) + '<span class="mpc-spark-val">' + mEsc(machinesFormatPct(cpuNow)) + '</span></div>'
|
|
642
|
+
+ '<div class="mpc-spark"><span class="mpc-spark-label">MEM</span>' + sparklineSvg(memSeries) + '<span class="mpc-spark-val">' + mEsc(machinesFormatPct(memNow)) + '</span></div>'
|
|
643
|
+
+ '</div>'
|
|
644
|
+
+ '<dl class="mpc-facts">' + facts + '</dl>'
|
|
645
|
+
+ machinesPageSignInHtml(m)
|
|
646
|
+
+ '<div class="mpc-actions">'
|
|
647
|
+
+ '<button type="button" class="mpc-btn mpc-fav" aria-pressed="' + (m.favorite ? 'true' : 'false') + '"'
|
|
648
|
+
+ ' title="Favorite — shown first in the picker" onclick="machinesPageToggle(' + mEsc(JSON.stringify(id)) + ',\'favorite\')">'
|
|
649
|
+
+ (m.favorite ? '★ Favorite' : '☆ Favorite') + '</button>'
|
|
650
|
+
+ '<button type="button" class="mpc-btn" onclick="machinesPageNewSessionHere(' + mEsc(JSON.stringify(id)) + ')">New session here</button>'
|
|
651
|
+
+ '<button type="button" class="mpc-btn" onclick="machinesPageToggle(' + mEsc(JSON.stringify(id)) + ',\'hidden\')">'
|
|
652
|
+
+ (m.hidden ? 'Show in picker' : 'Hide from picker') + '</button>'
|
|
653
|
+
+ credential.action
|
|
654
|
+
+ '<button type="button" class="mpc-btn mpc-danger" onclick="machinesPageConfirmRemove(' + mEsc(JSON.stringify(id)) + ')">Remove</button>'
|
|
655
|
+
+ '</div>'
|
|
656
|
+
+ machinesPageSignInRowHtml(m)
|
|
657
|
+
+ '</article>';
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// "This Mac" is not a registry row: its settings are /api/machine-self and its numbers are its own
|
|
661
|
+
// host sample. It renders first (AC-5a) and its actions are the same three settings writes.
|
|
662
|
+
function machinesSelfCardHtml() {
|
|
663
|
+
var settings = _machinesPageState.selfSettings || {};
|
|
664
|
+
var latest = (_machinesPageState.self && _machinesPageState.self.latest) || null;
|
|
665
|
+
var history = _machinesPageState.self || null;
|
|
666
|
+
var id = String(_machinesPageState.selfInstanceId || '');
|
|
667
|
+
var name = String(settings.displayName || 'This Mac');
|
|
668
|
+
var cpuSeries = machinesCpuSeries(history);
|
|
669
|
+
var memSeries = machinesMemSeries(history);
|
|
670
|
+
var cpuNow = latest && latest.cpuPct != null ? Number(latest.cpuPct) : machinesLastValue(cpuSeries);
|
|
671
|
+
var memNow = machinesLastValue(memSeries);
|
|
672
|
+
var facts = ''
|
|
673
|
+
+ machinesStatBlock('Load', latest && latest.load1 != null ? mEsc(Number(latest.load1).toFixed(2)) : '')
|
|
674
|
+
+ machinesStatBlock('Cores', latest && latest.cores != null ? mEsc(String(latest.cores)) : '')
|
|
675
|
+
+ machinesStatBlock('Disk free', latest ? mEsc(machinesFormatBytes(latest.diskFreeBytes)) : '')
|
|
676
|
+
+ machinesStatBlock('GPU', mEsc(machinesGpuText(latest && latest.gpu)))
|
|
677
|
+
+ machinesStatBlock('Tasks', mEsc(machinesTaskLine(latest && latest.tasks)))
|
|
678
|
+
+ machinesStatBlock('Memory', latest ? mEsc(machinesFormatBytes(latest.memUsedBytes) + ' / ' + machinesFormatBytes(latest.memTotalBytes)) : '');
|
|
679
|
+
var poolNote = _machinesPageState.machines.length
|
|
680
|
+
? 'In a pool with ' + _machinesPageState.machines.length + ' other machine' + (_machinesPageState.machines.length === 1 ? '' : 's')
|
|
681
|
+
: 'Not in a pool yet';
|
|
682
|
+
return '<article class="machine-page-card machine-page-self" data-machine-id="' + mEsc(id) + '" data-health="reachable">'
|
|
683
|
+
+ '<div class="mpc-head">'
|
|
684
|
+
+ '<span class="mpc-dot" data-health="reachable" title="reachable"></span>'
|
|
685
|
+
+ '<input class="mpc-name" type="text" maxlength="80" spellcheck="false" value="' + mEsc(name) + '"'
|
|
686
|
+
+ ' title="' + mEsc(name) + '" aria-label="This Mac name"'
|
|
687
|
+
+ ' onchange="machinesPageRename(\'\', this.value, this)">'
|
|
688
|
+
+ '<span class="mpc-cred" data-cred="self" title="This machine runs its own sessions">This Mac</span>'
|
|
689
|
+
+ '</div>'
|
|
690
|
+
+ '<div class="mpc-meta">' + mEsc(poolNote) + (id ? ' · ' + mEsc(id) : '') + '</div>'
|
|
691
|
+
+ '<div class="mpc-sparks">'
|
|
692
|
+
+ '<div class="mpc-spark"><span class="mpc-spark-label">CPU</span>' + sparklineSvg(cpuSeries) + '<span class="mpc-spark-val">' + mEsc(machinesFormatPct(cpuNow)) + '</span></div>'
|
|
693
|
+
+ '<div class="mpc-spark"><span class="mpc-spark-label">MEM</span>' + sparklineSvg(memSeries) + '<span class="mpc-spark-val">' + mEsc(machinesFormatPct(memNow)) + '</span></div>'
|
|
694
|
+
+ '</div>'
|
|
695
|
+
+ '<dl class="mpc-facts">' + facts + '</dl>'
|
|
696
|
+
+ '<div class="mpc-actions">'
|
|
697
|
+
+ '<button type="button" class="mpc-btn mpc-fav" aria-pressed="' + (settings.favorite ? 'true' : 'false') + '"'
|
|
698
|
+
+ ' onclick="machinesPageToggle(\'\',\'favorite\')">' + (settings.favorite ? '★ Favorite' : '☆ Favorite') + '</button>'
|
|
699
|
+
+ '<button type="button" class="mpc-btn" onclick="machinesPageNewSessionHere(\'\')">New session here</button>'
|
|
700
|
+
+ '<button type="button" class="mpc-btn" onclick="machinesPageToggle(\'\',\'hidden\')">'
|
|
701
|
+
+ (settings.hidden ? 'Show in picker' : 'Hide from picker') + '</button>'
|
|
702
|
+
+ '</div>'
|
|
703
|
+
+ '</article>';
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
function machinesPageHeaderHtml() {
|
|
707
|
+
var inPool = _machinesPageState.machines.length > 0;
|
|
708
|
+
return '<div class="machines-page-header">'
|
|
709
|
+
+ '<div>'
|
|
710
|
+
+ '<h2>Machines</h2>'
|
|
711
|
+
+ '<p>Other CTM instances this one can spawn sessions on and read sessions from. '
|
|
712
|
+
+ 'Names, favorites and visibility live on the machine they describe.</p>'
|
|
713
|
+
+ '</div>'
|
|
714
|
+
+ '<div class="machines-page-actions">'
|
|
715
|
+
+ '<button type="button" class="mpc-btn mpc-primary" onclick="openAddMachineDialog()">+ Add machine</button>'
|
|
716
|
+
+ '<button type="button" class="mpc-btn" onclick="openShowCodeDialog()">Show a code for this machine</button>'
|
|
717
|
+
+ (inPool ? '<button type="button" class="mpc-btn mpc-danger" onclick="machinesPageLeavePool()">Leave pool</button>' : '')
|
|
718
|
+
+ '<button type="button" class="mpc-btn" onclick="loadMachinesPanel()" title="Refresh">↻</button>'
|
|
719
|
+
+ '</div>'
|
|
720
|
+
+ '</div>';
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
function machinesPageBodyHtml() {
|
|
724
|
+
if (_machinesPageState.error) {
|
|
725
|
+
return '<div class="machines-page-error" role="status">' + mEsc(_machinesPageState.error) + '</div>';
|
|
726
|
+
}
|
|
727
|
+
if (!_machinesPageState.loaded && _machinesPageState.loading) {
|
|
728
|
+
return '<div class="machines-page-loading">Loading machines…</div>';
|
|
729
|
+
}
|
|
730
|
+
var cards = machinesSelfCardHtml();
|
|
731
|
+
var suspect = _machinesPageState.hubNetworkSuspect
|
|
732
|
+
? '<div class="machines-page-note">hub network suspect — a row may be stale</div>'
|
|
733
|
+
: '';
|
|
734
|
+
var rows = _machinesPageState.machines.map(function (machine) {
|
|
735
|
+
return machineCardHtml(machine, _machinesPageState.histories[String(machine && machine.instanceId || '')]);
|
|
736
|
+
}).join('');
|
|
737
|
+
if (!rows && !suspect) {
|
|
738
|
+
rows = '<div class="machines-page-note">No other machines registered yet — use “Add machine”.</div>';
|
|
739
|
+
}
|
|
740
|
+
return cards + suspect + rows;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
function renderMachinesPage() {
|
|
744
|
+
var panel = document.getElementById('machines-panel');
|
|
745
|
+
if (!panel) return false;
|
|
746
|
+
// WP-3 fix (A): the sign-in block's own Submit / Enter / Copy live on ONE delegated listener on
|
|
747
|
+
// the panel, bound here — the first render happens before any card's block can be typed into.
|
|
748
|
+
machinesPageSignInBind(panel);
|
|
749
|
+
// E.3: the panel's markup is replaced wholesale here; the open sign-in blocks are carried across
|
|
750
|
+
// the write (draft, renderKey, closedFlowId) so a page render never loses a sign-in in progress.
|
|
751
|
+
machinesPageCarrySignInBlocks(panel, function () {
|
|
752
|
+
panel.innerHTML = machinesPageHeaderHtml()
|
|
753
|
+
+ '<div class="machines-page-body" role="status" aria-live="polite">' + machinesPageBodyHtml() + '</div>'
|
|
754
|
+
+ '<div class="machines-page-toast" role="status" aria-live="polite"></div>';
|
|
755
|
+
});
|
|
756
|
+
return true;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// ── loading ─────────────────────────────────────────────────────────────────────────────────────
|
|
760
|
+
function machinesPageHistoryQuery(machineId, now) {
|
|
761
|
+
var since = Math.max(0, Number(now) - MACHINES_PAGE_HISTORY_MS);
|
|
762
|
+
var query = 'since=' + Math.trunc(since);
|
|
763
|
+
if (machineId) query += '&machine=' + encodeURIComponent(machineId);
|
|
764
|
+
return query;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function loadMachinesPanel() {
|
|
768
|
+
var panel = document.getElementById('machines-panel');
|
|
769
|
+
if (!panel) return Promise.resolve(false);
|
|
770
|
+
machinesPageStartPolling();
|
|
771
|
+
if (!_machinesPageState.loaded) {
|
|
772
|
+
_machinesPageState.loading = true;
|
|
773
|
+
renderMachinesPage();
|
|
774
|
+
}
|
|
775
|
+
var now = machinesPageNow();
|
|
776
|
+
return Promise.all([
|
|
777
|
+
machinesPageJson(machinesPageUrl('/api/machines')),
|
|
778
|
+
machinesPageJson(machinesPageUrl('/api/machine-self')),
|
|
779
|
+
machinesPageJson(machinesPageUrl('/api/host/stats', machinesPageHistoryQuery('', now))),
|
|
780
|
+
]).then(function (results) {
|
|
781
|
+
var list = results[0];
|
|
782
|
+
var self = results[1];
|
|
783
|
+
var stats = results[2];
|
|
784
|
+
_machinesPageState.loading = false;
|
|
785
|
+
if (!list.ok) {
|
|
786
|
+
_machinesPageState.error = machinesPageServerError(list.data, list.status);
|
|
787
|
+
renderMachinesPage();
|
|
788
|
+
return false;
|
|
789
|
+
}
|
|
790
|
+
_machinesPageState.error = '';
|
|
791
|
+
_machinesPageState.machines = (list.data && Array.isArray(list.data.machines)) ? list.data.machines : [];
|
|
792
|
+
_machinesPageState.hubNetworkSuspect = !!(list.data && list.data.hubNetworkSuspect);
|
|
793
|
+
_machinesPageState.selfSettings = (self.ok && self.data && self.data.self) || null;
|
|
794
|
+
_machinesPageState.self = stats.ok ? stats.data : null;
|
|
795
|
+
_machinesPageState.selfInstanceId = String((stats.ok && stats.data && stats.data.instanceId)
|
|
796
|
+
|| (self.ok && self.data && self.data.instanceId) || '');
|
|
797
|
+
// P-7: re-read the dial cache on every load, so a member this device has since reached stops
|
|
798
|
+
// offering the row (and a newly discovered member starts offering it).
|
|
799
|
+
_machinesPageState.origins = machinesPageReadPoolOrigins();
|
|
800
|
+
_machinesPageState.loaded = true;
|
|
801
|
+
renderMachinesPage();
|
|
802
|
+
return machinesPageLoadHistories(now).then(function () {
|
|
803
|
+
renderMachinesPage();
|
|
804
|
+
return true;
|
|
805
|
+
});
|
|
806
|
+
}).catch(function (error) {
|
|
807
|
+
// REPORT: a transport failure is shown on the page, never swallowed into an empty list.
|
|
808
|
+
_machinesPageState.loading = false;
|
|
809
|
+
_machinesPageState.error = 'machines_unavailable: ' + ((error && error.message) || 'fetch failed');
|
|
810
|
+
renderMachinesPage();
|
|
811
|
+
return false;
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// One history read per REACHABLE row. An unreachable box has nothing to read, and asking would cost
|
|
816
|
+
// one timeout per poll for every off-LAN laptop in the pool.
|
|
817
|
+
function machinesPageLoadHistories(now) {
|
|
818
|
+
var reachable = _machinesPageState.machines.filter(function (machine) {
|
|
819
|
+
return machine && machine.instanceId && String(machine.health || '').toLowerCase() === 'reachable';
|
|
820
|
+
});
|
|
821
|
+
return Promise.all(reachable.map(function (machine) {
|
|
822
|
+
var id = String(machine.instanceId);
|
|
823
|
+
return machinesPageJson(machinesPageUrl('/api/host/stats', machinesPageHistoryQuery(id, now)))
|
|
824
|
+
.then(function (result) {
|
|
825
|
+
_machinesPageState.histories[id] = result.ok ? (result.data || null) : null;
|
|
826
|
+
return null;
|
|
827
|
+
})
|
|
828
|
+
.catch(function () {
|
|
829
|
+
// HANDLE: one box's history is decoration; its card still renders from the hub's cached sample.
|
|
830
|
+
_machinesPageState.histories[id] = null;
|
|
831
|
+
return null;
|
|
832
|
+
});
|
|
833
|
+
}));
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// ── visibility + the one poll timer (AC-5f) ─────────────────────────────────────────────────────
|
|
837
|
+
function machinesPageIsVisible() {
|
|
838
|
+
var panel = document.getElementById('machines-panel');
|
|
839
|
+
if (!panel) return false;
|
|
840
|
+
if (panel.style && panel.style.display === 'none') return false;
|
|
841
|
+
if (typeof document.hidden === 'boolean' && document.hidden) return false;
|
|
842
|
+
return true;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
function machinesPageStartPolling() {
|
|
846
|
+
if (_machinesPagePollTimer != null) return false;
|
|
847
|
+
if (!machinesPageIsVisible()) return false;
|
|
848
|
+
_machinesPageBindVisibility();
|
|
849
|
+
var tick = function () {
|
|
850
|
+
_machinesPagePollTimer = null;
|
|
851
|
+
if (!machinesPageIsVisible()) return;
|
|
852
|
+
_machinesPagePollTimer = _machinesPageTimers.set(tick, MACHINES_PAGE_POLL_MS);
|
|
853
|
+
// Refresh in place: no `loading` flag, so the cards never flash back to "Loading machines…".
|
|
854
|
+
loadMachinesPanel();
|
|
855
|
+
};
|
|
856
|
+
_machinesPagePollTimer = _machinesPageTimers.set(tick, MACHINES_PAGE_POLL_MS);
|
|
857
|
+
return true;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
function machinesPageStopPolling() {
|
|
861
|
+
if (_machinesPagePollTimer == null) return false;
|
|
862
|
+
_machinesPageTimers.clear(_machinesPagePollTimer);
|
|
863
|
+
_machinesPagePollTimer = null;
|
|
864
|
+
return true;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
function machinesPageOnVisibilityChange() {
|
|
868
|
+
if (typeof document.hidden === 'boolean' && document.hidden) { machinesPageStopPolling(); return; }
|
|
869
|
+
if (machinesPageIsVisible()) machinesPageStartPolling();
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
function _machinesPageBindVisibility() {
|
|
873
|
+
if (_machinesPageVisibilityBound) return;
|
|
874
|
+
_machinesPageVisibilityBound = true;
|
|
875
|
+
if (typeof document.addEventListener === 'function') {
|
|
876
|
+
document.addEventListener('visibilitychange', machinesPageOnVisibilityChange);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// Called by index.html's panel-hide path (`_commitSurfaceVisibility`) and by anything else that
|
|
881
|
+
// takes the page off screen: the timer stops, the DOM stays so a return is instant.
|
|
882
|
+
function machinesPanelHidden() {
|
|
883
|
+
return machinesPageStopPolling();
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
// navTo('machines') entry point (mirrors showWorktreesPanel).
|
|
887
|
+
function showMachinesPanel(opts) {
|
|
888
|
+
if (typeof activateTab === 'function') activateTab('machines');
|
|
889
|
+
return loadMachinesPanel();
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// ── AC-5d: keep the picker grid and the sidebar chip in step without a reload ───────────────────
|
|
893
|
+
// There is no shared machines cache to patch: session-commands.js owns the picker's own copy
|
|
894
|
+
// (`_nsMachineState.machines`) and the sidebar chip is painted from each SESSION ROW's
|
|
895
|
+
// machine/machineName fields, which the hub serves. So the honest hook is the picker's own loader —
|
|
896
|
+
// it re-fetches /api/machines and rebuilds the grid (and therefore the cache) from the same read the
|
|
897
|
+
// page just made; the chip repaints from the hub's next session frame, which is the only thing that
|
|
898
|
+
// carries a machine's name next to a session.
|
|
899
|
+
function machinesPageRefreshSurfaces() {
|
|
900
|
+
if (typeof loadNewSessionMachines === 'function') loadNewSessionMachines();
|
|
901
|
+
return true;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// ── writes (machine-owned settings, D-4) ────────────────────────────────────────────────────────
|
|
905
|
+
function machinesPageSelfPath(instanceId) {
|
|
906
|
+
var id = String(instanceId || '');
|
|
907
|
+
return machinesPageUrl('/api/machine-self', id ? 'machine=' + encodeURIComponent(id) : '');
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
function machinesPageApplySelfPatch(instanceId, patch) {
|
|
911
|
+
var id = String(instanceId || '');
|
|
912
|
+
var body = patch || {};
|
|
913
|
+
return machinesPageJson(machinesPageSelfPath(id), { method: 'POST', body: body })
|
|
914
|
+
.then(function (result) {
|
|
915
|
+
if (!result.ok) {
|
|
916
|
+
machinesPageToast(machinesPageServerError(result.data, result.status));
|
|
917
|
+
return false;
|
|
918
|
+
}
|
|
919
|
+
machinesPageToast('');
|
|
920
|
+
return loadMachinesPanel().then(function () {
|
|
921
|
+
machinesPageRefreshSurfaces();
|
|
922
|
+
return true;
|
|
923
|
+
});
|
|
924
|
+
})
|
|
925
|
+
.catch(function (error) {
|
|
926
|
+
// REPORT: the server's refusal (or the transport fault) is what the user needs to see.
|
|
927
|
+
machinesPageToast((error && error.message) || 'machine_self_failed');
|
|
928
|
+
return false;
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
function machinesPageRename(instanceId, value, input) {
|
|
933
|
+
var name = String(value == null ? '' : value).trim();
|
|
934
|
+
if (name.length > 80) {
|
|
935
|
+
machinesPageToast('display_name_too_long');
|
|
936
|
+
if (input && input.value !== undefined) input.value = name.slice(0, 80);
|
|
937
|
+
return Promise.resolve(false);
|
|
938
|
+
}
|
|
939
|
+
return machinesPageApplySelfPatch(instanceId, { displayName: name });
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
function machinesPageToggle(instanceId, field) {
|
|
943
|
+
if (field !== 'favorite' && field !== 'hidden') return Promise.resolve(false);
|
|
944
|
+
var id = String(instanceId || '');
|
|
945
|
+
var current = id
|
|
946
|
+
? _machinesPageState.machines.find(function (machine) { return String(machine && machine.instanceId) === id; })
|
|
947
|
+
: _machinesPageState.selfSettings;
|
|
948
|
+
var next = !(current && (current[field] === 1 || current[field] === true));
|
|
949
|
+
var patch = {};
|
|
950
|
+
patch[field] = next ? 1 : 0;
|
|
951
|
+
return machinesPageApplySelfPatch(id, patch);
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
function machinesPageConfirmRemove(instanceId) {
|
|
955
|
+
var id = String(instanceId || '');
|
|
956
|
+
if (!id) return Promise.resolve(false);
|
|
957
|
+
var machine = _machinesPageState.machines.find(function (row) { return String(row && row.instanceId) === id; });
|
|
958
|
+
var name = (machine && (machine.displayName || machine.url)) || id;
|
|
959
|
+
if (typeof confirm === 'function' && !confirm('Remove ' + name + ' from this machine?')) return Promise.resolve(false);
|
|
960
|
+
return machinesPageJson(machinesPageUrl('/api/machines/' + encodeURIComponent(id) + '/remove'), { method: 'POST' })
|
|
961
|
+
.then(function (result) {
|
|
962
|
+
if (!result.ok) {
|
|
963
|
+
machinesPageToast(machinesPageServerError(result.data, result.status));
|
|
964
|
+
return false;
|
|
965
|
+
}
|
|
966
|
+
var data = result.data || {};
|
|
967
|
+
var rotated = Array.isArray(data.rotated) ? data.rotated : [];
|
|
968
|
+
var unreachable = Array.isArray(data.unreachable) ? data.unreachable : [];
|
|
969
|
+
var note = 'Removed ' + name + '.';
|
|
970
|
+
if (rotated.length) note += ' Re-keyed: ' + rotated.join(', ') + '.';
|
|
971
|
+
if (unreachable.length) note += ' Unreachable (re-pair after the grace): ' + unreachable.join(', ') + '.';
|
|
972
|
+
machinesPageToast(note);
|
|
973
|
+
return loadMachinesPanel();
|
|
974
|
+
})
|
|
975
|
+
.catch(function (error) {
|
|
976
|
+
// REPORT: a failed removal must not look like a successful one.
|
|
977
|
+
machinesPageToast((error && error.message) || 'machine_remove_failed');
|
|
978
|
+
return false;
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
function machinesPageLeavePool() {
|
|
983
|
+
if (typeof confirm === 'function' && !confirm('Leave the machine pool? This Mac drops the shared secret and its machine list.')) {
|
|
984
|
+
return Promise.resolve(false);
|
|
985
|
+
}
|
|
986
|
+
return machinesPageJson(machinesPageUrl('/api/pool/leave'), { method: 'POST' })
|
|
987
|
+
.then(function (result) {
|
|
988
|
+
if (!result.ok) {
|
|
989
|
+
machinesPageToast(machinesPageServerError(result.data, result.status));
|
|
990
|
+
return false;
|
|
991
|
+
}
|
|
992
|
+
machinesPageToast('Left the pool (' + (Number((result.data || {}).removed) || 0) + ' rows dropped).');
|
|
993
|
+
machinesPageRefreshSurfaces();
|
|
994
|
+
return loadMachinesPanel();
|
|
995
|
+
})
|
|
996
|
+
.catch(function (error) {
|
|
997
|
+
// REPORT: an unreachable hub must not read as "you left the pool".
|
|
998
|
+
machinesPageToast((error && error.message) || 'pool_leave_failed');
|
|
999
|
+
return false;
|
|
1000
|
+
});
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
// "New session here" opens the EXISTING new-session modal (no second create path) with this machine
|
|
1004
|
+
// selected. showNewSessionModal() re-reads /api/machines and clears the selection when that read
|
|
1005
|
+
// lands, so the choice is re-applied once the card exists — bounded, through the same injected timer
|
|
1006
|
+
// the poll uses.
|
|
1007
|
+
function machinesPageNewSessionHere(instanceId) {
|
|
1008
|
+
if (typeof showNewSessionModal !== 'function') return false;
|
|
1009
|
+
var id = String(instanceId || '');
|
|
1010
|
+
showNewSessionModal();
|
|
1011
|
+
if (!id || typeof _selectMachineCard !== 'function') return true;
|
|
1012
|
+
var tries = 0;
|
|
1013
|
+
var select = function () {
|
|
1014
|
+
var grid = document.getElementById('ns-machine-grid');
|
|
1015
|
+
var card = grid && typeof grid.querySelector === 'function'
|
|
1016
|
+
? grid.querySelector('.ns-machine-card[data-value="' + id + '"]')
|
|
1017
|
+
: null;
|
|
1018
|
+
if (card) { _selectMachineCard(id); return; }
|
|
1019
|
+
tries += 1;
|
|
1020
|
+
if (tries < 40) _machinesPageTimers.set(select, 100);
|
|
1021
|
+
};
|
|
1022
|
+
select();
|
|
1023
|
+
return true;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
// ── D-9 pairing (AC-5c) ─────────────────────────────────────────────────────────────────────────
|
|
1027
|
+
// WP-6: the container decoder and the Add-dialog preview are the ONE shared copy in
|
|
1028
|
+
// public/js/machine-pool-shared.js (machinesNormalizeCode / machinesFormatCode /
|
|
1029
|
+
// machinesBase64UrlDecode / decodePairingStringClient / machinesAddPreview), because the phone's Add
|
|
1030
|
+
// sheet redeems exactly the same strings. The desktop name is kept as the thin delegate the WP-5
|
|
1031
|
+
// tests read, so there is no second implementation to drift.
|
|
1032
|
+
function machinesPageAddDialogPreview(pairingText, codeText, urlText) {
|
|
1033
|
+
return machinesAddPreview(pairingText, codeText, urlText);
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
var MACHINES_PAGE_ADD_DIALOG_HTML = ''
|
|
1037
|
+
+ '<div class="machine-modal" role="dialog" aria-modal="true" aria-labelledby="machine-add-title">'
|
|
1038
|
+
+ '<h3 id="machine-add-title">Add a machine</h3>'
|
|
1039
|
+
+ '<p class="machine-modal-sub">Paste a pairing string from the other machine, or type its code and address.</p>'
|
|
1040
|
+
+ '<label class="machine-modal-label" for="machine-pair-input">Pairing string</label>'
|
|
1041
|
+
+ '<textarea id="machine-pair-input" class="machine-modal-input" rows="3" spellcheck="false"'
|
|
1042
|
+
+ ' placeholder="ctmpair1:…" aria-describedby="machine-pair-preview"></textarea>'
|
|
1043
|
+
+ '<div id="machine-pair-preview" class="machine-pair-preview" role="status" aria-live="polite"></div>'
|
|
1044
|
+
+ '<div class="machine-modal-or">or</div>'
|
|
1045
|
+
+ '<div class="machine-modal-row">'
|
|
1046
|
+
+ '<input id="machine-pair-code" class="machine-modal-input" type="text" autocomplete="off" spellcheck="false"'
|
|
1047
|
+
+ ' placeholder="XXXX-XXXX" aria-label="Pairing code" />'
|
|
1048
|
+
+ '<input id="machine-pair-url" class="machine-modal-input" type="text" autocomplete="off" spellcheck="false"'
|
|
1049
|
+
+ ' placeholder="http://spark.localhost:3556" aria-label="Machine address" />'
|
|
1050
|
+
+ '</div>'
|
|
1051
|
+
+ '<div id="machine-add-error" class="machine-modal-error" role="status" aria-live="polite"></div>'
|
|
1052
|
+
+ '<div class="machine-modal-actions">'
|
|
1053
|
+
+ '<button type="button" class="mpc-btn" id="machine-add-cancel">Cancel</button>'
|
|
1054
|
+
+ '<button type="button" class="mpc-btn mpc-primary" id="machine-add-confirm" disabled>Add machine</button>'
|
|
1055
|
+
+ '</div>'
|
|
1056
|
+
+ '<details class="machine-modal-legacy">'
|
|
1057
|
+
+ '<summary>Register by URL (legacy)</summary>'
|
|
1058
|
+
+ '<p class="machine-modal-sub">An env-token box (P-8) that has not joined the pool. Its identity is pinned '
|
|
1059
|
+
+ 'by the manifest it answers.</p>'
|
|
1060
|
+
+ '<div class="machine-modal-row">'
|
|
1061
|
+
+ '<input id="machine-legacy-url" class="machine-modal-input" type="text" autocomplete="off" spellcheck="false"'
|
|
1062
|
+
+ ' placeholder="http://192.168.1.20:3456" aria-label="Legacy machine URL" />'
|
|
1063
|
+
+ '<input id="machine-legacy-name" class="machine-modal-input" type="text" autocomplete="off" spellcheck="false"'
|
|
1064
|
+
+ ' placeholder="Display name" aria-label="Legacy machine display name" />'
|
|
1065
|
+
+ '</div>'
|
|
1066
|
+
+ '<div class="machine-modal-actions">'
|
|
1067
|
+
+ '<button type="button" class="mpc-btn" id="machine-legacy-submit">Register by URL</button>'
|
|
1068
|
+
+ '</div>'
|
|
1069
|
+
+ '</details>'
|
|
1070
|
+
+ '</div>';
|
|
1071
|
+
|
|
1072
|
+
function closeMachinesPageDialog() {
|
|
1073
|
+
if (_machinesPageDialog && _machinesPageDialog.overlay && _machinesPageDialog.overlay.parentNode) {
|
|
1074
|
+
_machinesPageDialog.overlay.parentNode.removeChild(_machinesPageDialog.overlay);
|
|
1075
|
+
}
|
|
1076
|
+
_machinesPageDialog = null;
|
|
1077
|
+
return true;
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
function _machinesPageMountDialog(html) {
|
|
1081
|
+
var overlay = document.createElement('div');
|
|
1082
|
+
overlay.className = 'machine-modal-overlay';
|
|
1083
|
+
overlay.innerHTML = html;
|
|
1084
|
+
var host = document.body || document.documentElement;
|
|
1085
|
+
if (host && typeof host.appendChild === 'function') host.appendChild(overlay);
|
|
1086
|
+
return overlay;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
// The dialog's live state: reads the three fields, paints the preview, and enables Confirm ONLY for
|
|
1090
|
+
// a form the server can redeem. Also exposed as a pure function (machinesPageAddDialogPreview) so
|
|
1091
|
+
// AC-5c is provable without a DOM.
|
|
1092
|
+
function machinesPageAddDialogUpdate() {
|
|
1093
|
+
if (!_machinesPageDialog) return null;
|
|
1094
|
+
var dialog = _machinesPageDialog;
|
|
1095
|
+
var preview = machinesPageAddDialogPreview(
|
|
1096
|
+
dialog.pairInput ? dialog.pairInput.value : '',
|
|
1097
|
+
dialog.codeInput ? dialog.codeInput.value : '',
|
|
1098
|
+
dialog.urlInput ? dialog.urlInput.value : '',
|
|
1099
|
+
);
|
|
1100
|
+
if (dialog.preview) {
|
|
1101
|
+
var text = '';
|
|
1102
|
+
if (preview.message === 'pairing_string_invalid') text = 'pairing_string_invalid — that is not a ctmpair1: string.';
|
|
1103
|
+
else if (preview.message === 'pairing_code_invalid') text = 'pairing_code_invalid — a code is 8 letters (XXXX-XXXX).';
|
|
1104
|
+
else if (preview.message === 'address_required') text = 'Enter the machine address (http://host:port).';
|
|
1105
|
+
else if (preview.ok && preview.name) text = 'Box: ' + preview.name + (preview.url ? ' — ' + preview.url : '');
|
|
1106
|
+
else if (preview.ok) text = 'Box: ' + preview.url;
|
|
1107
|
+
else if (preview.addressOnly) text = preview.url + ' — the name appears after pairing';
|
|
1108
|
+
dialog.preview.textContent = text;
|
|
1109
|
+
dialog.preview.classList.toggle('bad', !!preview.message);
|
|
1110
|
+
dialog.preview.classList.toggle('good', !!preview.ok);
|
|
1111
|
+
}
|
|
1112
|
+
if (dialog.confirm) dialog.confirm.disabled = !preview.ok;
|
|
1113
|
+
return preview;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
function openAddMachineDialog(opts) {
|
|
1117
|
+
var options = opts || {};
|
|
1118
|
+
closeMachinesPageDialog();
|
|
1119
|
+
var overlay = _machinesPageMountDialog(MACHINES_PAGE_ADD_DIALOG_HTML);
|
|
1120
|
+
var dialog = {
|
|
1121
|
+
overlay: overlay,
|
|
1122
|
+
pairInput: document.getElementById('machine-pair-input'),
|
|
1123
|
+
codeInput: document.getElementById('machine-pair-code'),
|
|
1124
|
+
urlInput: document.getElementById('machine-pair-url'),
|
|
1125
|
+
preview: document.getElementById('machine-pair-preview'),
|
|
1126
|
+
error: document.getElementById('machine-add-error'),
|
|
1127
|
+
confirm: document.getElementById('machine-add-confirm'),
|
|
1128
|
+
cancel: document.getElementById('machine-add-cancel'),
|
|
1129
|
+
};
|
|
1130
|
+
_machinesPageDialog = dialog;
|
|
1131
|
+
// P-8: the "Join pool" action on an env-token row opens this dialog with its address already in the
|
|
1132
|
+
// manual form — the member still needs the code from the box.
|
|
1133
|
+
if (options.url && dialog.urlInput) dialog.urlInput.value = String(options.url);
|
|
1134
|
+
if (typeof dialog.pairInput.addEventListener === 'function') dialog.pairInput.addEventListener('input', machinesPageAddDialogUpdate);
|
|
1135
|
+
if (typeof dialog.codeInput.addEventListener === 'function') dialog.codeInput.addEventListener('input', machinesPageAddDialogUpdate);
|
|
1136
|
+
if (typeof dialog.urlInput.addEventListener === 'function') dialog.urlInput.addEventListener('input', machinesPageAddDialogUpdate);
|
|
1137
|
+
if (dialog.cancel && typeof dialog.cancel.addEventListener === 'function') dialog.cancel.addEventListener('click', closeMachinesPageDialog);
|
|
1138
|
+
if (dialog.confirm && typeof dialog.confirm.addEventListener === 'function') dialog.confirm.addEventListener('click', machinesPageAddDialogSubmit);
|
|
1139
|
+
var legacy = document.getElementById('machine-legacy-submit');
|
|
1140
|
+
if (legacy && typeof legacy.addEventListener === 'function') legacy.addEventListener('click', machinesPageRegisterLegacy);
|
|
1141
|
+
machinesPageAddDialogUpdate();
|
|
1142
|
+
return dialog;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
function machinesPageAddDialogSubmit() {
|
|
1146
|
+
if (!_machinesPageDialog) return Promise.resolve(false);
|
|
1147
|
+
var dialog = _machinesPageDialog;
|
|
1148
|
+
var preview = machinesPageAddDialogPreview(
|
|
1149
|
+
dialog.pairInput ? dialog.pairInput.value : '',
|
|
1150
|
+
dialog.codeInput ? dialog.codeInput.value : '',
|
|
1151
|
+
dialog.urlInput ? dialog.urlInput.value : '',
|
|
1152
|
+
);
|
|
1153
|
+
if (!preview.ok) return Promise.resolve(false);
|
|
1154
|
+
var body = preview.mode === 'string'
|
|
1155
|
+
? { pairingString: preview.pairingString }
|
|
1156
|
+
: { code: machinesFormatCode(preview.code), url: preview.url };
|
|
1157
|
+
if (dialog.error) dialog.error.textContent = '';
|
|
1158
|
+
return machinesPageJson(machinesPageUrl('/api/machines/pair'), { method: 'POST', body: body })
|
|
1159
|
+
.then(function (result) {
|
|
1160
|
+
if (!result.ok) {
|
|
1161
|
+
// §6.3: the server's code is relayed VERBATIM (pairing_string_invalid, machine_unreachable,
|
|
1162
|
+
// machine_identity_mismatch, pairing_code_invalid, pool_member_exists, rate_limited).
|
|
1163
|
+
if (dialog.error) dialog.error.textContent = machinesPageServerError(result.data, result.status);
|
|
1164
|
+
return false;
|
|
1165
|
+
}
|
|
1166
|
+
closeMachinesPageDialog();
|
|
1167
|
+
machinesPageToast('Machine added.');
|
|
1168
|
+
machinesPageRefreshSurfaces();
|
|
1169
|
+
return loadMachinesPanel();
|
|
1170
|
+
})
|
|
1171
|
+
.catch(function (error) {
|
|
1172
|
+
// REPORT: a transport failure is shown in the dialog, which stays open for a retry.
|
|
1173
|
+
if (dialog.error) dialog.error.textContent = (error && error.message) || 'machine_pair_failed';
|
|
1174
|
+
return false;
|
|
1175
|
+
});
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
// P-8 legacy path: POST /api/machines { url, displayName } — the pre-pool registration the Settings
|
|
1179
|
+
// card used to offer, kept reachable from the Add dialog under "Register by URL (legacy)".
|
|
1180
|
+
function machinesPageRegisterLegacy() {
|
|
1181
|
+
var urlInput = document.getElementById('machine-legacy-url');
|
|
1182
|
+
var nameInput = document.getElementById('machine-legacy-name');
|
|
1183
|
+
var url = String((urlInput && urlInput.value) || '').trim();
|
|
1184
|
+
var displayName = String((nameInput && nameInput.value) || '').trim();
|
|
1185
|
+
var errorNode = _machinesPageDialog ? _machinesPageDialog.error : document.getElementById('machine-add-error');
|
|
1186
|
+
if (!url) {
|
|
1187
|
+
if (errorNode) errorNode.textContent = 'Enter the machine URL.';
|
|
1188
|
+
return Promise.resolve(false);
|
|
1189
|
+
}
|
|
1190
|
+
return machinesPageJson(machinesPageUrl('/api/machines'), { method: 'POST', body: { url: url, displayName: displayName } })
|
|
1191
|
+
.then(function (result) {
|
|
1192
|
+
if (!result.ok) {
|
|
1193
|
+
if (errorNode) errorNode.textContent = machinesPageServerError(result.data, result.status);
|
|
1194
|
+
return false;
|
|
1195
|
+
}
|
|
1196
|
+
closeMachinesPageDialog();
|
|
1197
|
+
machinesPageToast('Machine registered.');
|
|
1198
|
+
machinesPageRefreshSurfaces();
|
|
1199
|
+
return loadMachinesPanel();
|
|
1200
|
+
})
|
|
1201
|
+
.catch(function (error) {
|
|
1202
|
+
// REPORT: the register failure is shown verbatim in the dialog.
|
|
1203
|
+
if (errorNode) errorNode.textContent = (error && error.message) || 'machine_register_failed';
|
|
1204
|
+
return false;
|
|
1205
|
+
});
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
// "Show a code for this machine": mint an offer HERE (D-5/D-6 unchanged — the box still mints the
|
|
1209
|
+
// code) and paint it with the pairing string the other machine pastes.
|
|
1210
|
+
function openShowCodeDialog() {
|
|
1211
|
+
closeMachinesPageDialog();
|
|
1212
|
+
var overlay = _machinesPageMountDialog('<div class="machine-modal" role="dialog" aria-modal="true" aria-labelledby="machine-code-title">'
|
|
1213
|
+
+ '<h3 id="machine-code-title">Show a code for this machine</h3>'
|
|
1214
|
+
+ '<p class="machine-modal-sub">On the other machine, choose “Add machine” and paste this string — or type the code.</p>'
|
|
1215
|
+
+ '<div id="machine-code-value" class="machine-code-value" role="status" aria-live="polite">…</div>'
|
|
1216
|
+
+ '<textarea id="machine-pair-string" class="machine-modal-input" rows="4" readonly spellcheck="false" aria-label="Pairing string"></textarea>'
|
|
1217
|
+
+ '<div id="machine-code-error" class="machine-modal-error" role="status" aria-live="polite"></div>'
|
|
1218
|
+
+ '<div class="machine-modal-actions"><button type="button" class="mpc-btn" id="machine-code-close">Close</button></div>'
|
|
1219
|
+
+ '</div>');
|
|
1220
|
+
_machinesPageDialog = {
|
|
1221
|
+
overlay: overlay,
|
|
1222
|
+
codeValue: document.getElementById('machine-code-value'),
|
|
1223
|
+
pairString: document.getElementById('machine-pair-string'),
|
|
1224
|
+
error: document.getElementById('machine-code-error'),
|
|
1225
|
+
close: document.getElementById('machine-code-close'),
|
|
1226
|
+
};
|
|
1227
|
+
var dialog = _machinesPageDialog;
|
|
1228
|
+
if (dialog.close && typeof dialog.close.addEventListener === 'function') dialog.close.addEventListener('click', closeMachinesPageDialog);
|
|
1229
|
+
return machinesPageJson(machinesPageUrl('/api/machine-pairing/offers'), { method: 'POST', body: {} })
|
|
1230
|
+
.then(function (result) {
|
|
1231
|
+
if (!result.ok) {
|
|
1232
|
+
var code = machinesPageServerError(result.data, result.status);
|
|
1233
|
+
if (dialog.error) dialog.error.textContent = code;
|
|
1234
|
+
return false;
|
|
1235
|
+
}
|
|
1236
|
+
var data = result.data || {};
|
|
1237
|
+
if (dialog.codeValue) dialog.codeValue.textContent = String(data.code || '');
|
|
1238
|
+
if (dialog.pairString) dialog.pairString.value = String(data.pairingString || '');
|
|
1239
|
+
return true;
|
|
1240
|
+
})
|
|
1241
|
+
.catch(function (error) {
|
|
1242
|
+
// REPORT: the offer failure stays in the dialog so the user can retry.
|
|
1243
|
+
if (dialog.error) dialog.error.textContent = (error && error.message) || 'pairing_offer_failed';
|
|
1244
|
+
return false;
|
|
1245
|
+
});
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
// P-8: the "Join pool" action on an env-token row. It is the same D-9 flow — a code from that box —
|
|
1249
|
+
// so the dialog opens with the row's address already filled in.
|
|
1250
|
+
function machinesPageJoinPool(instanceId, url) {
|
|
1251
|
+
return openAddMachineDialog({ url: url || '' });
|
|
1252
|
+
}
|