From c35492605337f5021f36f3f81c5e374aadac3aae Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Mar 2026 15:09:43 +0000 Subject: [PATCH 1/2] Initial plan From d1f9e5fa127ddef43e0873cf565fa840792d80f1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Mar 2026 15:18:36 +0000 Subject: [PATCH 2/2] Add per-node first-seen timestamp via NodeBlock model - Create node_blocks migration and model (node_id, block_id, first_seen_at) - Add has_many :node_blocks to Node and Block models - Populate NodeBlock in Block.create_or_update_with and create_headers_only - Add block_first_seen_at to Node#as_json - Add includes(node: :node_blocks) in Chaintip#nodes_for_identical_chaintips - Remove global First seen from blockInfo.jsx - Add per-node first-seen with colour coding in node.jsx Assisted-by: GitHub Copilot Assisted-by: OpenAI GPT-5-Codex Co-authored-by: jonathanbier <42411042+jonathanbier@users.noreply.github.com> --- .../forkMonitorApp/components/blockInfo.jsx | 2 -- .../packs/forkMonitorApp/components/node.jsx | 22 +++++++++++++++++++ app/models/block.rb | 7 ++++++ app/models/chaintip.rb | 4 +++- app/models/node.rb | 4 +++- app/models/node_block.rb | 6 +++++ .../20260305151011_create_node_blocks.rb | 15 +++++++++++++ 7 files changed, 56 insertions(+), 4 deletions(-) create mode 100644 app/models/node_block.rb create mode 100644 db/migrate/20260305151011_create_node_blocks.rb diff --git a/app/javascript/packs/forkMonitorApp/components/blockInfo.jsx b/app/javascript/packs/forkMonitorApp/components/blockInfo.jsx index 087e3c1a..18192a9f 100644 --- a/app/javascript/packs/forkMonitorApp/components/blockInfo.jsx +++ b/app/javascript/packs/forkMonitorApp/components/blockInfo.jsx @@ -18,8 +18,6 @@ class BlockInfo extends React.Component {
Miner timestamp: {this.props.block.timestamp} UTC
- First seen: {this.props.block.created_at} UTC -
{ this.props.block.pool && Mined by: { this.props.block.pool } diff --git a/app/javascript/packs/forkMonitorApp/components/node.jsx b/app/javascript/packs/forkMonitorApp/components/node.jsx index 4e8b0ee3..f27b4434 100644 --- a/app/javascript/packs/forkMonitorApp/components/node.jsx +++ b/app/javascript/packs/forkMonitorApp/components/node.jsx @@ -3,6 +3,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import Moment from 'react-moment'; +import 'moment-timezone'; import { BreadcrumbItem, @@ -33,6 +34,27 @@ class Node extends React.Component { + { this.props.chaintip && this.props.chaintip.block && this.props.node.block_first_seen_at && + (() => { + const times = this.props.chaintip.nodes + .map(n => n.block_first_seen_at) + .filter(Boolean) + .map(t => new Date(t).getTime()); + const minTime = Math.min(...times); + const maxTime = Math.max(...times); + const myTime = new Date(this.props.node.block_first_seen_at).getTime(); + let color = 'black'; + if (times.length > 1) { + if (myTime === minTime) color = 'green'; + else if (myTime === maxTime) color = 'red'; + } + return ( + + First seen: { this.props.node.block_first_seen_at } UTC + + ); + })() + } { this.props.node.has_mirror_node && { where(status: 'active') }, class_name: 'Chaintip' + has_many :node_blocks, dependent: :destroy has_many :softforks, dependent: :destroy scope :bitcoin_core_by_version, lambda { @@ -74,7 +75,8 @@ def as_json(options = nil) last_tx_outset: tx_outsets.last, has_mirror_node: mirror_rpchost.present?, bip9_softforks: softforks.where(fork_type: :bip9), # rubocop:disable Naming/VariableNumber - bip8_softforks: softforks.where(fork_type: :bip8) # rubocop:disable Naming/VariableNumber + bip8_softforks: softforks.where(fork_type: :bip8), # rubocop:disable Naming/VariableNumber + block_first_seen_at: active_chaintip&.block&.node_blocks&.find_by(node: self)&.first_seen_at }) end diff --git a/app/models/node_block.rb b/app/models/node_block.rb new file mode 100644 index 00000000..0060db6c --- /dev/null +++ b/app/models/node_block.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +class NodeBlock < ApplicationRecord + belongs_to :node + belongs_to :block +end diff --git a/db/migrate/20260305151011_create_node_blocks.rb b/db/migrate/20260305151011_create_node_blocks.rb new file mode 100644 index 00000000..b7afd504 --- /dev/null +++ b/db/migrate/20260305151011_create_node_blocks.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class CreateNodeBlocks < ActiveRecord::Migration[6.1] + def change + create_table :node_blocks do |t| + t.references :node, null: false, foreign_key: true + t.references :block, null: false, foreign_key: true + t.datetime :first_seen_at, null: false + + t.timestamps + end + + add_index :node_blocks, %i[node_id block_id], unique: true + end +end