hecks 7.0.0 → 7.1.0
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.
- checksums.yaml +4 -4
- data/lib/hecks/adapters/driven/postgres_era/lineage/chain_sql.rb +2 -1
- data/lib/hecks/adapters/driven/postgres_era/lineage/provisioning.rb +4 -2
- data/lib/hecks/adapters/driven/postgres_era/lineage/split_sql.rb +16 -8
- data/lib/hecks/adapters/driven/postgres_era/lineage/transform_sql.rb +7 -3
- data/lib/hecks/adapters/driven/postgres_era/lineage_manager/era_resolver.rb +2 -1
- data/lib/hecks/adapters/driven/postgres_era/lineage_manager/mint_request.rb +2 -1
- data/lib/hecks/adapters/driven/postgres_era/lineage_manager/refusals.rb +2 -1
- data/lib/hecks/adapters/driving/cli_runner/bang_rule.rb +3 -0
- data/lib/hecks/adapters/driving/cli_runner/completion_words.rb +2 -0
- data/lib/hecks/adapters/driving/cli_runner/read_context.rb +6 -3
- data/lib/hecks/adapters/driving/cli_runner/renamed_names.rb +2 -1
- data/lib/hecks/adapters/driving/cli_runner/resolution.rb +2 -2
- data/lib/hecks/adapters/driving/cli_runner/resume_words.rb +1 -0
- data/lib/hecks/adapters/driving/cli_runner/run_reference.rb +6 -3
- data/lib/hecks/adapters/driving/cli_runner/search_words.rb +2 -0
- data/lib/hecks/adapters/driving/cli_runner/tour_words.rb +1 -0
- data/lib/hecks/adapters/driving/cli_runner/verb_names.rb +2 -1
- data/lib/hecks/adapters/driving/cli_runner/verbosity.rb +2 -0
- data/lib/hecks/adapters/driving/cli_runner/word_help.rb +40 -0
- data/lib/hecks/adapters/driving/cli_runner.rb +2 -0
- data/lib/hecks/adapters/driving/mcp/progress_sink.rb +4 -2
- data/lib/hecks/adapters/driving/mcp.rb +2 -1
- data/lib/hecks/adapters/driving/mcp_scope/argument_policy.rb +2 -1
- data/lib/hecks/adapters/driving/ruby/aggregate_module/hooks.rb +28 -14
- data/lib/hecks/adapters/driving/ruby/aggregate_module.rb +4 -2
- data/lib/hecks/assembly.rb +18 -4
- data/lib/hecks/behaviors/expectations/adapter_selection.rb +82 -0
- data/lib/hecks/behaviors/expectations/boot.rb +12 -5
- data/lib/hecks/bluebook/acknowledgements.rb +1 -1
- data/lib/hecks/bluebook/command.rb +3 -2
- data/lib/hecks/bluebook/declaration.rb +2 -1
- data/lib/hecks/bluebook/dsl/adapter_builder.rb +9 -1
- data/lib/hecks/bluebook/dsl/bluebook_builder/metadata.rb +6 -4
- data/lib/hecks/bluebook/dsl/bluebook_builder/modules.rb +6 -3
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation/correlation_keys.rb +2 -1
- data/lib/hecks/bluebook/dsl/command_builder/folded_sets.rb +4 -2
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +2 -1
- data/lib/hecks/bluebook/hecksagon.rb +6 -3
- data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +9 -2
- data/lib/hecks/bluebook/meta_validator.rb +1 -1
- data/lib/hecks/bluebook/model_check/aggregate_checks.rb +2 -1
- data/lib/hecks/bluebook/model_check/classification_checks.rb +51 -0
- data/lib/hecks/bluebook/model_check/context_checks.rb +2 -0
- data/lib/hecks/bluebook/model_check/redundant_relationships.rb +2 -1
- data/lib/hecks/ci_speed/adapters/shard_partitioner.adapter +7 -0
- data/lib/hecks/ci_speed/adapters/shard_partitioner.rb +66 -0
- data/lib/hecks/ci_speed/adapters/timings_source.adapter +7 -0
- data/lib/hecks/ci_speed/adapters/timings_source.rb +68 -0
- data/lib/hecks/ci_speed/ci_speed.bluebook +111 -0
- data/lib/hecks/ci_speed/ci_speed.ports.hecksagon +22 -0
- data/lib/hecks/ci_workflows/ci_workflows.bluebook +50 -6
- data/lib/hecks/coder/coder.bluebook +3 -1
- data/lib/hecks/command_names/index.rb +1 -1
- data/lib/hecks/command_names/resolution.rb +15 -5
- data/lib/hecks/composer/toolbox.rb +2 -1
- data/lib/hecks/corpus.rb +4 -0
- data/lib/hecks/doc/reference/readme.rb +1 -1
- data/lib/hecks/framework_bluebooks.rb +3 -2
- data/lib/hecks/fuzzing/differential.rb +1 -1
- data/lib/hecks/fuzzing/era_boundary.rb +1 -1
- data/lib/hecks/fuzzing/isolated_boot/postgres_scratch.rb +1 -1
- data/lib/hecks/fuzzing/persistence_parity.rb +1 -1
- data/lib/hecks/fuzzing/target_capabilities.rb +1 -1
- data/lib/hecks/hecks/adapters/ci_runs.adapter +7 -0
- data/lib/hecks/hecks/adapters/ci_runs.rb +80 -0
- data/lib/hecks/hecks/adapters/claude_agent_list.rb +43 -0
- data/lib/hecks/hecks/adapters/claude_agents.rb +50 -19
- data/lib/hecks/hecks/adapters/claude_jobs.rb +36 -0
- data/lib/hecks/hecks/adapters/codebase/corpus_queries.rb +2 -1
- data/lib/hecks/hecks/adapters/codebase/minitest_runs.rb +4 -2
- data/lib/hecks/hecks/adapters/codebase/run_options.rb +2 -1
- data/lib/hecks/hecks/adapters/codebase/test_suite.rb +3 -19
- data/lib/hecks/hecks/adapters/dispatching/brief_text.rb +5 -3
- data/lib/hecks/hecks/adapters/dispatching/launcher.rb +2 -1
- data/lib/hecks/hecks/adapters/dispatching/plan.rb +8 -4
- data/lib/hecks/hecks/adapters/dispatching/settings.rb +2 -1
- data/lib/hecks/hecks/adapters/dispatching/settler.rb +4 -2
- data/lib/hecks/hecks/adapters/dispatching.rb +6 -3
- data/lib/hecks/hecks/adapters/doctor_reading.rb +4 -2
- data/lib/hecks/hecks/adapters/git_landing/carried.rb +42 -0
- data/lib/hecks/hecks/adapters/git_landing/finisher.rb +2 -1
- data/lib/hecks/hecks/adapters/git_landing/flagged.rb +32 -0
- data/lib/hecks/hecks/adapters/git_landing/tidy.rb +27 -14
- data/lib/hecks/hecks/adapters/git_landing.rb +30 -13
- data/lib/hecks/hecks/adapters/git_leftovers.adapter +7 -0
- data/lib/hecks/hecks/adapters/git_leftovers.rb +49 -0
- data/lib/hecks/hecks/adapters/git_main_pull.rb +2 -2
- data/lib/hecks/hecks/adapters/git_train.rb +9 -5
- data/lib/hecks/hecks/adapters/git_worktree_add.rb +8 -4
- data/lib/hecks/hecks/adapters/github_pull_requests.rb +2 -1
- data/lib/hecks/hecks/adapters/in_process_operations.rb +2 -1
- data/lib/hecks/hecks/adapters/journal_store/edge_audit.rb +1 -1
- data/lib/hecks/hecks/adapters/journal_store/readings/composition.rb +2 -1
- data/lib/hecks/hecks/adapters/journal_store/translation_write/shape_edge.rb +4 -2
- data/lib/hecks/hecks/adapters/journal_store/translation_write.rb +4 -2
- data/lib/hecks/hecks/adapters/landed_branches.rb +38 -0
- data/lib/hecks/hecks/adapters/main_boarding.adapter +7 -0
- data/lib/hecks/hecks/adapters/main_boarding.rb +85 -0
- data/lib/hecks/hecks/adapters/main_integration/checks.rb +63 -0
- data/lib/hecks/hecks/adapters/main_integration/command_runner.rb +32 -0
- data/lib/hecks/hecks/adapters/main_integration/range_guard.rb +49 -0
- data/lib/hecks/hecks/adapters/main_integration/repository.rb +83 -0
- data/lib/hecks/hecks/adapters/main_integration/runs.rb +51 -0
- data/lib/hecks/hecks/adapters/main_integration/train.rb +74 -0
- data/lib/hecks/hecks/adapters/main_integration.adapter +7 -0
- data/lib/hecks/hecks/adapters/main_integration.rb +77 -0
- data/lib/hecks/hecks/adapters/main_verification.adapter +7 -0
- data/lib/hecks/hecks/adapters/main_verification.rb +89 -0
- data/lib/hecks/hecks/adapters/push_recorder.adapter +7 -0
- data/lib/hecks/hecks/adapters/push_recorder.rb +78 -0
- data/lib/hecks/hecks/adapters/red_flag_guard.rb +62 -0
- data/lib/hecks/hecks/adapters/red_flag_marker.rb +101 -0
- data/lib/hecks/hecks/adapters/red_flag_report.rb +59 -0
- data/lib/hecks/hecks/adapters/red_flag_survey.rb +65 -0
- data/lib/hecks/hecks/adapters/red_flagging.adapter +7 -0
- data/lib/hecks/hecks/adapters/red_flagging.rb +98 -0
- data/lib/hecks/hecks/adapters/rehearsal/inquiry.rb +6 -1
- data/lib/hecks/hecks/adapters/rehearsal/memory_store.rb +6 -3
- data/lib/hecks/hecks/adapters/settings_reading/event_tap_row.rb +33 -0
- data/lib/hecks/hecks/adapters/settings_reading.rb +3 -1
- data/lib/hecks/hecks/codebase.behaviors +0 -6
- data/lib/hecks/hecks/custodian.behaviors +22 -1
- data/lib/hecks/hecks/delegate/delegate.bluebook +128 -15
- data/lib/hecks/hecks/deliver/deliver.bluebook +450 -2
- data/lib/hecks/hecks/environments/memory.world +5 -0
- data/lib/hecks/hecks/hecks.contextmap +8 -0
- data/lib/hecks/hecks/hecks.hecksagon +106 -6
- data/lib/hecks/hecks/hecks.world +9 -1
- data/lib/hecks/hecks/migrate/migrate.bluebook +6 -1
- data/lib/hecks/hecks/redflag/redflag.bluebook +249 -0
- data/lib/hecks/hecks/test/test.bluebook +0 -19
- data/lib/hecks/language/adapter.bluebook +13 -0
- data/lib/hecks/language/bluebook/vocabulary.bluebook +8 -9
- data/lib/hecks/language/sentence/sentence.bluebook +2 -0
- data/lib/hecks/pipeline/pipeline.bluebook +251 -0
- data/lib/hecks/pipeline/pipeline.hecksagon +17 -0
- data/lib/hecks/pipeline_board/adapters/cache.rb +43 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_fill.rb +56 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_git.rb +82 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_github/runs.rb +66 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_github.adapter +7 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_github.rb +70 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_jobs.adapter +7 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_jobs.rb +87 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_journal/chain.rb +40 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_journal/tap.rb +75 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_journal.adapter +7 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_journal.rb +83 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_stages/ci_tiles.rb +85 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_stages/fanout.rb +73 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_stages/gather.rb +57 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_stages/metrics.rb +36 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_stages/strip.rb +102 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_stages/tile_kit.rb +57 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_stages/work_tiles.rb +59 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_stages.adapter +7 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_stages.rb +84 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_tickets.adapter +7 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_tickets.rb +77 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_web/page.html +292 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_web/sequence.html +190 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_web/server.rb +105 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_web/snapshot.rb +57 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_web/stream.rb +59 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_web/wiring.html +536 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_web.adapter +7 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_web.rb +80 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_wiring/details.rb +85 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_wiring/graph.rb +120 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_wiring.adapter +7 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_wiring.rb +36 -0
- data/lib/hecks/pipeline_board/adapters/probe.rb +48 -0
- data/lib/hecks/pipeline_board/pipeline_board.bluebook +144 -0
- data/lib/hecks/pipeline_board/pipeline_board.hecksagon +12 -0
- data/lib/hecks/pipeline_board/pipeline_board.ports.hecksagon +47 -0
- data/lib/hecks/ports/persistence/binding_policy.rb +2 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/compose.rb +4 -3
- data/lib/hecks/ports/persistence/repository_factory.rb +2 -1
- data/lib/hecks/projections/ci_pipeline/table.rb +2 -1
- data/lib/hecks/projections/deploy/lambda/stack/recipes.rb +2 -2
- data/lib/hecks/projections/undo.rb +2 -1
- data/lib/hecks/projector/cli_projector/command_help.rb +5 -4
- data/lib/hecks/projector/cli_projector/renames.rb +4 -2
- data/lib/hecks/projector/cli_projector/usage.rb +4 -3
- data/lib/hecks/projector/exporter/adapters.rb +3 -2
- data/lib/hecks/projector/narrate_projector/use_cases.rb +74 -0
- data/lib/hecks/projector/narrate_projector.rb +10 -3
- data/lib/hecks/quality_control/adapters/main_maintenance.rb +9 -5
- data/lib/hecks/quality_control/adapters/qa_auto_mint.rb +21 -2
- data/lib/hecks/quality_control/cli/qa_concurrency_racer.rb +1 -1
- data/lib/hecks/quality_control/cli/qa_domain_novelty.rb +2 -1
- data/lib/hecks/quality_control/cli/qa_fix_main_bug/publishing.rb +8 -4
- data/lib/hecks/quality_control/cli/qa_fix_main_bug.rb +10 -8
- data/lib/hecks/quality_control/cli/qa_generated_domains/promotion.rb +2 -2
- data/lib/hecks/quality_control/cli/qa_main_gate/gate_run.rb +6 -3
- data/lib/hecks/quality_control/cli/qa_main_gate/logging.rb +8 -4
- data/lib/hecks/quality_control/cli/qa_main_gate/shell.rb +4 -2
- data/lib/hecks/quality_control/cli/qa_main_gate/sweeping.rb +8 -4
- data/lib/hecks/quality_control/cli/qa_main_gate/worktree.rb +3 -2
- data/lib/hecks/quality_control/cli/qa_main_gate.rb +10 -5
- data/lib/hecks/quality_control/cli/qa_sweep/all_mode/children.rb +1 -1
- data/lib/hecks/quality_control/cli/qa_sweep/checks.rb +1 -1
- data/lib/hecks/quality_control/cli/qa_sweep.rb +1 -1
- data/lib/hecks/quality_control/cli/qa_tick.rb +6 -3
- data/lib/hecks/quality_control/quality_control.bluebook +11 -11
- data/lib/hecks/quality_control/quality_control.ports.hecksagon +6 -3
- data/lib/hecks/query_ir/touchpoints.rb +2 -1
- data/lib/hecks/refused.rb +2 -1
- data/lib/hecks/release/approval.rb +4 -2
- data/lib/hecks/runtime/event_log.rb +27 -0
- data/lib/hecks/runtime/event_tap.rb +106 -0
- data/lib/hecks/runtime/loader/boot_steps.rb +1 -1
- data/lib/hecks/runtime/loader.rb +35 -19
- data/lib/hecks/runtime/registry/durability_warnings.rb +12 -5
- data/lib/hecks/runtime/registry/runtime_state.rb +4 -1
- data/lib/hecks/runtime/registry/saga_anchor.rb +54 -0
- data/lib/hecks/runtime/registry/saga_durability_rule.rb +66 -0
- data/lib/hecks/runtime/registry/saga_persistence.rb +58 -12
- data/lib/hecks/runtime/registry/verification.rb +7 -3
- data/lib/hecks/runtime/saga_interpreter/lifecycle.rb +2 -1
- data/lib/hecks/runtime/saga_interpreter.rb +1 -1
- data/lib/hecks/rust_format.rb +2 -1
- data/lib/hecks/shape_ledger/edge_hygiene.rb +10 -5
- data/lib/hecks/shape_ledger.rb +1 -1
- data/lib/hecks/storehouse/audit.rb +2 -1
- data/lib/hecks/storehouse/identity.rb +6 -3
- data/lib/hecks/storehouse.rb +2 -1
- data/lib/hecks/tickets/bluebook/tickets.bluebook +5 -4
- data/lib/hecks/tools/ci_workflows.rb +2 -1
- data/lib/hecks/tools/document.rb +12 -1
- data/lib/hecks/tools/downstream_model_check.rb +4 -2
- data/lib/hecks/tools/fuzz_sweep/shards.rb +9 -30
- data/lib/hecks/tools/model_check/examination.rb +25 -2
- data/lib/hecks/tools/move_disables.rb +4 -2
- data/lib/hecks/tools/regeneration_run/scratch.rb +2 -1
- data/lib/hecks/tools/rspec_shard_files.rb +12 -15
- data/lib/hecks/tools/sequence_script.rb +1 -1
- data/lib/hecks/tools/shard_planner.rb +65 -0
- data/lib/hecks/tools/stage_progress.rb +2 -1
- data/lib/hecks/tools/stress_concurrency_specs.rb +2 -1
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks/vocabulary.rb +8 -9
- data/lib/hecks.rb +5 -2
- data/prebuilt/{13c04bdc5970295b310f0a980d397ba722232c8da99cc1a0cdb383249921e871.marshal → 4b81646f769ae5b1e7457f9b11c48d8bc1093e96a1dd6fb61864065121ca6f01.marshal} +0 -0
- data/prebuilt/{e3857b77f47d9ecc1787b2e1f5136364f3cb3444ef4aea5479f3b98167affea2.marshal → 681890632e3f4a99646623dcae44af36b1bad66c515f5abf67a2460372b9c6fd.marshal} +0 -0
- data/rust/host/HECKS_RELEASE +1 -1
- data/rust/parser/src/emit.rs +5 -1
- data/rust/parser/src/ir.rs +2 -0
- data/rust/parser/src/keywords.rs +12 -0
- data/rust/parser/src/main.rs +1 -0
- data/rust/parser/src/parse/adapter.rs +1 -0
- data/rust/parser/src/phrases.rs +16 -0
- data/rust/parser/src/sentence/examples.rs +93 -0
- data/rust/parser/src/sentence/mod.rs +1 -0
- metadata +89 -5
- data/lib/hecks/tools/refresh_rspec_runtime_baseline.rb +0 -166
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Pipeline Wiring</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
color-scheme: light;
|
|
10
|
+
--page: #f9f9f7; --surface: #fcfcfb; --ink: #0b0b0b; --ink2: #52514e; --muted: #898781;
|
|
11
|
+
--grid: #e1e0d9; --axis: #c3c2b7; --border: rgba(11,11,11,.10);
|
|
12
|
+
--series1: #2a78d6; --good: #0ca30c; --warning: #fab219; --critical: #d03b3b;
|
|
13
|
+
}
|
|
14
|
+
@media (prefers-color-scheme: dark) {
|
|
15
|
+
:root:not([data-theme="light"]) {
|
|
16
|
+
color-scheme: dark;
|
|
17
|
+
--page: #0d0d0d; --surface: #1a1a19; --ink: #f0efec; --ink2: #c3c2b7; --muted: #898781;
|
|
18
|
+
--grid: #2c2c2a; --axis: #383835; --border: rgba(255,255,255,.10); --series1: #3987e5;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
:root[data-theme="dark"] {
|
|
22
|
+
color-scheme: dark;
|
|
23
|
+
--page: #0d0d0d; --surface: #1a1a19; --ink: #f0efec; --ink2: #c3c2b7; --muted: #898781;
|
|
24
|
+
--grid: #2c2c2a; --axis: #383835; --border: rgba(255,255,255,.10); --series1: #3987e5;
|
|
25
|
+
}
|
|
26
|
+
* { box-sizing: border-box; }
|
|
27
|
+
body { margin: 0; background: var(--page); color: var(--ink); font: 14px/1.45 system-ui, -apple-system, "Segoe UI", sans-serif; }
|
|
28
|
+
main { max-width: 1280px; margin: 0 auto; padding: 16px; }
|
|
29
|
+
header { display: flex; flex-wrap: wrap; gap: 8px 16px; align-items: baseline; justify-content: space-between; margin-bottom: 12px; }
|
|
30
|
+
h1 { font-size: 20px; margin: 0; font-weight: 600; }
|
|
31
|
+
a { color: var(--series1); }
|
|
32
|
+
.meta { color: var(--ink2); font-size: 13px; display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
|
|
33
|
+
button { font: inherit; color: var(--ink); background: var(--surface); border: 1px solid var(--axis); border-radius: 6px; padding: 4px 10px; cursor: pointer; }
|
|
34
|
+
button:focus-visible, a:focus-visible { outline: 2px solid var(--series1); outline-offset: 2px; }
|
|
35
|
+
section { background: var(--surface); border: 1px solid var(--border); border-radius: 10px; padding: 12px; margin-bottom: 16px; }
|
|
36
|
+
.legend { display: flex; gap: 16px; flex-wrap: wrap; font-size: 12px; color: var(--ink2); margin-bottom: 8px; }
|
|
37
|
+
.legend b { color: var(--ink); font-weight: 600; }
|
|
38
|
+
.swatch { display: inline-block; width: 10px; height: 10px; border-radius: 2px; margin-right: 5px; vertical-align: middle; }
|
|
39
|
+
canvas { width: 100%; height: 720px; display: block; touch-action: none; border: 1px solid var(--grid); border-radius: 8px; background: var(--page); }
|
|
40
|
+
.log { list-style: none; margin: 0; padding: 0; font-size: 13px; max-height: 260px; overflow: auto; }
|
|
41
|
+
.log li { padding: 4px 0; border-bottom: 1px solid var(--grid); overflow-wrap: anywhere; }
|
|
42
|
+
.log time { color: var(--muted); font-variant-numeric: tabular-nums; margin-right: 8px; }
|
|
43
|
+
.log .mark { font-weight: 600; margin-left: 6px; }
|
|
44
|
+
h2 { font-size: 13px; margin: 0 0 8px; color: var(--ink2); font-weight: 600; text-transform: uppercase; letter-spacing: .04em; }
|
|
45
|
+
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
46
|
+
th, td { text-align: left; padding: 4px 8px; border-bottom: 1px solid var(--grid); }
|
|
47
|
+
th { color: var(--ink2); font-size: 12px; }
|
|
48
|
+
.scroll { overflow: auto; max-height: 420px; }
|
|
49
|
+
.tables-only canvas { display: none; } body:not(.tables-only) #edgetable { display: none; }
|
|
50
|
+
.tip { position: fixed; pointer-events: none; background: var(--ink); color: var(--surface); padding: 4px 8px; border-radius: 6px; font-size: 12px; z-index: 10; display: none; max-width: 300px; }
|
|
51
|
+
.unknown-note { color: var(--ink2); font-style: italic; }
|
|
52
|
+
.panel { position: fixed; top: 0; right: 0; width: 400px; max-width: 100vw; height: 100vh; overflow: auto; background: var(--surface); border-left: 1px solid var(--border); box-shadow: -8px 0 24px rgba(0,0,0,.18); padding: 14px 16px 24px; z-index: 20; }
|
|
53
|
+
.panel[hidden] { display: none; }
|
|
54
|
+
.panel .top { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; }
|
|
55
|
+
.panel h2 { font-size: 16px; margin: 0; color: var(--ink); text-transform: none; letter-spacing: 0; }
|
|
56
|
+
.panel h3 { font-size: 12px; margin: 16px 0 6px; color: var(--ink2); text-transform: uppercase; letter-spacing: .04em; }
|
|
57
|
+
.panel .chip { display: inline-block; font-size: 11px; padding: 1px 7px; border-radius: 999px; border: 1px solid var(--border); color: var(--ink2); margin-right: 6px; }
|
|
58
|
+
.panel .desc { color: var(--ink2); font-size: 13px; }
|
|
59
|
+
.panel .stat { display: flex; gap: 14px; flex-wrap: wrap; font-size: 13px; }
|
|
60
|
+
.panel .stat b { font-size: 20px; font-weight: 600; display: block; }
|
|
61
|
+
.panel ul { list-style: none; margin: 0; padding: 0; font-size: 13px; }
|
|
62
|
+
.panel li { padding: 4px 0; border-bottom: 1px solid var(--grid); overflow-wrap: anywhere; }
|
|
63
|
+
.panel li small { color: var(--muted); }
|
|
64
|
+
.panel .ev time { color: var(--muted); font-variant-numeric: tabular-nums; margin-right: 6px; font-size: 12px; }
|
|
65
|
+
.panel .ev .re { display: block; color: var(--ink2); font-size: 12px; padding-left: 14px; }
|
|
66
|
+
.panel .mark { font-weight: 600; }
|
|
67
|
+
.panel .states span { display: inline-block; border: 1px solid var(--axis); border-radius: 4px; padding: 1px 6px; margin: 2px 4px 2px 0; font-size: 12px; }
|
|
68
|
+
.panel .link { background: none; border: none; color: var(--series1); padding: 0; cursor: pointer; text-decoration: underline; }
|
|
69
|
+
@media (max-width: 700px) { canvas { height: 460px; } }
|
|
70
|
+
</style>
|
|
71
|
+
</head>
|
|
72
|
+
<body>
|
|
73
|
+
<main>
|
|
74
|
+
<header>
|
|
75
|
+
<h1>Pipeline Wiring</h1>
|
|
76
|
+
<div class="meta">
|
|
77
|
+
<a href="/">Board</a><a href="/sequence">Sequence</a>
|
|
78
|
+
<span id="count">loading the wiring</span>
|
|
79
|
+
<span id="live">stream: connecting</span>
|
|
80
|
+
<button id="zout" type="button" aria-label="Zoom out">−</button>
|
|
81
|
+
<button id="zin" type="button" aria-label="Zoom in">+</button>
|
|
82
|
+
<button id="fit" type="button">Fit</button>
|
|
83
|
+
<button id="all" type="button">Expand all</button>
|
|
84
|
+
<button id="bounds" type="button" aria-pressed="true">Hide boundaries</button>
|
|
85
|
+
<button id="replay" type="button">Replay recent</button>
|
|
86
|
+
<button id="toggle" type="button" aria-pressed="false">Show table</button>
|
|
87
|
+
</div>
|
|
88
|
+
</header>
|
|
89
|
+
<section aria-labelledby="h-map">
|
|
90
|
+
<h2 id="h-map">Aggregates, ports and adapters, with messages as they move</h2>
|
|
91
|
+
<div class="legend" id="legend">
|
|
92
|
+
<span><b>▭</b> aggregate</span><span><b>◇</b> port</span><span><b>⬢</b> adapter</span>
|
|
93
|
+
<span><b style="color:var(--good)">✓</b> delivered</span>
|
|
94
|
+
<span><b style="color:var(--warning)">▲</b> waiting</span>
|
|
95
|
+
<span><b style="color:var(--critical)">✕</b> refused</span>
|
|
96
|
+
<span>a line rests in its domain's colour, lights in the outcome's colour when a message crosses it, and fades over 20 s</span>
|
|
97
|
+
<span>each context is one block with its aggregate count; click a block to open it, click its name to close it; scroll to zoom (zooming in opens everything), drag to pan; packed shapes move aside under the pointer, and a click opens the sidebar</span>
|
|
98
|
+
</div>
|
|
99
|
+
<div class="legend" id="domains"></div>
|
|
100
|
+
<canvas id="map" role="img" aria-label="Wiring diagram: policies between aggregates, ports and adapters, with live messages"></canvas>
|
|
101
|
+
<div class="scroll" id="edgetable"></div>
|
|
102
|
+
</section>
|
|
103
|
+
<section aria-labelledby="h-log"><h2 id="h-log">Messages</h2><ul class="log" id="log" aria-live="polite"></ul></section>
|
|
104
|
+
</main>
|
|
105
|
+
<div class="tip" id="tip" role="tooltip"></div>
|
|
106
|
+
<aside class="panel" id="panel" hidden aria-label="Selected node">
|
|
107
|
+
<div class="top"><h2 id="ptitle"></h2><span><button id="focus" type="button" aria-pressed="false">Focus</button> <button id="pclose" type="button" aria-label="Close">✕</button></span></div>
|
|
108
|
+
<div id="pbody"></div>
|
|
109
|
+
</aside>
|
|
110
|
+
<script>
|
|
111
|
+
"use strict";
|
|
112
|
+
const $ = (id) => document.getElementById(id);
|
|
113
|
+
const canvas = $("map"), ctx = canvas.getContext("2d"), tip = $("tip");
|
|
114
|
+
const css = (name) => getComputedStyle(document.documentElement).getPropertyValue(name).trim();
|
|
115
|
+
const reduceMotion = matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
116
|
+
let nodes = [], edges = [], byId = new Map(), view = { s: 1, x: 0, y: 0 }, hover = null;
|
|
117
|
+
const pulses = new Map(), particles = [], seen = new Map();
|
|
118
|
+
/* Categorical slots from the reference palette, in their fixed order; a 9th domain folds to muted. */
|
|
119
|
+
const SLOTS = { light: ["#2a78d6", "#eb6834", "#1baf7a", "#eda100", "#e87ba4", "#008300", "#6250d6", "#e34948"],
|
|
120
|
+
dark: ["#3987e5", "#d95926", "#199e70", "#c98500", "#d55181", "#008300", "#9085e9", "#e66767"] };
|
|
121
|
+
let domains = [], domainColour = new Map(), showBounds = true;
|
|
122
|
+
/* A lit line or node stays at full for LINGER - LINGER_FADE ms, then fades over LINGER_FADE ms. */
|
|
123
|
+
const LINGER = 22000, LINGER_FADE = 14000;
|
|
124
|
+
const dark = () => matchMedia("(prefers-color-scheme: dark)").matches && document.documentElement.dataset.theme !== "light" || document.documentElement.dataset.theme === "dark";
|
|
125
|
+
function colourOf(domain) { const i = domainColour.get(domain); return i === undefined || i > 7 ? css("--muted") : SLOTS[dark() ? "dark" : "light"][i]; }
|
|
126
|
+
|
|
127
|
+
function h(tag, attrs, ...kids) {
|
|
128
|
+
const el = document.createElement(tag);
|
|
129
|
+
for (const [k, v] of Object.entries(attrs || {})) el.setAttribute(k, v);
|
|
130
|
+
for (const kid of kids.flat()) if (kid !== null && kid !== undefined) el.append(kid.nodeType ? kid : String(kid));
|
|
131
|
+
return el;
|
|
132
|
+
}
|
|
133
|
+
function clock(iso) { const d = new Date(iso); return isNaN(d) ? "" : d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" }); }
|
|
134
|
+
|
|
135
|
+
/* ---- domains: an adapter belongs to the domain of the port it answers, but sits outside its hull ---- */
|
|
136
|
+
/* A context is a bluebook's domain module when it declares one, else the bluebook itself. */
|
|
137
|
+
const keyOf = (n) => n.context ? n.domain + " / " + n.context : n.domain;
|
|
138
|
+
let contexts = [];
|
|
139
|
+
function assignDomains() {
|
|
140
|
+
for (const e of edges) if (e.kind === "adapter") { const p = byId.get(e.from), a = byId.get(e.to); if (p && a) { a.domain = p.domain; a.context = p.context; } }
|
|
141
|
+
const count = new Map(), ctx = new Map();
|
|
142
|
+
for (const n of nodes) if (n.kind !== "adapter") { count.set(n.domain, (count.get(n.domain) || 0) + 1); ctx.set(keyOf(n), (ctx.get(keyOf(n)) || 0) + 1); }
|
|
143
|
+
domains = [...count.keys()].sort((a, b) => count.get(b) - count.get(a));
|
|
144
|
+
domainColour = new Map(domains.map((d, i) => [d, i]));
|
|
145
|
+
contexts = [...ctx.keys()].sort((a, b) => ctx.get(b) - ctx.get(a));
|
|
146
|
+
}
|
|
147
|
+
/* ---- layout: domains on a ring, their contexts on a ring around each, aggregates inside, ports on the rim, adapters outside ---- */
|
|
148
|
+
let centre = new Map();
|
|
149
|
+
function layout() {
|
|
150
|
+
centre = new Map();
|
|
151
|
+
domains.forEach((d, i) => {
|
|
152
|
+
const mine = contexts.filter((k) => k === d || k.startsWith(d + " / ")), size = nodes.filter((n) => n.domain === d).length;
|
|
153
|
+
const c = { x: Math.cos(i / domains.length * 6.283) * (640 + size * 3), y: Math.sin(i / domains.length * 6.283) * (460 + size * 2) };
|
|
154
|
+
const r = mine.length > 1 ? 90 + mine.length * 16 : 0;
|
|
155
|
+
mine.forEach((k, j) => centre.set(k, { x: c.x + Math.cos(j / mine.length * 6.283) * r, y: c.y + Math.sin(j / mine.length * 6.283) * r }));
|
|
156
|
+
});
|
|
157
|
+
nodes.forEach((n, i) => { const c = centre.get(keyOf(n)) || { x: 0, y: 0 }; n.x = c.x + Math.cos(i * 2.4) * 30; n.y = c.y + Math.sin(i * 2.4) * 30; n.vx = 0; n.vy = 0; });
|
|
158
|
+
for (let step = 0; step < 400; step++) tick(1 - step / 420);
|
|
159
|
+
}
|
|
160
|
+
function tick(heat) {
|
|
161
|
+
for (let i = 0; i < nodes.length; i++) for (let j = i + 1; j < nodes.length; j++) {
|
|
162
|
+
const a = nodes[i], b = nodes[j]; let dx = a.x - b.x, dy = a.y - b.y; const d2 = Math.max(dx * dx + dy * dy, 36);
|
|
163
|
+
const f = (keyOf(a) === keyOf(b) ? 1800 : a.domain === b.domain ? 7000 : 5200) / d2, d = Math.sqrt(d2); dx /= d; dy /= d;
|
|
164
|
+
a.vx += dx * f; a.vy += dy * f; b.vx -= dx * f; b.vy -= dy * f;
|
|
165
|
+
}
|
|
166
|
+
for (const e of edges) {
|
|
167
|
+
const a = byId.get(e.from), b = byId.get(e.to); if (!a || !b) continue;
|
|
168
|
+
const rest = e.kind === "adapter" ? 30 : 60, dx = b.x - a.x, dy = b.y - a.y, d = Math.max(Math.hypot(dx, dy), 1);
|
|
169
|
+
const f = (d - rest) * (a.domain === b.domain && keyOf(a) !== keyOf(b) ? 0.004 : 0.01);
|
|
170
|
+
a.vx += dx / d * f; a.vy += dy / d * f; b.vx -= dx / d * f; b.vy -= dy / d * f;
|
|
171
|
+
}
|
|
172
|
+
for (const n of nodes) {
|
|
173
|
+
const c = centre.get(keyOf(n)) || { x: 0, y: 0 }, dx = n.x - c.x, dy = n.y - c.y, d = Math.max(Math.hypot(dx, dy), 1);
|
|
174
|
+
const pull = n.kind === "aggregate" ? 0.02 : n.kind === "port" ? 0.01 : 0.005;
|
|
175
|
+
n.vx += (c.x - n.x) * pull; n.vy += (c.y - n.y) * pull;
|
|
176
|
+
if (n.kind !== "aggregate") { const out = n.kind === "port" ? 0.9 : 1.6; n.vx += dx / d * out; n.vy += dy / d * out; }
|
|
177
|
+
n.x += n.vx * 0.5 * heat; n.y += n.vy * 0.5 * heat; n.vx *= 0.82; n.vy *= 0.82;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/* ---- boundaries: a padded convex hull around each domain's aggregates and ports ---- */
|
|
181
|
+
function hull(points) {
|
|
182
|
+
const p = points.slice().sort((a, b) => a[0] - b[0] || a[1] - b[1]); if (p.length < 3) return p;
|
|
183
|
+
const cross = (o, a, b) => (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]);
|
|
184
|
+
const lower = []; for (const q of p) { while (lower.length > 1 && cross(lower[lower.length - 2], lower[lower.length - 1], q) <= 0) lower.pop(); lower.push(q); }
|
|
185
|
+
const upper = []; for (const q of p.slice().reverse()) { while (upper.length > 1 && cross(upper[upper.length - 2], upper[upper.length - 1], q) <= 0) upper.pop(); upper.push(q); }
|
|
186
|
+
return lower.slice(0, -1).concat(upper.slice(0, -1));
|
|
187
|
+
}
|
|
188
|
+
function outline(members, pad) {
|
|
189
|
+
const cx = members.reduce((s, q) => s + q[0], 0) / members.length, cy = members.reduce((s, q) => s + q[1], 0) / members.length;
|
|
190
|
+
const ring = hull(members).map(([x, y]) => { const dx = x - cx, dy = y - cy, l = Math.max(Math.hypot(dx, dy), 1); return [x + dx / l * pad, y + dy / l * pad]; });
|
|
191
|
+
ctx.beginPath();
|
|
192
|
+
if (ring.length === 1) ctx.arc(cx, cy, pad, 0, 6.283);
|
|
193
|
+
else if (ring.length === 2) ctx.ellipse(cx, cy, Math.hypot(ring[0][0] - ring[1][0], ring[0][1] - ring[1][1]) / 2 + pad, pad, Math.atan2(ring[1][1] - ring[0][1], ring[1][0] - ring[0][0]), 0, 6.283);
|
|
194
|
+
else { const m = (i) => [(ring[i][0] + ring[(i + 1) % ring.length][0]) / 2, (ring[i][1] + ring[(i + 1) % ring.length][1]) / 2];
|
|
195
|
+
ctx.moveTo(...m(ring.length - 1)); for (let i = 0; i < ring.length; i++) ctx.quadraticCurveTo(ring[i][0], ring[i][1], ...m(i)); }
|
|
196
|
+
ctx.closePath();
|
|
197
|
+
return { cx, top: Math.min(...members.map((q) => q[1])) - pad - 4 };
|
|
198
|
+
}
|
|
199
|
+
function paint(colour, fillAlpha, strokeAlpha, dash) {
|
|
200
|
+
ctx.globalAlpha = fillAlpha; ctx.fillStyle = colour; ctx.fill();
|
|
201
|
+
ctx.globalAlpha = strokeAlpha; ctx.lineWidth = 1.5; ctx.strokeStyle = colour; ctx.setLineDash(dash); ctx.stroke(); ctx.setLineDash([]); ctx.globalAlpha = 1;
|
|
202
|
+
}
|
|
203
|
+
function caption(text, x, y, weight, active, now) {
|
|
204
|
+
ctx.font = weight + " 12px system-ui, sans-serif"; ctx.fillStyle = active ? css("--series1") : css("--ink"); ctx.textAlign = "center";
|
|
205
|
+
ctx.globalAlpha = active && !blink(now) ? 0.25 : 1; ctx.fillText(text, x, y); ctx.globalAlpha = 1; ctx.textAlign = "start";
|
|
206
|
+
}
|
|
207
|
+
function corners(b) { return [[b.x - b.w / 2, b.y - b.hgt / 2], [b.x + b.w / 2, b.y - b.hgt / 2], [b.x + b.w / 2, b.y + b.hgt / 2], [b.x - b.w / 2, b.y + b.hgt / 2]]; }
|
|
208
|
+
function drawBounds(now) {
|
|
209
|
+
const inside = (n) => n.kind !== "adapter";
|
|
210
|
+
const active = (test) => nodes.some((n) => test(n) && heatOf(n.id, now) > 0);
|
|
211
|
+
for (const d of domains) {
|
|
212
|
+
const split = contexts.filter((k) => k.startsWith(d + " / ")).length > 1; if (!split) continue;
|
|
213
|
+
const members = contexts.filter((k) => k === d || k.startsWith(d + " / ")).flatMap((k) => blocks.has(k) ? corners(blocks.get(k)) : nodes.filter((n) => keyOf(n) === k && inside(n)).map((n) => [sx(n), sy(n)]));
|
|
214
|
+
if (!members.length) continue;
|
|
215
|
+
const at = outline(members, 36); paint(colourOf(d), 0.04, 0.35, [2, 6]); caption(d, at.cx, at.top, "700", active((n) => n.domain === d), now);
|
|
216
|
+
}
|
|
217
|
+
for (const k of contexts) {
|
|
218
|
+
if (blocks.has(k)) continue;
|
|
219
|
+
const members = nodes.filter((n) => keyOf(n) === k && inside(n)).map((n) => [sx(n), sy(n)]);
|
|
220
|
+
if (!members.length) continue;
|
|
221
|
+
const d = k.split(" / ")[0], name = k.split(" / ")[1] || d;
|
|
222
|
+
const at = outline(members, 20); paint(colourOf(d), 0.09, 0.7, [6, 4]); caption(name + " −", at.cx, at.top, "600", active((n) => keyOf(n) === k), now);
|
|
223
|
+
ctx.font = "600 12px system-ui, sans-serif"; const w = ctx.measureText(name + " −").width;
|
|
224
|
+
captions.push({ k, x: at.cx - w / 2, y: at.top - 12, w, hgt: 16 });
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
let touched = false, fitScale = 1;
|
|
228
|
+
function size() {
|
|
229
|
+
const r = canvas.getBoundingClientRect(), dpr = devicePixelRatio || 1;
|
|
230
|
+
canvas.width = r.width * dpr; canvas.height = r.height * dpr; ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
231
|
+
}
|
|
232
|
+
function fit() {
|
|
233
|
+
size(); touched = false;
|
|
234
|
+
const r = canvas.getBoundingClientRect();
|
|
235
|
+
if (!nodes.length) return;
|
|
236
|
+
const xs = nodes.map((n) => n.x), ys = nodes.map((n) => n.y);
|
|
237
|
+
const w = Math.max(...xs) - Math.min(...xs) + 120, hgt = Math.max(...ys) - Math.min(...ys) + 120;
|
|
238
|
+
view.s = Math.min(r.width / w, r.height / hgt); fitScale = view.s;
|
|
239
|
+
view.x = r.width / 2 - (Math.max(...xs) + Math.min(...xs)) / 2 * view.s;
|
|
240
|
+
view.y = r.height / 2 - (Math.max(...ys) + Math.min(...ys)) / 2 * view.s;
|
|
241
|
+
}
|
|
242
|
+
/* A node's place on screen, plus the scoot: nodes within reach of the cursor ease away from it radially,
|
|
243
|
+
so a packed cluster opens under the pointer and closes when it leaves. The lines follow. */
|
|
244
|
+
const sx = (n) => n.x * view.s + view.x + (n.ox || 0), sy = (n) => n.y * view.s + view.y + (n.oy || 0);
|
|
245
|
+
const SCOOT_REACH = 90, SCOOT_PUSH = 0.6;
|
|
246
|
+
let cursor = null;
|
|
247
|
+
const base = (n) => ({ x: n.x * view.s + view.x, y: n.y * view.s + view.y });
|
|
248
|
+
function scoot() {
|
|
249
|
+
/* Nothing moves until the pointer is on a shape; then that shape stays and its neighbours make room. */
|
|
250
|
+
let pinned = null, nearest = 14;
|
|
251
|
+
if (cursor) for (const n of nodes) { if (blocks.has(keyOf(n))) continue; const b = base(n), d = Math.hypot(b.x - cursor.x, b.y - cursor.y); if (d < nearest) { nearest = d; pinned = n; } }
|
|
252
|
+
for (const n of nodes) {
|
|
253
|
+
let tx = 0, ty = 0;
|
|
254
|
+
if (pinned && n !== pinned && !blocks.has(keyOf(n)) && !reduceMotion) {
|
|
255
|
+
const b = base(n), dx = b.x - cursor.x, dy = b.y - cursor.y, d = Math.hypot(dx, dy);
|
|
256
|
+
if (d < SCOOT_REACH && d > 0.5) { const push = (SCOOT_REACH - d) * SCOOT_PUSH; tx = dx / d * push; ty = dy / d * push; }
|
|
257
|
+
}
|
|
258
|
+
n.ox = (n.ox || 0) + (tx - (n.ox || 0)) * 0.22; n.oy = (n.oy || 0) + (ty - (n.oy || 0)) * 0.22;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
const scooted = (n) => Math.hypot(n.ox || 0, n.oy || 0) > 6;
|
|
262
|
+
/* ---- zoom with the wheel around the cursor, pan by dragging, or use the buttons ---- */
|
|
263
|
+
function zoomAt(mx, my, k) { view.x = mx - (mx - view.x) * k; view.y = my - (my - view.y) * k; view.s *= k; touched = true; }
|
|
264
|
+
function zoomCentre(k) { const r = canvas.getBoundingClientRect(); zoomAt(r.width / 2, r.height / 2, k); }
|
|
265
|
+
let drag = null;
|
|
266
|
+
canvas.addEventListener("wheel", (e) => { e.preventDefault(); const r = canvas.getBoundingClientRect(); zoomAt(e.clientX - r.left, e.clientY - r.top, Math.exp(-e.deltaY * 0.0015)); }, { passive: false });
|
|
267
|
+
canvas.addEventListener("mousedown", (e) => { drag = { x: e.clientX, y: e.clientY, vx: view.x, vy: view.y, moved: false }; });
|
|
268
|
+
addEventListener("mousemove", (e) => { if (!drag) return; drag.moved = true; view.x = drag.vx + e.clientX - drag.x; view.y = drag.vy + e.clientY - drag.y; touched = true; });
|
|
269
|
+
addEventListener("mouseup", (e) => {
|
|
270
|
+
const was = drag; drag = null; if (!was || was.moved || e.target !== canvas) return;
|
|
271
|
+
const r = canvas.getBoundingClientRect(), mx = e.clientX - r.left, my = e.clientY - r.top;
|
|
272
|
+
const hit = [...blocks.values()].find((b) => Math.abs(mx - b.x) < b.w / 2 && Math.abs(my - b.y) < b.hgt / 2);
|
|
273
|
+
if (hit) { expanded.add(hit.k); return; }
|
|
274
|
+
const cap = captions.find((c) => mx >= c.x && mx <= c.x + c.w && my >= c.y && my <= c.y + c.hgt);
|
|
275
|
+
if (cap) { expanded.delete(cap.k); allOpen = false; $("all").textContent = "Expand all"; return; }
|
|
276
|
+
if (hover) openPanel(hover);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
/* ---- the sidebar: what a node declares, and what it is doing right now ---- */
|
|
280
|
+
const recent = [];
|
|
281
|
+
let selected = null, focusId = null, panelTimer = null;
|
|
282
|
+
/* The stream sends an event again when its reactions settle, so a key is kept once, at its newest. */
|
|
283
|
+
function remember(ev) {
|
|
284
|
+
const i = recent.findIndex((held) => held.key === ev.key);
|
|
285
|
+
if (i >= 0) recent[i] = ev; else { recent.push(ev); if (recent.length > 800) recent.shift(); }
|
|
286
|
+
}
|
|
287
|
+
function touches(ev, n) {
|
|
288
|
+
if (n.kind === "aggregate") return ev.aggregate === n.id || (ev.reactions || []).some((r) => String(r.command).startsWith(n.id + "."));
|
|
289
|
+
const stem = n.kind === "port" ? "::" + n.label + "." : null;
|
|
290
|
+
return stem ? (ev.reactions || []).some((r) => String(r.command).includes(stem)) : false;
|
|
291
|
+
}
|
|
292
|
+
function openPanel(n) {
|
|
293
|
+
selected = n; $("panel").hidden = false; $("ptitle").textContent = n.label; renderPanel();
|
|
294
|
+
clearInterval(panelTimer); panelTimer = setInterval(renderPanel, 2000);
|
|
295
|
+
try { history.replaceState(null, "", "#" + encodeURIComponent(n.id)); } catch (e) { /* history may be unavailable */ }
|
|
296
|
+
}
|
|
297
|
+
function closePanel() { selected = null; focusId = null; $("panel").hidden = true; $("focus").setAttribute("aria-pressed", "false"); clearInterval(panelTimer); try { history.replaceState(null, "", location.pathname); } catch (e) { /* ignore */ } }
|
|
298
|
+
function section(title, ...kids) { return [h("h3", {}, title), ...kids]; }
|
|
299
|
+
function nodeLink(id) { const n = byId.get(id); return h("button", { class: "link", type: "button", "data-id": id }, n ? n.label : id); }
|
|
300
|
+
function renderPanel() {
|
|
301
|
+
const n = selected; if (!n) return;
|
|
302
|
+
const d = n.details || {}, now = Date.now(), mine = recent.filter((ev) => touches(ev, n)), tenMin = mine.filter((ev) => now - Date.parse(ev.at) < 600000);
|
|
303
|
+
const outcomes = mine.flatMap((ev) => ev.reactions || []), refused = outcomes.filter((r) => /refus|fail/.test(r.outcome || "")).length;
|
|
304
|
+
const last = mine.map((ev) => ev.at).sort().pop();
|
|
305
|
+
const body = [
|
|
306
|
+
h("div", {}, h("span", { class: "chip", style: "border-color:" + colourOf(n.domain) + ";color:" + colourOf(n.domain) }, keyOf(n)), h("span", { class: "chip" }, n.kind)),
|
|
307
|
+
d.description ? h("p", { class: "desc" }, d.description) : null,
|
|
308
|
+
d.vision ? h("p", { class: "desc" }, h("i", {}, d.vision)) : null,
|
|
309
|
+
...section("Live", h("div", { class: "stat" },
|
|
310
|
+
h("span", {}, h("b", {}, String(tenMin.length)), "events, 10 min"),
|
|
311
|
+
h("span", {}, h("b", {}, String(outcomes.length - refused)), "delivered"),
|
|
312
|
+
h("span", {}, h("b", { style: refused ? "color:var(--critical)" : "" }, String(refused)), "refused"),
|
|
313
|
+
h("span", {}, h("b", {}, last ? clock(last) : "—"), "last seen")),
|
|
314
|
+
h("ul", {}, mine.slice(-12).reverse().map((ev) => h("li", { class: "ev" }, h("time", {}, clock(ev.at)), h("b", {}, ev.name), " ", h("small", {}, ev.aggregate.split("::").pop() + "#" + String(ev.id).slice(0, 8)),
|
|
315
|
+
(ev.reactions || []).map((r) => { const k = outcomeKind(r.outcome || ""); return h("span", { class: "re" }, h("span", { class: "mark", style: "color:var(" + OUTCOME[k][0] + ")" }, OUTCOME[k][1].slice(0, 1)), " " + String(r.policy).split("::").pop() + (r.command ? " → " + String(r.command).split("::").pop() : "")); }))))),
|
|
316
|
+
];
|
|
317
|
+
if (n.kind === "aggregate") body.push(...declaredAggregate(d));
|
|
318
|
+
if (n.kind === "port") body.push(...section("Operations", h("ul", {}, (d.operations || []).map((o) => h("li", {}, h("b", {}, o.name), " ", h("small", {}, o.direction), o.answers ? " answers " + o.answers : "", o.refuses ? ", refuses " + o.refuses : "")))));
|
|
319
|
+
if (n.kind === "adapter") body.push(...section("Binds", h("ul", {}, h("li", {}, "port ", h("b", {}, d.port || "")), (d.fields || []).length ? h("li", {}, "settings: " + d.fields.join(", ")) : null)));
|
|
320
|
+
body.push(...wiringOf(n));
|
|
321
|
+
$("pbody").replaceChildren(...body.flat().filter(Boolean));
|
|
322
|
+
}
|
|
323
|
+
function declaredAggregate(d) {
|
|
324
|
+
const out = [];
|
|
325
|
+
if (d.lifecycle) out.push(...section("Lifecycle: " + d.lifecycle.field, h("div", { class: "states" }, d.lifecycle.states.map((s) => h("span", { style: s === d.lifecycle.default ? "border-color:var(--series1)" : "" }, s)))));
|
|
326
|
+
if ((d.commands || []).length) out.push(...section("Commands", h("ul", {}, d.commands.map((c) => h("li", {}, h("b", {}, c.name), " ", c.goal, c.emits.length ? h("small", {}, " → " + c.emits.join(", ")) : null)))));
|
|
327
|
+
if ((d.queries || []).length) out.push(...section("Queries", h("ul", {}, d.queries.map((q) => h("li", {}, h("b", {}, q.name), " ", h("small", {}, q.description))))));
|
|
328
|
+
return out;
|
|
329
|
+
}
|
|
330
|
+
function wiringOf(n) {
|
|
331
|
+
const into = edges.filter((e) => e.to === n.id && e.kind !== "adapter"), out = edges.filter((e) => e.from === n.id && e.kind !== "adapter"), bound = edges.filter((e) => e.kind === "adapter" && (e.from === n.id || e.to === n.id));
|
|
332
|
+
return [
|
|
333
|
+
...(into.length ? section("Triggered by", h("ul", {}, into.map((e) => h("li", {}, nodeLink(e.from), " → ", h("b", {}, e.label))))) : []),
|
|
334
|
+
...(out.length ? section("Reacts to its events", h("ul", {}, out.map((e) => h("li", {}, h("b", {}, e.label), " → ", nodeLink(e.to), e.kind === "ask" ? h("small", {}, " (ask)") : null)))) : []),
|
|
335
|
+
...(bound.length ? section("Bound", h("ul", {}, bound.map((e) => h("li", {}, nodeLink(e.from), " ↔ ", nodeLink(e.to))))) : []),
|
|
336
|
+
];
|
|
337
|
+
}
|
|
338
|
+
$("pbody").addEventListener("click", (e) => { const id = e.target.dataset && e.target.dataset.id; if (!id) return; const n = byId.get(id); if (!n) return; expanded.add(keyOf(n)); openPanel(n); });
|
|
339
|
+
$("pclose").addEventListener("click", closePanel);
|
|
340
|
+
$("focus").addEventListener("click", () => { focusId = focusId ? null : selected && selected.id; $("focus").setAttribute("aria-pressed", String(!!focusId)); });
|
|
341
|
+
addEventListener("keydown", (e) => { if (e.key === "Escape") closePanel(); });
|
|
342
|
+
canvas.addEventListener("dblclick", (e) => { const r = canvas.getBoundingClientRect(); zoomAt(e.clientX - r.left, e.clientY - r.top, 1.6); });
|
|
343
|
+
let placed = [];
|
|
344
|
+
function free(x, y, w, hgt) {
|
|
345
|
+
if (placed.some((p) => x < p.x + p.w && x + w > p.x && y < p.y + p.hgt && y + hgt > p.y)) return false;
|
|
346
|
+
placed.push({ x, y, w, hgt }); return true;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/* ---- drawing ---- */
|
|
350
|
+
function shape(n, x, y, r) {
|
|
351
|
+
ctx.beginPath();
|
|
352
|
+
if (n.kind === "port") { ctx.moveTo(x, y - r); ctx.lineTo(x + r, y); ctx.lineTo(x, y + r); ctx.lineTo(x - r, y); ctx.closePath(); }
|
|
353
|
+
else if (n.kind === "adapter") { for (let i = 0; i < 6; i++) { const a = i / 6 * 6.283; ctx.lineTo(x + Math.cos(a) * r, y + Math.sin(a) * r); } ctx.closePath(); }
|
|
354
|
+
else ctx.roundRect(x - r, y - r * 0.7, r * 2, r * 1.4, 3);
|
|
355
|
+
}
|
|
356
|
+
/* ---- level of detail: a context is one block until it is opened by a click, "Expand all", or zooming in ---- */
|
|
357
|
+
const expanded = new Set();
|
|
358
|
+
let blocks = new Map(), captions = [], allOpen = false;
|
|
359
|
+
const open = (k) => allOpen || expanded.has(k) || view.s >= fitScale * 2.2;
|
|
360
|
+
function blockOf(k) {
|
|
361
|
+
const members = nodes.filter((n) => keyOf(n) === k && n.kind !== "adapter");
|
|
362
|
+
if (!members.length) return null;
|
|
363
|
+
const x = members.reduce((s, n) => s + sx(n), 0) / members.length, y = members.reduce((s, n) => s + sy(n), 0) / members.length;
|
|
364
|
+
const d = k.split(" / ")[0], name = k.split(" / ")[1] || d, count = members.filter((n) => n.kind === "aggregate").length;
|
|
365
|
+
ctx.font = "600 12px system-ui, sans-serif";
|
|
366
|
+
const w = Math.max(ctx.measureText(name).width + 34, 64), hgt = 30;
|
|
367
|
+
return { k, d, name, count, x, y, w, hgt, members };
|
|
368
|
+
}
|
|
369
|
+
const anchor = (n) => { const b = blocks.get(keyOf(n)); return b ? { x: b.x, y: b.y } : { x: sx(n), y: sy(n) }; };
|
|
370
|
+
/* Blocks are pixel-sized, so two neighbouring contexts can collide at the fitted scale: push them apart. */
|
|
371
|
+
function separate(list) {
|
|
372
|
+
for (let pass = 0; pass < 12; pass++) for (let i = 0; i < list.length; i++) for (let j = i + 1; j < list.length; j++) {
|
|
373
|
+
const a = list[i], b = list[j], gapX = (a.w + b.w) / 2 + 10 - Math.abs(a.x - b.x), gapY = (a.hgt + b.hgt) / 2 + 8 - Math.abs(a.y - b.y);
|
|
374
|
+
if (gapX <= 0 || gapY <= 0) continue;
|
|
375
|
+
if (gapX < gapY) { const s = Math.sign(a.x - b.x) || 1; a.x += s * gapX / 2; b.x -= s * gapX / 2; }
|
|
376
|
+
else { const s = Math.sign(a.y - b.y) || 1; a.y += s * gapY / 2; b.y -= s * gapY / 2; }
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
const blink = (now) => reduceMotion || Math.sin(now / 140) > -0.3;
|
|
380
|
+
function heatOf(id, now) { const p = pulses.get(id); return p && p > now ? Math.min(1, (p - now) / LINGER_FADE) : 0; }
|
|
381
|
+
function drawBlock(b, now) {
|
|
382
|
+
const heat = Math.max(0, ...b.members.map((n) => heatOf(n.id, now))), colour = colourOf(b.d);
|
|
383
|
+
ctx.beginPath(); ctx.roundRect(b.x - b.w / 2, b.y - b.hgt / 2, b.w, b.hgt, 8);
|
|
384
|
+
ctx.globalAlpha = 0.18 + 0.5 * heat; ctx.fillStyle = heat ? css("--series1") : colour; ctx.fill();
|
|
385
|
+
ctx.globalAlpha = 1; ctx.lineWidth = heat ? 2 : 1.2; ctx.strokeStyle = heat ? css("--series1") : colour; ctx.stroke();
|
|
386
|
+
ctx.font = "600 12px system-ui, sans-serif"; ctx.fillStyle = css("--ink"); ctx.textAlign = "center";
|
|
387
|
+
ctx.globalAlpha = heat && !blink(now) ? 0.25 : 1; ctx.fillText(b.name, b.x - 8, b.y + 4); ctx.globalAlpha = 1; ctx.textAlign = "start";
|
|
388
|
+
ctx.font = "10px system-ui, sans-serif"; ctx.fillStyle = css("--ink2"); ctx.fillText(String(b.count), b.x + b.w / 2 - 16, b.y + 4);
|
|
389
|
+
free(b.x - b.w / 2, b.y - b.hgt / 2, b.w, b.hgt);
|
|
390
|
+
}
|
|
391
|
+
function drawEdges(now) {
|
|
392
|
+
const bundles = new Map();
|
|
393
|
+
for (const e of edges) {
|
|
394
|
+
const a = byId.get(e.from), b = byId.get(e.to); if (!a || !b) continue;
|
|
395
|
+
const ca = blocks.get(keyOf(a)), cb = blocks.get(keyOf(b));
|
|
396
|
+
if (ca && ca === cb) continue;
|
|
397
|
+
if (!ca && !cb) { strokeEdge(anchor(a), anchor(b), a === b, e.lit, 1, colourOf(a.domain), now, focusId && e.from !== focusId && e.to !== focusId); continue; }
|
|
398
|
+
if (e.kind === "adapter" && cb) continue;
|
|
399
|
+
const key = (ca ? ca.k : a.id) + "|" + (cb ? cb.k : b.id), held = bundles.get(key) || { from: anchor(a), to: anchor(b), count: 0, lit: null, colour: colourOf(a.domain) };
|
|
400
|
+
held.count++; if (e.lit && e.lit.until > now && (!held.lit || e.lit.until > held.lit.until)) held.lit = e.lit;
|
|
401
|
+
bundles.set(key, held);
|
|
402
|
+
}
|
|
403
|
+
for (const b of bundles.values()) {
|
|
404
|
+
strokeEdge(b.from, b.to, false, b.lit, 1 + Math.log2(b.count), b.colour, now);
|
|
405
|
+
if (b.count > 1) { const mx = (b.from.x + b.to.x) / 2, my = (b.from.y + b.to.y) / 2; ctx.font = "10px system-ui, sans-serif";
|
|
406
|
+
if (free(mx - 8, my - 8, 16, 14)) { ctx.fillStyle = css("--ink2"); ctx.textAlign = "center"; ctx.fillText(String(b.count), mx, my + 3); ctx.textAlign = "start"; } }
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
function strokeEdge(from, to, own, litAt, width, colour, now, dim) {
|
|
410
|
+
const lit = litAt && litAt.until > now ? litAt : null, heat = lit ? Math.min(1, (lit.until - now) / LINGER_FADE) : 0;
|
|
411
|
+
ctx.globalAlpha = (dim ? 0.08 : 0.35) + 0.65 * heat; ctx.lineWidth = width + 1.5 * heat;
|
|
412
|
+
ctx.strokeStyle = lit ? css(lit.token) : (showBounds ? colour : css("--axis"));
|
|
413
|
+
ctx.beginPath();
|
|
414
|
+
if (own) ctx.arc(from.x + 9, from.y - 9, 7, 0, 6.283); else { ctx.moveTo(from.x, from.y); ctx.lineTo(to.x, to.y); }
|
|
415
|
+
ctx.stroke(); ctx.globalAlpha = 1;
|
|
416
|
+
}
|
|
417
|
+
function draw(now) {
|
|
418
|
+
const r = canvas.getBoundingClientRect(); ctx.clearRect(0, 0, r.width, r.height); placed = []; captions = [];
|
|
419
|
+
blocks = new Map(); for (const k of contexts) if (!open(k)) { const b = blockOf(k); if (b) blocks.set(k, b); }
|
|
420
|
+
scoot();
|
|
421
|
+
separate([...blocks.values()]);
|
|
422
|
+
if (showBounds) drawBounds(now);
|
|
423
|
+
drawEdges(now);
|
|
424
|
+
for (const b of blocks.values()) drawBlock(b, now);
|
|
425
|
+
for (const n of nodes) if (!blocks.has(keyOf(n))) drawNode(n, now);
|
|
426
|
+
drawParticles(now);
|
|
427
|
+
const named = view.s >= fitScale * 2.2;
|
|
428
|
+
for (const n of nodes) if (!blocks.has(keyOf(n)) && !(pulses.get(n.id) > now) && (named || scooted(n))) label(n, false, true);
|
|
429
|
+
if (hover && !blocks.has(keyOf(hover))) label(hover, true);
|
|
430
|
+
requestAnimationFrame(draw);
|
|
431
|
+
}
|
|
432
|
+
function drawNode(n, now) {
|
|
433
|
+
const x = sx(n), y = sy(n), r = n.kind === "aggregate" ? 6 : 7, pulse = pulses.get(n.id);
|
|
434
|
+
const live = pulse && pulse > now, heat = live ? Math.min(1, (pulse - now) / LINGER_FADE) : 0;
|
|
435
|
+
shape(n, x, y, r + (live && !reduceMotion ? 3 * heat : 0));
|
|
436
|
+
ctx.fillStyle = live ? css("--series1") : css("--surface"); ctx.globalAlpha = live ? 0.4 + 0.6 * heat : 1; ctx.fill(); ctx.globalAlpha = 1;
|
|
437
|
+
ctx.lineWidth = live ? 2 : 1.2; ctx.strokeStyle = live ? css("--series1") : (showBounds ? colourOf(n.domain) : css("--ink2")); ctx.stroke();
|
|
438
|
+
if (live && heat > 0.6) label(n, false);
|
|
439
|
+
}
|
|
440
|
+
function label(n, boxed, quiet) {
|
|
441
|
+
const x = sx(n) + 10, y = sy(n) - 8, text = quiet ? n.label : n.label + (n.domain ? " " + keyOf(n) : "");
|
|
442
|
+
ctx.font = (quiet ? "11px" : "12px") + " system-ui, sans-serif";
|
|
443
|
+
const w = ctx.measureText(text).width + 8;
|
|
444
|
+
if (!boxed && !free(x - 4, y - 12, w, 18)) return;
|
|
445
|
+
if (boxed) { ctx.fillStyle = css("--surface"); ctx.fillRect(x - 4, y - 12, w, 18); ctx.strokeStyle = css("--axis"); ctx.strokeRect(x - 4, y - 12, w, 18); }
|
|
446
|
+
ctx.fillStyle = quiet ? css("--ink2") : css("--ink"); ctx.fillText(text, x, y);
|
|
447
|
+
}
|
|
448
|
+
const OUTCOME = { ok: ["--good", "✓ delivered"], wait: ["--warning", "▲ waiting"], bad: ["--critical", "✕ refused"] };
|
|
449
|
+
function drawParticles(now) {
|
|
450
|
+
for (let i = particles.length - 1; i >= 0; i--) {
|
|
451
|
+
const p = particles[i], t = (now - p.start) / p.ms;
|
|
452
|
+
if (t >= 1) { pulses.set(p.to.id, now + LINGER); particles.splice(i, 1); continue; }
|
|
453
|
+
if (t < 0) continue;
|
|
454
|
+
const from = anchor(p.from), to = anchor(p.to), own = from.x === to.x && from.y === to.y, angle = t * 6.283 + 1.57;
|
|
455
|
+
const x = own ? from.x + 9 + Math.cos(angle) * 7 : from.x + (to.x - from.x) * t;
|
|
456
|
+
const y = own ? from.y - 9 + Math.sin(angle) * 7 : from.y + (to.y - from.y) * t, [token, word] = OUTCOME[p.kind];
|
|
457
|
+
ctx.beginPath(); ctx.arc(x, y, 5, 0, 6.283); ctx.fillStyle = css(token); ctx.fill(); ctx.strokeStyle = css("--surface"); ctx.lineWidth = 2; ctx.stroke();
|
|
458
|
+
ctx.font = "11px system-ui, sans-serif"; const text = word.slice(0, 1) + " " + p.text;
|
|
459
|
+
if (free(x + 8, y - 16, ctx.measureText(text).width + 4, 14)) { ctx.fillStyle = css("--ink"); ctx.fillText(text, x + 8, y - 6); }
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/* ---- messages ---- */
|
|
464
|
+
function outcomeKind(text) { return /deliver/.test(text) ? "ok" : /refus|fail/.test(text) ? "bad" : "wait"; }
|
|
465
|
+
function fire(ev, animate) {
|
|
466
|
+
const now = performance.now(), node = byId.get(ev.aggregate);
|
|
467
|
+
if (node && animate) pulses.set(node.id, now + LINGER);
|
|
468
|
+
for (const r of ev.reactions || []) {
|
|
469
|
+
const bare = String(r.policy).split("::").pop(), kind = outcomeKind(r.outcome || "");
|
|
470
|
+
const token = ev.key + "|" + bare + "|" + kind;
|
|
471
|
+
if (seen.has(token)) continue; seen.set(token, true);
|
|
472
|
+
for (const e of edges.filter((x) => x.label === bare)) {
|
|
473
|
+
const a = byId.get(e.from), b = byId.get(e.to);
|
|
474
|
+
if (a && b && animate) { particles.push({ from: a, to: b, kind, text: ev.name, start: now + 150, ms: reduceMotion ? 1 : 1400 }); e.lit = { until: now + LINGER, token: OUTCOME[kind][0] }; }
|
|
475
|
+
}
|
|
476
|
+
logLine(ev, r, kind);
|
|
477
|
+
}
|
|
478
|
+
if (!(ev.reactions || []).length) logLine(ev, null, null);
|
|
479
|
+
}
|
|
480
|
+
function logLine(ev, reaction, kind) {
|
|
481
|
+
const li = h("li", {}, h("time", {}, clock(ev.at)), (ev.aggregate || "") + " ", h("b", {}, ev.name));
|
|
482
|
+
if (reaction) li.append(" → " + String(reaction.policy).split("::").pop() + (reaction.command ? " → " + reaction.command : ""),
|
|
483
|
+
h("span", { class: "mark", style: "color:var(" + OUTCOME[kind][0] + ")" }, OUTCOME[kind][1]));
|
|
484
|
+
const log = $("log"); log.prepend(li); while (log.children.length > 60) log.lastChild.remove();
|
|
485
|
+
}
|
|
486
|
+
let backlog = [], liveStarted = false;
|
|
487
|
+
function listen() {
|
|
488
|
+
const es = new EventSource("/api/events");
|
|
489
|
+
es.onopen = () => { $("live").textContent = "stream: live"; };
|
|
490
|
+
es.onerror = () => { $("live").textContent = "stream: reconnecting"; };
|
|
491
|
+
es.addEventListener("board", (m) => {
|
|
492
|
+
const ev = JSON.parse(m.data); if (!ev.key) return;
|
|
493
|
+
remember(ev);
|
|
494
|
+
if (!liveStarted) { backlog.push(ev); clearTimeout(listen.t); listen.t = setTimeout(startLive, 600); } else fire(ev, true);
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
function startLive() { liveStarted = true; replay(backlog.slice(-12), true); }
|
|
498
|
+
function replay(list, first) {
|
|
499
|
+
if (!first) seen.clear();
|
|
500
|
+
list.forEach((ev, i) => setTimeout(() => fire(ev, true), i * 450));
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/* ---- table view, hover, boot ---- */
|
|
504
|
+
function renderTable() {
|
|
505
|
+
$("edgetable").replaceChildren(h("table", {}, h("thead", {}, h("tr", {}, ["From", "Policy", "To", "Kind"].map((t) => h("th", {}, t)))),
|
|
506
|
+
h("tbody", {}, edges.map((e) => h("tr", {}, [e.from, e.label || "(bound adapter)", e.to, e.kind].map((t) => h("td", {}, t)))))));
|
|
507
|
+
}
|
|
508
|
+
canvas.addEventListener("mousemove", (e) => {
|
|
509
|
+
const r = canvas.getBoundingClientRect(), mx = e.clientX - r.left, my = e.clientY - r.top;
|
|
510
|
+
cursor = drag ? null : { x: mx, y: my };
|
|
511
|
+
hover = nodes.find((n) => !blocks.has(keyOf(n)) && Math.hypot(sx(n) - mx, sy(n) - my) < 10) || null;
|
|
512
|
+
const onBlock = [...blocks.values()].some((b) => Math.abs(mx - b.x) < b.w / 2 && Math.abs(my - b.y) < b.hgt / 2);
|
|
513
|
+
canvas.style.cursor = drag ? "grabbing" : hover || onBlock ? "pointer" : "grab";
|
|
514
|
+
});
|
|
515
|
+
$("all").addEventListener("click", () => { allOpen = !allOpen; if (!allOpen) expanded.clear(); $("all").textContent = allOpen ? "Collapse all" : "Expand all"; });
|
|
516
|
+
$("fit").addEventListener("click", fit);
|
|
517
|
+
$("zin").addEventListener("click", () => zoomCentre(1.4));
|
|
518
|
+
$("zout").addEventListener("click", () => zoomCentre(1 / 1.4));
|
|
519
|
+
canvas.addEventListener("mouseleave", () => { hover = null; cursor = null; });
|
|
520
|
+
$("toggle").addEventListener("click", () => { const on = document.body.classList.toggle("tables-only"); $("toggle").setAttribute("aria-pressed", String(on)); $("toggle").textContent = on ? "Show diagram" : "Show table"; });
|
|
521
|
+
$("replay").addEventListener("click", () => replay(backlog.slice(-12), false));
|
|
522
|
+
$("bounds").addEventListener("click", () => { showBounds = !showBounds; $("bounds").setAttribute("aria-pressed", String(showBounds)); $("bounds").textContent = showBounds ? "Hide boundaries" : "Show boundaries"; });
|
|
523
|
+
addEventListener("resize", () => { if (touched) size(); else fit(); });
|
|
524
|
+
|
|
525
|
+
fetch("/api/wiring", { cache: "no-store" }).then((r) => r.json()).then((g) => {
|
|
526
|
+
if (g.unknown) { $("count").textContent = "unknown: " + g.unknown; return; }
|
|
527
|
+
nodes = g.nodes; edges = g.edges; byId = new Map(nodes.map((n) => [n.id, n]));
|
|
528
|
+
assignDomains();
|
|
529
|
+
$("count").textContent = domains.length + " domains, " + contexts.length + " contexts, " + nodes.filter((n) => n.kind === "aggregate").length + " aggregates, " + edges.filter((e) => e.kind !== "adapter").length + " policies";
|
|
530
|
+
$("domains").replaceChildren(...domains.map((d) => h("span", {}, h("i", { class: "swatch", style: "background:" + colourOf(d) }), d)));
|
|
531
|
+
layout(); fit(); renderTable(); requestAnimationFrame(draw); listen();
|
|
532
|
+
const wanted = decodeURIComponent(location.hash.slice(1)); if (wanted && byId.has(wanted)) { expanded.add(keyOf(byId.get(wanted))); openPanel(byId.get(wanted)); }
|
|
533
|
+
}).catch((e) => { $("count").textContent = "unknown: " + e.message; });
|
|
534
|
+
</script>
|
|
535
|
+
</body>
|
|
536
|
+
</html>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Binds the `Web` port to `PipelineWeb`, which serves the board on 127.0.0.1 until it is stopped.
|
|
2
|
+
# Required by name, not relative, so a copy of this directory shares the one implementation loaded.
|
|
3
|
+
require "hecks/pipeline_board/adapters/pipeline_web"
|
|
4
|
+
|
|
5
|
+
Hecks.adapter "PipelineWeb" do
|
|
6
|
+
port "Web"
|
|
7
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "pipeline_stages"
|
|
4
|
+
require_relative "pipeline_wiring"
|
|
5
|
+
require_relative "pipeline_web/server"
|
|
6
|
+
require_relative "pipeline_web/stream"
|
|
7
|
+
require_relative "pipeline_web/snapshot"
|
|
8
|
+
|
|
9
|
+
module Hecks
|
|
10
|
+
module Adapters
|
|
11
|
+
# The `Web` port's adapter: opens the board on 127.0.0.1 and answers `GET /`, `GET /api/state`
|
|
12
|
+
# and the `GET /api/events` stream until it is stopped. The server is the only effect the board
|
|
13
|
+
# has; every answer is read from {PipelineStages} and nothing is written.
|
|
14
|
+
class PipelineWeb
|
|
15
|
+
PAGE = File.expand_path("pipeline_web/page.html", __dir__)
|
|
16
|
+
WIRING_PAGE = File.expand_path("pipeline_web/wiring.html", __dir__)
|
|
17
|
+
SEQUENCE_PAGE = File.expand_path("pipeline_web/sequence.html", __dir__)
|
|
18
|
+
DEFAULT_PORT = 4747
|
|
19
|
+
|
|
20
|
+
# @param registry [Runtime::Registry, nil] handed to the readers
|
|
21
|
+
# @param aggregate [Object, nil] unused
|
|
22
|
+
# @param settings [Hash] `stages:` stands in for {PipelineStages}, `interval:` is the seconds
|
|
23
|
+
# between looks at the journal on a stream
|
|
24
|
+
# @param root [String, nil] the checkout the readers run in
|
|
25
|
+
def initialize(registry: nil, aggregate: nil, settings: {}, root: nil)
|
|
26
|
+
@stages = settings[:stages] || PipelineStages.new(registry: registry, root: root)
|
|
27
|
+
@wiring = settings[:wiring] || PipelineWiring.new(registry: registry)
|
|
28
|
+
@interval = settings.fetch(:interval, 1.0)
|
|
29
|
+
@snapshot = PipelineBoard::Snapshot.new(source: @stages.method(:state), background: settings.fetch(:background, true))
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Serves the board until interrupted.
|
|
33
|
+
#
|
|
34
|
+
# @param held [Hash] the `PipelineBoard` record: `port`
|
|
35
|
+
# @return [Hash{Symbol => Hash}] `last:` why the server ended
|
|
36
|
+
def listen(**held)
|
|
37
|
+
port = held[:port].is_a?(Hash) ? held[:port][:value] : held[:port]
|
|
38
|
+
server = open_at(port)
|
|
39
|
+
warn "PipelineBoard listening on http://#{server.address}:#{server.port}/ (Ctrl-C to stop)"
|
|
40
|
+
trap("INT") { server.stop }
|
|
41
|
+
server.join
|
|
42
|
+
{ last: { value: "stopped" } }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# @param port [Integer, nil] 0 binds a free port; nil the board's default
|
|
46
|
+
# @return [PipelineBoard::Server] a running server
|
|
47
|
+
def open_at(port)
|
|
48
|
+
@snapshot.start
|
|
49
|
+
PipelineBoard::Server.new(port: port || DEFAULT_PORT, app: self).start
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# @return [String] the page
|
|
53
|
+
def page = File.read(PAGE)
|
|
54
|
+
|
|
55
|
+
# @return [Hash] every section the page shows
|
|
56
|
+
def state = @snapshot.latest
|
|
57
|
+
|
|
58
|
+
# @return [String] the wiring diagram's page
|
|
59
|
+
def wiring_page = File.read(WIRING_PAGE)
|
|
60
|
+
|
|
61
|
+
# @return [String] the sequence diagram's page
|
|
62
|
+
def sequence_page = File.read(SEQUENCE_PAGE)
|
|
63
|
+
|
|
64
|
+
# @return [Hash] the wiring graph (`nodes` and `edges`), or `{unknown: reason}`
|
|
65
|
+
def wiring
|
|
66
|
+
@wiring.graph
|
|
67
|
+
rescue PipelineBoard::Unavailable => e
|
|
68
|
+
{ unknown: e.message }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Yields each event of the journal once and again when its reactions change, until the reader
|
|
72
|
+
# goes away.
|
|
73
|
+
#
|
|
74
|
+
# @yieldparam event [Hash] an event row, an `unknown` note or a heartbeat
|
|
75
|
+
def stream(&)
|
|
76
|
+
PipelineBoard::Stream.new(feed: @stages.method(:feed), interval: @interval).each(&)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|