ctrlrelay 0.16.0__tar.gz → 0.16.2__tar.gz
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.
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/CHANGELOG.md +47 -1
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/PKG-INFO +5 -5
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/README.md +4 -4
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/SECURITY.md +1 -1
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/docs/cli.md +2 -2
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/docs/configuration.md +2 -2
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/docs/development.md +2 -2
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/docs/getting-started.md +4 -4
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/docs/index.md +2 -2
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/docs/operations.md +6 -2
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/pyproject.toml +1 -1
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/src/ctrlrelay/cli.py +4 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/src/ctrlrelay/templates/systemd/bridge.service.template +1 -1
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/src/ctrlrelay/web/server.py +18 -1
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/src/ctrlrelay/web/static/index.html +139 -12
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/src/ctrlrelay/web/status.py +113 -1
- ctrlrelay-0.16.2/tests/test_bridge_start_command.py +63 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/tests/test_install.py +11 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/tests/test_web_server.py +227 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/.github/dependabot.yml +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/.github/workflows/build.yml +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/.github/workflows/cla.yml +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/.github/workflows/pages.yml +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/.github/workflows/publish.yml +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/.github/workflows/test.yml +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/.gitignore +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/CODE_OF_CONDUCT.md +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/CONTRIBUTING.md +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/LICENSE +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/config/orchestrator.yaml.example +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/docs/Gemfile +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/docs/_config.yml +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/docs/adding-a-machine.md +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/docs/architecture.md +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/docs/bridge.md +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/docs/feedback-loop.md +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/docs/personalization.md +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/docs/superpowers/plans/2026-10-30-status-page.md +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/docs/superpowers/specs/2026-10-30-status-page-design.md +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/src/ctrlrelay/__init__.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/src/ctrlrelay/bridge/__init__.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/src/ctrlrelay/bridge/__main__.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/src/ctrlrelay/bridge/factory.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/src/ctrlrelay/bridge/handler.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/src/ctrlrelay/bridge/mattermost_handler.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/src/ctrlrelay/bridge/node_tag.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/src/ctrlrelay/bridge/protocol.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/src/ctrlrelay/bridge/server.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/src/ctrlrelay/bridge/telegram_handler.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/src/ctrlrelay/core/__init__.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/src/ctrlrelay/core/agent_env.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/src/ctrlrelay/core/archived.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/src/ctrlrelay/core/audit.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/src/ctrlrelay/core/binaries.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/src/ctrlrelay/core/checkpoint.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/src/ctrlrelay/core/claims.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/src/ctrlrelay/core/code_review.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/src/ctrlrelay/core/config.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/src/ctrlrelay/core/dispatch_health.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/src/ctrlrelay/core/dispatcher.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/src/ctrlrelay/core/events.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/src/ctrlrelay/core/github.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/src/ctrlrelay/core/github_budget.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/src/ctrlrelay/core/heartbeat.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/src/ctrlrelay/core/job_claims.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/src/ctrlrelay/core/network.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/src/ctrlrelay/core/node_check.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/src/ctrlrelay/core/obs.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/src/ctrlrelay/core/poller.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/src/ctrlrelay/core/pr_verifier.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/src/ctrlrelay/core/pr_watcher.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/src/ctrlrelay/core/publish.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/src/ctrlrelay/core/question_expiry.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/src/ctrlrelay/core/redact.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/src/ctrlrelay/core/scheduler.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/src/ctrlrelay/core/state.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/src/ctrlrelay/core/usage.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/src/ctrlrelay/core/worktree.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/src/ctrlrelay/dashboard/__init__.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/src/ctrlrelay/dashboard/client.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/src/ctrlrelay/gh_protocol.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/src/ctrlrelay/install.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/src/ctrlrelay/personalization/__init__.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/src/ctrlrelay/personalization/manager.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/src/ctrlrelay/personalization/paths.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/src/ctrlrelay/pipelines/__init__.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/src/ctrlrelay/pipelines/base.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/src/ctrlrelay/pipelines/dev.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/src/ctrlrelay/pipelines/post_merge.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/src/ctrlrelay/pipelines/secops.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/src/ctrlrelay/pipelines/task.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/src/ctrlrelay/setup.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/src/ctrlrelay/templates/__init__.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/src/ctrlrelay/templates/global-CLAUDE.md.snippet +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/src/ctrlrelay/templates/launchd/__init__.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/src/ctrlrelay/templates/launchd/bridge.plist.template +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/src/ctrlrelay/templates/launchd/poller.plist.template +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/src/ctrlrelay/templates/systemd/__init__.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/src/ctrlrelay/templates/systemd/poller.service.template +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/src/ctrlrelay/templates/systemd/web.service.template +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/src/ctrlrelay/transports/__init__.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/src/ctrlrelay/transports/base.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/src/ctrlrelay/transports/file_mock.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/src/ctrlrelay/transports/socket_client.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/src/ctrlrelay/web/__init__.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/src/ctrlrelay/web/fleet.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/src/ctrlrelay/web/health.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/src/ctrlrelay/web/links.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/src/ctrlrelay/web/transcript.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/src/ctrlrelay/web/waiting.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/tests/__init__.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/tests/_publish_fakes.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/tests/conftest.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/tests/fixtures/gh_search_issues.json +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/tests/fixtures/stream_json_rate_limit.jsonl +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/tests/fixtures/transcript_sample.jsonl +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/tests/test_agent_no_keys.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/tests/test_agent_sandbox.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/tests/test_archived.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/tests/test_audit.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/tests/test_binary_resolution.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/tests/test_bridge_protocol.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/tests/test_bridge_server.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/tests/test_chat_machines.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/tests/test_checkpoint.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/tests/test_claim_reconciliation.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/tests/test_cli_ci_wait.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/tests/test_cli_dev.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/tests/test_cli_repos.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/tests/test_cli_secops.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/tests/test_cli_start.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/tests/test_cli_version.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/tests/test_code_review.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/tests/test_config.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/tests/test_dashboard_client.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/tests/test_dev_integration.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/tests/test_dev_pipeline.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/tests/test_dispatch_health.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/tests/test_dispatch_outcome_hook.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/tests/test_dispatcher.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/tests/test_docs_site.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/tests/test_event_emission.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/tests/test_events.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/tests/test_fleet.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/tests/test_gh_protocol.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/tests/test_github.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/tests/test_github_budget.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/tests/test_issue_outcome_announcement.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/tests/test_job_claims.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/tests/test_mattermost_handler.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/tests/test_network.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/tests/test_node_check.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/tests/test_node_claims.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/tests/test_notify_transport.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/tests/test_obs.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/tests/test_personalization.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/tests/test_pipeline_base.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/tests/test_poller.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/tests/test_post_merge.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/tests/test_pr_verifier.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/tests/test_pr_watcher.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/tests/test_pre_dispatch_retry.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/tests/test_question_backoff.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/tests/test_question_expiry.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/tests/test_redact.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/tests/test_relay_alert_off_lock.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/tests/test_resume_prompt_contract.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/tests/test_scheduler.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/tests/test_secops_integration.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/tests/test_secops_pipeline.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/tests/test_setup.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/tests/test_state.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/tests/test_task_pipeline.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/tests/test_telegram_handler.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/tests/test_transport.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/tests/test_transport_resolution.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/tests/test_unreviewed_pr_announcement.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/tests/test_usage.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/tests/test_web_health.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/tests/test_web_links.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/tests/test_web_status.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/tests/test_web_transcript.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/tests/test_web_waiting.py +0 -0
- {ctrlrelay-0.16.0 → ctrlrelay-0.16.2}/tests/test_worktree.py +0 -0
|
@@ -7,6 +7,50 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.16.2] - 2026-11-01
|
|
11
|
+
|
|
12
|
+
Status page changes only. **No config edit is required.**
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- The Today counters (dispatched, PRs opened, merged, failed, blocked,
|
|
17
|
+
messages sent) are buttons: each opens today's events of that kind under
|
|
18
|
+
the counters. A failed run shows why (exit code, the run's summary and its
|
|
19
|
+
stored output), a blocked one the question, PRs link to the PR. New
|
|
20
|
+
`/api/events?kind=<kind>&day=today` (#271, closes #257)
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- A GitHub API bucket whose reset time has passed since the last read shows
|
|
25
|
+
as "refilled at <time>, not read since" instead of "unknown"; search,
|
|
26
|
+
which refills every minute, usually shows that way (#272)
|
|
27
|
+
- More space under the header and between the Machines strip and "Who has
|
|
28
|
+
what" (#271)
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
- The live feed now scrubs credential shapes from event details: a
|
|
33
|
+
question an agent asked could show a token on the page (#271)
|
|
34
|
+
|
|
35
|
+
## [0.16.1] - 2026-11-01
|
|
36
|
+
|
|
37
|
+
Fixes the chat bridge, which did not start on 0.16.0. **No config edit is
|
|
38
|
+
required.** If you upgraded to 0.16.0, upgrade to this.
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
|
|
42
|
+
- `ctrlrelay bridge start` (what the bridge service runs) exited at startup
|
|
43
|
+
on 0.16.0 with `BridgeServer.__init__() missing 1 required keyword-only
|
|
44
|
+
argument: 'node'`, for Telegram and Mattermost alike, so chat questions
|
|
45
|
+
and answers stopped. It now passes the machine's name and the shared
|
|
46
|
+
setting, like `python -m ctrlrelay.bridge` (#265)
|
|
47
|
+
|
|
48
|
+
### Changed
|
|
49
|
+
|
|
50
|
+
- The bridge's systemd unit is described as "ctrlrelay chat bridge
|
|
51
|
+
(Telegram or Mattermost)", and the docs call it the chat bridge. An
|
|
52
|
+
installed unit keeps its old description until it is re-rendered (#266)
|
|
53
|
+
|
|
10
54
|
## [0.16.0] - 2026-11-01
|
|
11
55
|
|
|
12
56
|
Several machines can now run ctrlrelay on one GitHub account, each claiming
|
|
@@ -1957,7 +2001,9 @@ pipeline).
|
|
|
1957
2001
|
per-phase implementation plans (Phase 0 through Phase 4).
|
|
1958
2002
|
- `docs/Claude_Code_Project_Guide.md` — project development guide.
|
|
1959
2003
|
|
|
1960
|
-
[Unreleased]: https://github.com/AInvirion/ctrlrelay/compare/v0.16.
|
|
2004
|
+
[Unreleased]: https://github.com/AInvirion/ctrlrelay/compare/v0.16.2...HEAD
|
|
2005
|
+
[0.16.2]: https://github.com/AInvirion/ctrlrelay/compare/v0.16.1...v0.16.2
|
|
2006
|
+
[0.16.1]: https://github.com/AInvirion/ctrlrelay/compare/v0.16.0...v0.16.1
|
|
1961
2007
|
[0.16.0]: https://github.com/AInvirion/ctrlrelay/compare/v0.15.0...v0.16.0
|
|
1962
2008
|
[0.15.0]: https://github.com/AInvirion/ctrlrelay/compare/v0.14.0...v0.15.0
|
|
1963
2009
|
[0.14.0]: https://github.com/AInvirion/ctrlrelay/compare/v0.13.0...v0.14.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: ctrlrelay
|
|
3
|
-
Version: 0.16.
|
|
3
|
+
Version: 0.16.2
|
|
4
4
|
Summary: Local-first orchestrator for headless coding agents across multiple GitHub repos
|
|
5
5
|
Project-URL: Homepage, https://github.com/AInvirion/ctrlrelay
|
|
6
6
|
Project-URL: Documentation, https://ainvirion.github.io/ctrlrelay/
|
|
@@ -128,11 +128,11 @@ are on the roadmap (see [Roadmap](#roadmap)).
|
|
|
128
128
|
new issue │ │ blocked session
|
|
129
129
|
│ │
|
|
130
130
|
┌──────────▼──┐ ┌─▼───────────────────┐
|
|
131
|
-
│ dev pipeline│ │
|
|
132
|
-
│ in worktree │ │ (socket ↔
|
|
131
|
+
│ dev pipeline│ │ chat bridge │
|
|
132
|
+
│ in worktree │ │ (socket ↔ chat API) │
|
|
133
133
|
│ agent CLI │ └──────────┬──────────┘
|
|
134
134
|
└──────┬──────┘ │
|
|
135
|
-
│ PR opened │
|
|
135
|
+
│ PR opened │ asks you
|
|
136
136
|
▼ ▼
|
|
137
137
|
┌────────────┐ ┌─────────┐
|
|
138
138
|
│ PR watcher │ │ You │
|
|
@@ -207,7 +207,7 @@ Run as a supervised daemon (launchd on macOS / systemd on Linux) — see
|
|
|
207
207
|
|
|
208
208
|
- [Getting started][docs-start]
|
|
209
209
|
- [Configuration][docs-config]
|
|
210
|
-
- [Telegram
|
|
210
|
+
- [Chat bridge (Telegram or Mattermost)][docs-bridge]
|
|
211
211
|
- [Feedback loop][docs-feedback]
|
|
212
212
|
- [CLI reference][docs-cli]
|
|
213
213
|
- [Operations (launchd / systemd / scheduled jobs)][ops-docs]
|
|
@@ -87,11 +87,11 @@ are on the roadmap (see [Roadmap](#roadmap)).
|
|
|
87
87
|
new issue │ │ blocked session
|
|
88
88
|
│ │
|
|
89
89
|
┌──────────▼──┐ ┌─▼───────────────────┐
|
|
90
|
-
│ dev pipeline│ │
|
|
91
|
-
│ in worktree │ │ (socket ↔
|
|
90
|
+
│ dev pipeline│ │ chat bridge │
|
|
91
|
+
│ in worktree │ │ (socket ↔ chat API) │
|
|
92
92
|
│ agent CLI │ └──────────┬──────────┘
|
|
93
93
|
└──────┬──────┘ │
|
|
94
|
-
│ PR opened │
|
|
94
|
+
│ PR opened │ asks you
|
|
95
95
|
▼ ▼
|
|
96
96
|
┌────────────┐ ┌─────────┐
|
|
97
97
|
│ PR watcher │ │ You │
|
|
@@ -166,7 +166,7 @@ Run as a supervised daemon (launchd on macOS / systemd on Linux) — see
|
|
|
166
166
|
|
|
167
167
|
- [Getting started][docs-start]
|
|
168
168
|
- [Configuration][docs-config]
|
|
169
|
-
- [Telegram
|
|
169
|
+
- [Chat bridge (Telegram or Mattermost)][docs-bridge]
|
|
170
170
|
- [Feedback loop][docs-feedback]
|
|
171
171
|
- [CLI reference][docs-cli]
|
|
172
172
|
- [Operations (launchd / systemd / scheduled jobs)][ops-docs]
|
|
@@ -25,7 +25,7 @@ mitigation within **14 days** for high-severity issues.
|
|
|
25
25
|
In-scope:
|
|
26
26
|
|
|
27
27
|
- The `ctrlrelay` CLI and its pipelines (`dev`, `secops`, scheduled jobs).
|
|
28
|
-
- The
|
|
28
|
+
- The chat bridge (Telegram or Mattermost) and socket transport.
|
|
29
29
|
- Anything in `src/` and `config/`.
|
|
30
30
|
|
|
31
31
|
Out-of-scope:
|
|
@@ -27,7 +27,7 @@ ctrlrelay [--version] <command> ...
|
|
|
27
27
|
| `ctrlrelay status` | Show active locks and the 5 most-recent sessions. |
|
|
28
28
|
| `ctrlrelay config ...` | Configuration helpers. |
|
|
29
29
|
| `ctrlrelay skills ...` | Skill discovery / audit. |
|
|
30
|
-
| `ctrlrelay bridge ...` | Manage the
|
|
30
|
+
| `ctrlrelay bridge ...` | Manage the chat bridge daemon (Telegram or Mattermost). |
|
|
31
31
|
| `ctrlrelay run ...` | Run a pipeline interactively. |
|
|
32
32
|
| `ctrlrelay ci ...` | CI helpers used by the dev pipeline prompt. |
|
|
33
33
|
| `ctrlrelay poller ...` | Manage the issue-poller daemon. |
|
|
@@ -128,7 +128,7 @@ Lists discovered skills as a table (name, path).
|
|
|
128
128
|
|
|
129
129
|
## `ctrlrelay bridge`
|
|
130
130
|
|
|
131
|
-
See [
|
|
131
|
+
See [Chat bridge]({{ '/bridge/' | relative_url }}) for the full setup walkthrough.
|
|
132
132
|
|
|
133
133
|
### `bridge start`
|
|
134
134
|
|
|
@@ -302,7 +302,7 @@ transport:
|
|
|
302
302
|
| Key | Type | Required | Default | Description |
|
|
303
303
|
|---|---|---|---|---|
|
|
304
304
|
| `type` | enum | no | `"file_mock"` | One of `"telegram"`, `"mattermost"`, `"file_mock"`. |
|
|
305
|
-
| `telegram` | object | required when `type=telegram` | — | Telegram
|
|
305
|
+
| `telegram` | object | required when `type=telegram` | — | Telegram settings — see below. |
|
|
306
306
|
| `mattermost` | object | required when `type=mattermost` | — | Mattermost bridge settings — see below. |
|
|
307
307
|
| `file_mock` | object | required when `type=file_mock` | — | Local-file fake transport, used for tests/dev. |
|
|
308
308
|
|
|
@@ -321,7 +321,7 @@ reach anybody.
|
|
|
321
321
|
| `question_ttl_seconds` | int | `172800` (48h) | How long an **unanswered** question stays routable before it is retired. Minimum `3600`. Without a ceiling those rows never die — a question whose PR was merged by hand weeks ago stays a live target for an orphan reply. The hourly `question_expiry_sweeper` also retires a question early once every issue/PR it cites is closed or merged. |
|
|
322
322
|
| `question_retention_days` | int | `90` | How long a **finished** question (expired, or answered and resumed) is kept before the hourly sweeper deletes it. Minimum `1`. Open questions are never deleted. The kept history is what lets a re-asked question be marked as a repeat. |
|
|
323
323
|
|
|
324
|
-
See [
|
|
324
|
+
See [Chat bridge]({{ '/bridge/' | relative_url }}) for the full setup walkthrough.
|
|
325
325
|
|
|
326
326
|
### transport.mattermost
|
|
327
327
|
|
|
@@ -52,7 +52,7 @@ pytest --cov=src/ctrlrelay --cov-report=term-missing
|
|
|
52
52
|
`pyproject.toml` sets `pythonpath = ["src"]` and `testpaths = ["tests"]` so
|
|
53
53
|
pytest finds the package without an extra install step on a fresh checkout.
|
|
54
54
|
|
|
55
|
-
The
|
|
55
|
+
The chat bridge tests stub the network — no real bot token is required to
|
|
56
56
|
run the suite. Tests that exercise the dev / secops pipelines use the
|
|
57
57
|
`file_mock` transport so they stay hermetic.
|
|
58
58
|
|
|
@@ -112,7 +112,7 @@ ctrlrelay/
|
|
|
112
112
|
│ ├── cli.py # Typer CLI entry point
|
|
113
113
|
│ ├── core/ # Dispatcher, worktree, state, config, ...
|
|
114
114
|
│ ├── pipelines/ # dev, secops, post_merge
|
|
115
|
-
│ ├── bridge/ #
|
|
115
|
+
│ ├── bridge/ # chat bridge daemon (Telegram, Mattermost)
|
|
116
116
|
│ ├── transports/ # SocketTransport, FileMockTransport
|
|
117
117
|
│ └── dashboard/ # Optional dashboard push client
|
|
118
118
|
├── tests/ # pytest suites (one file per module)
|
|
@@ -24,7 +24,7 @@ and running the dev pipeline against a real GitHub issue. Allow about 15 minutes
|
|
|
24
24
|
Optional:
|
|
25
25
|
|
|
26
26
|
- [`uv`](https://github.com/astral-sh/uv) for faster installs.
|
|
27
|
-
- A Telegram bot if you want the human-in-the-loop bridge — see [
|
|
27
|
+
- A Telegram bot or a Mattermost server if you want the human-in-the-loop bridge — see [Chat bridge]({{ '/bridge/' | relative_url }}).
|
|
28
28
|
|
|
29
29
|
## Install
|
|
30
30
|
|
|
@@ -127,8 +127,8 @@ What happens:
|
|
|
127
127
|
|
|
128
128
|
If Claude blocks asking a question, the answer mechanism only works when a
|
|
129
129
|
transport is connected. With `file_mock`, you'll see the question on the console
|
|
130
|
-
but cannot resume. Switch to the Telegram transport to actually
|
|
131
|
-
[
|
|
130
|
+
but cannot resume. Switch to the Telegram or Mattermost transport to actually
|
|
131
|
+
answer — see [Chat bridge]({{ '/bridge/' | relative_url }}) and [Feedback loop]({{ '/feedback-loop/' | relative_url }}).
|
|
132
132
|
|
|
133
133
|
## Your first poller tick
|
|
134
134
|
|
|
@@ -151,7 +151,7 @@ To run the poller as a long-lived background service, see [Operations]({{ '/oper
|
|
|
151
151
|
## Where to next
|
|
152
152
|
|
|
153
153
|
- [Configuration]({{ '/configuration/' | relative_url }}) — every key in `orchestrator.yaml`.
|
|
154
|
-
- [
|
|
154
|
+
- [Chat bridge]({{ '/bridge/' | relative_url }}) — how to set up the human-in-the-loop channel.
|
|
155
155
|
- [Feedback loop]({{ '/feedback-loop/' | relative_url }}) — what `BLOCKED_NEEDS_INPUT` actually does end-to-end.
|
|
156
156
|
- [CLI reference]({{ '/cli/' | relative_url }}) — every subcommand and flag.
|
|
157
157
|
- [Operations]({{ '/operations/' | relative_url }}) — running ctrlrelay as a service.
|
|
@@ -26,8 +26,8 @@ isolated git worktree, opens a PR, and asks you on Telegram when it gets stuck.
|
|
|
26
26
|
write your first config, run the dev pipeline against an issue.
|
|
27
27
|
- **[Configuration]({{ '/configuration/' | relative_url }})** — every key in
|
|
28
28
|
`orchestrator.yaml`, with defaults and what they actually do.
|
|
29
|
-
- **[
|
|
30
|
-
starting the bridge, troubleshooting
|
|
29
|
+
- **[Chat bridge]({{ '/bridge/' | relative_url }})** — setting up Telegram or
|
|
30
|
+
Mattermost, starting the bridge, troubleshooting delivery.
|
|
31
31
|
- **[Feedback loop]({{ '/feedback-loop/' | relative_url }})** — how
|
|
32
32
|
`BLOCKED_NEEDS_INPUT` checkpoints reach you and how your reply resumes the
|
|
33
33
|
paused Claude session.
|
|
@@ -184,7 +184,7 @@ installer uses. Save under `~/.config/systemd/user/`.
|
|
|
184
184
|
|
|
185
185
|
```ini
|
|
186
186
|
[Unit]
|
|
187
|
-
Description=ctrlrelay Telegram
|
|
187
|
+
Description=ctrlrelay chat bridge (Telegram or Mattermost)
|
|
188
188
|
After=network-online.target
|
|
189
189
|
|
|
190
190
|
[Service]
|
|
@@ -264,6 +264,7 @@ loopback or on the home network.
|
|
|
264
264
|
| `/` | The page. Refreshes itself every `web.refresh_seconds`. |
|
|
265
265
|
| `/api/status` | Everything on the page, as JSON (the contract is in `src/ctrlrelay/web/status.py`). A section that could not be read is `null` and `errors` says why; `[]` means "read, and empty". The feed holds at most the newest `feed_limit` (200) events: `feed_total` is how many the history window holds (`null` if unreadable) and `feed_truncated` is `true` when some were left out, in which case the page says "showing newest N of M". |
|
|
266
266
|
| `/api/events?since=<epoch seconds>` | The feed, newest first, at most `limit` (200) events. Without `since`, the last `web.history_days` days. Carries the same `feed_total` and `feed_truncated`, plus the `limit` used. |
|
|
267
|
+
| `/api/events?kind=<kind>&day=today` | One Today counter's events, for the server's local day, newest first, at most 200: `{kind, events, total, truncated, error}`. `kind` is `dispatched`, `pr_opened`, `merged`, `failed`, `blocked` or `message_sent`. A `failed` or `blocked` event also carries `reason`: `exit_code`, the run's `summary` (up to 500 characters) and `output_tail` (up to 2,000), both scrubbed of credential shapes again before they are sent, the `question` asked, and `session: "not found"` when the run's row is gone. `events` is `null` with `error` set when they could not be read. A query with `kind` and anything other than exactly `kind` and `day=today` is refused with 400, except that one with `since` is the feed, as before. A summary or question longer than 500 characters ends in "…". On the page, clicking a Today counter shows this list under it, read again with every refresh. |
|
|
267
268
|
|
|
268
269
|
The page header shows how long ago it last heard from the server ("updated
|
|
269
270
|
12s ago", ticking every second) and a dot that is green while the last good
|
|
@@ -290,7 +291,10 @@ machine last read it: `read_at`, and for `core`, `search` and `graphql` each
|
|
|
290
291
|
`{remaining, limit, resets_at, state}` with `state` one of `ok`, `low`, `out`,
|
|
291
292
|
or `unknown` once its reset time has passed (the numbers are then old), or
|
|
292
293
|
`"unknown"` in place of the object for a bucket it could not read; the whole value is `"unknown"`
|
|
293
|
-
before any reading. The page shows it in the **Usage** box
|
|
294
|
+
before any reading. The page shows it in the **Usage** box; a bucket whose
|
|
295
|
+
reset time has passed since the last read shows as "refilled at <time>, not
|
|
296
|
+
read since" (GitHub refills it at that time), and `search`, which refills
|
|
297
|
+
every minute, usually shows that way. Low and used-up
|
|
294
298
|
buckets are described in [configuration: github_api]({{ '/configuration/' | relative_url }}#github_api).
|
|
295
299
|
Which bucket a `gh` command spends is worked out from the command where the
|
|
296
300
|
command says: `gh api` REST paths are `core`, `gh api graphql` is `graphql`,
|
|
@@ -1846,10 +1846,14 @@ def bridge_start(
|
|
|
1846
1846
|
from ctrlrelay.core.state import StateDB
|
|
1847
1847
|
state_db = StateDB(config.paths.state_db)
|
|
1848
1848
|
|
|
1849
|
+
# #248: this machine's name on every post, and whether other
|
|
1850
|
+
# machines read the same chat (Mattermost only).
|
|
1849
1851
|
server = BridgeServer(
|
|
1850
1852
|
socket_path=socket_path,
|
|
1851
1853
|
handler=handler,
|
|
1852
1854
|
state_db=state_db,
|
|
1855
|
+
node=config.node_id,
|
|
1856
|
+
shared=bool(getattr(chat_config, "shared", False)),
|
|
1853
1857
|
)
|
|
1854
1858
|
|
|
1855
1859
|
loop = asyncio.new_event_loop()
|
|
@@ -45,7 +45,7 @@ from ctrlrelay.core.obs import get_logger, log_event
|
|
|
45
45
|
from ctrlrelay.core.usage import UsageStore, pause_for, usage_json
|
|
46
46
|
from ctrlrelay.web.fleet import PeerReader
|
|
47
47
|
from ctrlrelay.web.health import read_server_health
|
|
48
|
-
from ctrlrelay.web.status import build_status, list_feed
|
|
48
|
+
from ctrlrelay.web.status import DAY_KINDS, build_status, list_day_events, list_feed
|
|
49
49
|
|
|
50
50
|
_logger = get_logger("web.server")
|
|
51
51
|
|
|
@@ -206,6 +206,23 @@ class _Handler(BaseHTTPRequestHandler):
|
|
|
206
206
|
self._json(200, srv.status())
|
|
207
207
|
elif url.path == "/api/events":
|
|
208
208
|
query = urllib.parse.parse_qs(url.query, keep_blank_values=True)
|
|
209
|
+
if "kind" in query and "since" not in query:
|
|
210
|
+
# One Today counter's events (#257). A query with ``since``
|
|
211
|
+
# is the feed, as it was before ``kind`` existed.
|
|
212
|
+
kinds, days = query.get("kind"), query.get("day")
|
|
213
|
+
if (
|
|
214
|
+
len(kinds) != 1 or kinds[0] not in DAY_KINDS
|
|
215
|
+
or days != ["today"] or set(query) != {"kind", "day"}
|
|
216
|
+
):
|
|
217
|
+
self._json(400, {
|
|
218
|
+
"error": "kind must be one of " + ", ".join(sorted(DAY_KINDS))
|
|
219
|
+
+ ", with day=today and nothing else",
|
|
220
|
+
})
|
|
221
|
+
return
|
|
222
|
+
self._json(200, list_day_events(
|
|
223
|
+
db_path=srv.config.paths.state_db, kind=kinds[0],
|
|
224
|
+
))
|
|
225
|
+
return
|
|
209
226
|
values = query.get("since")
|
|
210
227
|
if values is None:
|
|
211
228
|
since = srv.default_since()
|
|
@@ -90,6 +90,17 @@ ul.plain > li { padding: 7px 0; border-bottom: 1px solid var(--line); }
|
|
|
90
90
|
ul.plain > li:last-child { border-bottom: 0; }
|
|
91
91
|
.today { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; }
|
|
92
92
|
.today .n { font-size: 22px; font-weight: 700; }
|
|
93
|
+
.today button.card {
|
|
94
|
+
font: inherit; color: inherit; text-align: left; cursor: pointer; width: 100%;
|
|
95
|
+
}
|
|
96
|
+
.today button.card:hover { border-color: var(--accent); }
|
|
97
|
+
.today button.card[aria-pressed="true"] { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
|
|
98
|
+
#today-list { margin-top: 10px; }
|
|
99
|
+
#today-list .reason { font-size: 13px; color: var(--muted); margin: 2px 0 0; }
|
|
100
|
+
#today-list pre {
|
|
101
|
+
white-space: pre-wrap; word-break: break-word; font-size: 12px; margin: 6px 0 0;
|
|
102
|
+
max-height: 260px; overflow: auto; background: var(--tag); padding: 8px; border-radius: 6px;
|
|
103
|
+
}
|
|
93
104
|
.feed .when { color: var(--muted); font-size: 12px; font-variant-numeric: tabular-nums; min-width: 4.5em; }
|
|
94
105
|
.feed .detail { color: var(--muted); font-size: 13px; overflow-wrap: anywhere; }
|
|
95
106
|
a { color: var(--accent); text-decoration: none; }
|
|
@@ -108,8 +119,8 @@ a:hover, a:focus-visible { text-decoration: underline; }
|
|
|
108
119
|
.usage .bar { height: 6px; background: var(--line, #ddd); border-radius: 3px; margin: 2px 0 6px; }
|
|
109
120
|
.usage .bar > span { display: block; height: 100%; border-radius: 3px; background: currentColor; }
|
|
110
121
|
.usage .bar.high { color: var(--bad); }
|
|
111
|
-
.fleet { margin:
|
|
112
|
-
.strip { display: flex; flex-wrap: wrap; gap: 10px; }
|
|
122
|
+
.fleet { margin: 18px 20px 4px; }
|
|
123
|
+
.strip { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 18px; }
|
|
113
124
|
.strip .machine { border: 1px solid var(--line); border-radius: 8px; padding: 8px 12px;
|
|
114
125
|
min-width: 190px; background: var(--panel); font-size: 13px; }
|
|
115
126
|
.strip .machine.bad { border-color: var(--bad); }
|
|
@@ -164,6 +175,7 @@ details > summary .count { font-weight: 400; margin-left: 6px; }
|
|
|
164
175
|
<section>
|
|
165
176
|
<h2>Today</h2>
|
|
166
177
|
<div id="today"><p class="unknown">loading…</p></div>
|
|
178
|
+
<div id="today-list" aria-live="polite"></div>
|
|
167
179
|
</section>
|
|
168
180
|
</div>
|
|
169
181
|
<div>
|
|
@@ -782,18 +794,32 @@ function renderGitHubBudget(api, now, lines) {
|
|
|
782
794
|
}
|
|
783
795
|
["core", "search", "graphql"].forEach(function (name) {
|
|
784
796
|
var b = api[name];
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
lines.push(el("div", "line", "GitHub " + name + " unknown"));
|
|
788
|
-
return;
|
|
789
|
-
}
|
|
790
|
-
var high = b.state === "low" || b.state === "out";
|
|
791
|
-
lines.push(el("div", "line" + (high ? " high" : ""),
|
|
792
|
-
"GitHub " + name + " " + b.remaining + " of " + b.limit + " left" +
|
|
793
|
-
(b.state === "out" ? " (used up)" : "") + ", " + untilText(b.resets_at, now)));
|
|
797
|
+
var high = b && !refilled(b, now) && (b.state === "low" || b.state === "out");
|
|
798
|
+
lines.push(el("div", "line" + (high ? " high" : ""), budgetLine(name, b, now)));
|
|
794
799
|
});
|
|
795
800
|
}
|
|
796
801
|
|
|
802
|
+
// One bucket's line. A reading whose reset time has passed is old: GitHub
|
|
803
|
+
// has refilled that bucket since, so say that rather than "unknown" (search
|
|
804
|
+
// refills every minute and is read every few, so it is usually in this state).
|
|
805
|
+
function refilled(b, now) {
|
|
806
|
+
var reset = b && typeof b === "object" ? Date.parse(b.resets_at) : NaN;
|
|
807
|
+
return !isNaN(reset) && reset / 1000 <= now;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
function budgetLine(name, b, now) {
|
|
811
|
+
if (!b || typeof b !== "object" || typeof b.remaining !== "number") {
|
|
812
|
+
return "GitHub " + name + " unknown";
|
|
813
|
+
}
|
|
814
|
+
if (refilled(b, now)) {
|
|
815
|
+
return "GitHub " + name + " refilled at " + clock(Date.parse(b.resets_at) / 1000) +
|
|
816
|
+
", not read since";
|
|
817
|
+
}
|
|
818
|
+
if (b.state === "unknown") { return "GitHub " + name + " unknown"; }
|
|
819
|
+
return "GitHub " + name + " " + b.remaining + " of " + b.limit + " left" +
|
|
820
|
+
(b.state === "out" ? " (used up)" : "") + ", " + untilText(b.resets_at, now);
|
|
821
|
+
}
|
|
822
|
+
|
|
797
823
|
function renderUsage(data, now) {
|
|
798
824
|
var target = document.getElementById("usage");
|
|
799
825
|
var n = data.node;
|
|
@@ -835,21 +861,121 @@ function renderUsage(data, now) {
|
|
|
835
861
|
put(target, lines);
|
|
836
862
|
}
|
|
837
863
|
|
|
864
|
+
// The event kind each Today counter counts (#257); the counter's own key
|
|
865
|
+
// differs only for messages.
|
|
866
|
+
var TODAY_KIND = { messages_sent: "message_sent" };
|
|
867
|
+
var todayOpen = null;
|
|
868
|
+
// Each list request is numbered; only the newest one may draw, so a slow
|
|
869
|
+
// answer for an earlier refresh or another counter never replaces a newer one.
|
|
870
|
+
var todaySeq = 0;
|
|
871
|
+
// The kind whose list request is still out. A refresh does not start
|
|
872
|
+
// another for it (on a slow server every answer would be overtaken and the
|
|
873
|
+
// list would stay at "loading…"); a click always does.
|
|
874
|
+
var todayBusy = null;
|
|
875
|
+
// A refresh skipped while a request was out: read again when it lands, so
|
|
876
|
+
// the list does not lag the count by more than one answer.
|
|
877
|
+
var todayStale = false;
|
|
878
|
+
|
|
838
879
|
function renderToday(data) {
|
|
839
880
|
var target = document.getElementById("today");
|
|
840
881
|
var t = data.today || {};
|
|
841
882
|
var grid = el("div", "today");
|
|
842
883
|
TODAY.forEach(function (pair) {
|
|
843
884
|
var v = t[pair[0]];
|
|
844
|
-
var
|
|
885
|
+
var kind = TODAY_KIND[pair[0]] || pair[0];
|
|
886
|
+
var cell = el("button", "card");
|
|
887
|
+
cell.type = "button";
|
|
888
|
+
cell.setAttribute("aria-pressed", todayOpen === kind ? "true" : "false");
|
|
889
|
+
cell.title = "Show today's " + pair[1];
|
|
845
890
|
if (typeof v === "number") { cell.appendChild(el("div", "n", v)); }
|
|
846
891
|
else { cell.appendChild(el("div", "n unknown", "unknown")); }
|
|
847
892
|
cell.appendChild(el("div", "muted", pair[1]));
|
|
893
|
+
cell.addEventListener("click", function () {
|
|
894
|
+
todayOpen = todayOpen === kind ? null : kind;
|
|
895
|
+
renderToday(lastData || data);
|
|
896
|
+
if (todayOpen) { put(document.getElementById("today-list"), [empty("loading…")]); }
|
|
897
|
+
loadTodayList(true);
|
|
898
|
+
});
|
|
848
899
|
grid.appendChild(cell);
|
|
849
900
|
});
|
|
850
901
|
put(target, [grid]);
|
|
851
902
|
}
|
|
852
903
|
|
|
904
|
+
// The list behind the open counter, read again with every refresh so it
|
|
905
|
+
// follows the count.
|
|
906
|
+
function loadTodayList(force) {
|
|
907
|
+
var target = document.getElementById("today-list");
|
|
908
|
+
var kind = todayOpen;
|
|
909
|
+
if (!kind) { todaySeq++; todayBusy = null; put(target, []); return; }
|
|
910
|
+
if (!force && todayBusy === kind) { todayStale = true; return; }
|
|
911
|
+
var seq = ++todaySeq;
|
|
912
|
+
todayBusy = kind;
|
|
913
|
+
todayStale = false;
|
|
914
|
+
fetch("/api/events?kind=" + encodeURIComponent(kind) + "&day=today", { cache: "no-store" })
|
|
915
|
+
.then(function (resp) { return resp.json(); })
|
|
916
|
+
.then(function (data) { if (seq === todaySeq) { renderTodayList(data); } })
|
|
917
|
+
.catch(function (err) {
|
|
918
|
+
if (seq === todaySeq) {
|
|
919
|
+
put(target, [unknown("unknown: the list could not be read (" +
|
|
920
|
+
(err && err.message ? err.message : "error") + ")")]);
|
|
921
|
+
}
|
|
922
|
+
})
|
|
923
|
+
.then(function () {
|
|
924
|
+
if (seq !== todaySeq) { return; }
|
|
925
|
+
todayBusy = null;
|
|
926
|
+
if (todayStale && todayOpen === kind) { loadTodayList(false); }
|
|
927
|
+
});
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
function renderTodayList(data) {
|
|
931
|
+
var target = document.getElementById("today-list");
|
|
932
|
+
if (!Array.isArray(data.events)) {
|
|
933
|
+
put(target, [unknown("unknown: " + (data.error || "the list could not be read"))]);
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
if (data.events.length === 0) { put(target, [empty("none today")]); return; }
|
|
937
|
+
var list = el("ul", "plain");
|
|
938
|
+
data.events.forEach(function (ev) {
|
|
939
|
+
var li = el("li", "card");
|
|
940
|
+
var row = el("div", "row");
|
|
941
|
+
row.appendChild(el("span", "when", clock(ev.ts)));
|
|
942
|
+
if (ev.repo) {
|
|
943
|
+
row.appendChild(ghLink(ev.url,
|
|
944
|
+
ev.repo + (typeof ev.issue_number === "number" ? "#" + ev.issue_number : ""), "repo"));
|
|
945
|
+
}
|
|
946
|
+
if (ev.session_id) { row.appendChild(el("span", "muted", ev.session_id)); }
|
|
947
|
+
li.appendChild(row);
|
|
948
|
+
var r = ev.reason;
|
|
949
|
+
if (r) {
|
|
950
|
+
if (r.session === "not found") {
|
|
951
|
+
li.appendChild(el("p", "reason", "the run's record is gone"));
|
|
952
|
+
}
|
|
953
|
+
if (typeof r.exit_code === "number") {
|
|
954
|
+
li.appendChild(el("p", "reason", "exit code " + r.exit_code));
|
|
955
|
+
}
|
|
956
|
+
if (r.question) { li.appendChild(el("p", "reason", "asked: " + r.question)); }
|
|
957
|
+
if (r.summary) { li.appendChild(el("p", "reason", r.summary)); }
|
|
958
|
+
if (r.output_tail) {
|
|
959
|
+
var more = el("details");
|
|
960
|
+
more.appendChild(el("summary", null, "output"));
|
|
961
|
+
more.appendChild(el("pre", null, r.output_tail));
|
|
962
|
+
li.appendChild(more);
|
|
963
|
+
}
|
|
964
|
+
if (!r.question && !r.summary && !r.output_tail && typeof r.exit_code !== "number"
|
|
965
|
+
&& r.session !== "not found") {
|
|
966
|
+
li.appendChild(el("p", "reason", "no reason was recorded"));
|
|
967
|
+
}
|
|
968
|
+
} else {
|
|
969
|
+
var text = detailText(ev.detail);
|
|
970
|
+
if (text) { li.appendChild(el("div", "detail", text)); }
|
|
971
|
+
}
|
|
972
|
+
list.appendChild(li);
|
|
973
|
+
});
|
|
974
|
+
var note = data.truncated === true
|
|
975
|
+
? el("p", "empty", "showing newest " + data.events.length + " of " + data.total) : null;
|
|
976
|
+
put(target, note ? [note, list] : [list]);
|
|
977
|
+
}
|
|
978
|
+
|
|
853
979
|
function detailText(detail) {
|
|
854
980
|
if (detail === null || detail === undefined) { return ""; }
|
|
855
981
|
if (typeof detail !== "object") { return String(detail); }
|
|
@@ -917,6 +1043,7 @@ function render(data) {
|
|
|
917
1043
|
renderWaiting(data);
|
|
918
1044
|
renderSkipped(data);
|
|
919
1045
|
renderToday(data);
|
|
1046
|
+
loadTodayList();
|
|
920
1047
|
renderFleet(data, now);
|
|
921
1048
|
renderUsage(data, now);
|
|
922
1049
|
renderServer(data);
|