source_monitor 0.9.0 → 0.9.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 37c1b9f849296d336eef1ced8584d8cad21caadb8c67606332ebd0f5db8b6fea
4
- data.tar.gz: 36b8646b5f89a6b2b4317b9c914268b9ab399cd7b8cb4cb3aa9ee38809d7fb85
3
+ metadata.gz: c3dd7577c86e15ec9926a631998d423b1d2fd1bc18cbdfc83e8d7dc57b6be365
4
+ data.tar.gz: 65fc2870418c04d3741a98404558ffdd3e8f5a901294681446f337b662dd50f2
5
5
  SHA512:
6
- metadata.gz: d05d79d697e3d2889e0c4e48f8559601c8f43787d94db7d70e517badd606e2ced0252015b3d95bb3721b1acd3ef0bc1096c280c704c602f2a16269160322ef02
7
- data.tar.gz: 607355cdf142b9fd7587004c570ece825ac30e1f40864b39fef934e347cf13c22ff09bda06b0f5bf8dd6e1984b3bcc4644d3667e22890c0d6d9bfe5fa28fcf09
6
+ metadata.gz: 115775737ef8f40ea9323932d58e941fccb9b6903371cbdab80d62bcdfbf31d85f44c5eacbcc7b536489483972a2b02e7e547454715dae9af8b19083dce62a62
7
+ data.tar.gz: 2a285b946a069420c28a1588f580f721f0ea23da6b5c002a0dfb8d149c338c120291e4ad8e77609b65f59ae55bfadec7db6b85c49f92d2097616588d9425c363
@@ -155,7 +155,7 @@ Items use soft delete via `deleted_at` column (NOT default_scope):
155
155
  | `url` | presence |
156
156
 
157
157
  ### Content Delegation
158
- `scraped_html` and `scraped_content` delegate to `ItemContent`. Setting these values auto-creates/destroys the ItemContent association.
158
+ `scraped_html` and `scraped_content` delegate to `ItemContent`. Setting these values auto-creates the ItemContent association. `ensure_feed_content_record` also creates ItemContent for feed word count tracking. ItemContent is only auto-destroyed when both scraped fields are blank AND the item has no feed content.
159
159
 
160
160
  ## LogEntry Delegated Type
161
161
 
@@ -30,8 +30,8 @@
30
30
  v v
31
31
  ItemContent ScrapeLog
32
32
  =========== (also belongs_to :source)
33
- Scraped
34
- content
33
+ Feed + scraped
34
+ word counts
35
35
  ```
36
36
 
37
37
  ## Association Details
@@ -43,10 +43,11 @@
43
43
  - Counter cache: `items_count` on Source (tracks active items)
44
44
 
45
45
  ### Item -> ItemContent
46
- - `has_one :item_content` -- Lazy-created when scraped content is assigned
46
+ - `has_one :item_content` -- Created when item has feed content (via `ensure_feed_content_record`) or when scraped content is assigned
47
47
  - `dependent: :destroy, autosave: true`
48
48
  - `touch: true` on the belongs_to side
49
- - Content is auto-destroyed when both `scraped_html` and `scraped_content` become blank
49
+ - Stores `feed_word_count` (from `item.content`) and `scraped_word_count` (from `scraped_content`)
50
+ - Content is auto-destroyed only when both scraped fields are blank AND item has no feed content
50
51
 
51
52
  ### Source -> FetchLog
52
53
  - `has_many :fetch_logs, dependent: :destroy`
data/CHANGELOG.md CHANGED
@@ -15,6 +15,14 @@ All notable changes to this project are documented below. The format follows [Ke
15
15
 
16
16
  - No unreleased changes yet.
17
17
 
18
+ ## [0.9.1] - 2026-02-22
19
+
20
+ ### Fixed
21
+
22
+ - **Feed word counts now always computed.** Items fetched from feeds but never scraped now get an `ItemContent` record with `feed_word_count` automatically. Previously, only scraped items had `ItemContent`, so the `backfill_word_counts` rake task found nothing for feed-only items.
23
+ - **Backfill task creates missing ItemContent records.** `source_monitor:backfill_word_counts` now has a two-phase approach: first creates `ItemContent` for items with feed content but no record, then recomputes all word counts.
24
+ - **ItemContent preserved when item has feed content.** Clearing scraped fields no longer destroys the `ItemContent` record if the item still has feed content (which provides `feed_word_count`).
25
+
18
26
  ## [0.9.0] - 2026-02-22
19
27
 
20
28
  ### Added
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- source_monitor (0.9.0)
4
+ source_monitor (0.9.1)
5
5
  cssbundling-rails (~> 1.4)
6
6
  faraday (~> 2.9)
7
7
  faraday-follow_redirects (~> 0.4)
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.9.0
1
+ 0.9.1
@@ -671,10 +671,6 @@ video {
671
671
  top: 50%;
672
672
  }
673
673
 
674
- .fm-admin .top-4 {
675
- top: 1rem;
676
- }
677
-
678
674
  .fm-admin .top-16 {
679
675
  top: 4rem;
680
676
  }
@@ -701,6 +697,14 @@ video {
701
697
  margin-bottom: 0.25rem;
702
698
  }
703
699
 
700
+ .fm-admin .mb-1 {
701
+ margin-bottom: 0.25rem;
702
+ }
703
+
704
+ .fm-admin .ml-0\.5 {
705
+ margin-left: 0.125rem;
706
+ }
707
+
704
708
  .fm-admin .ml-2 {
705
709
  margin-left: 0.5rem;
706
710
  }
@@ -745,14 +749,6 @@ video {
745
749
  margin-top: 1.5rem;
746
750
  }
747
751
 
748
- .fm-admin .mb-1 {
749
- margin-bottom: 0.25rem;
750
- }
751
-
752
- .fm-admin .ml-0\.5 {
753
- margin-left: 0.125rem;
754
- }
755
-
756
752
  .fm-admin .block {
757
753
  display: block;
758
754
  }
@@ -861,10 +857,6 @@ video {
861
857
  width: 20rem;
862
858
  }
863
859
 
864
- .fm-admin .w-full {
865
- width: 100%;
866
- }
867
-
868
860
  .fm-admin .w-\[15\%\] {
869
861
  width: 15%;
870
862
  }
@@ -881,14 +873,18 @@ video {
881
873
  width: 45%;
882
874
  }
883
875
 
884
- .fm-admin .min-w-full {
885
- min-width: 100%;
876
+ .fm-admin .w-full {
877
+ width: 100%;
886
878
  }
887
879
 
888
880
  .fm-admin .min-w-\[12rem\] {
889
881
  min-width: 12rem;
890
882
  }
891
883
 
884
+ .fm-admin .min-w-full {
885
+ min-width: 100%;
886
+ }
887
+
892
888
  .fm-admin .max-w-2xl {
893
889
  max-width: 42rem;
894
890
  }
@@ -1599,6 +1595,10 @@ video {
1599
1595
  line-height: 1rem;
1600
1596
  }
1601
1597
 
1598
+ .fm-admin .font-bold {
1599
+ font-weight: 700;
1600
+ }
1601
+
1602
1602
  .fm-admin .font-medium {
1603
1603
  font-weight: 500;
1604
1604
  }
@@ -1611,10 +1611,6 @@ video {
1611
1611
  font-weight: 600;
1612
1612
  }
1613
1613
 
1614
- .fm-admin .font-bold {
1615
- font-weight: 700;
1616
- }
1617
-
1618
1614
  .fm-admin .uppercase {
1619
1615
  text-transform: uppercase;
1620
1616
  }
@@ -1938,6 +1934,11 @@ video {
1938
1934
  color: rgb(37 99 235 / var(--tw-text-opacity, 1));
1939
1935
  }
1940
1936
 
1937
+ .fm-admin .hover\:text-blue-700:hover {
1938
+ --tw-text-opacity: 1;
1939
+ color: rgb(29 78 216 / var(--tw-text-opacity, 1));
1940
+ }
1941
+
1941
1942
  .fm-admin .hover\:text-rose-500:hover {
1942
1943
  --tw-text-opacity: 1;
1943
1944
  color: rgb(244 63 94 / var(--tw-text-opacity, 1));
@@ -1963,11 +1964,6 @@ video {
1963
1964
  color: rgb(255 255 255 / var(--tw-text-opacity, 1));
1964
1965
  }
1965
1966
 
1966
- .fm-admin .hover\:text-blue-700:hover {
1967
- --tw-text-opacity: 1;
1968
- color: rgb(29 78 216 / var(--tw-text-opacity, 1));
1969
- }
1970
-
1971
1967
  .fm-admin .hover\:underline:hover {
1972
1968
  text-decoration-line: underline;
1973
1969
  }
@@ -2071,10 +2067,6 @@ video {
2071
2067
  width: auto;
2072
2068
  }
2073
2069
 
2074
- .fm-admin .sm\:min-w-\[16rem\] {
2075
- min-width: 16rem;
2076
- }
2077
-
2078
2070
  .fm-admin .sm\:grid-cols-2 {
2079
2071
  grid-template-columns: repeat(2, minmax(0, 1fr));
2080
2072
  }
@@ -33,6 +33,15 @@ module SourceMonitor
33
33
 
34
34
  SourceMonitor::ModelExtensions.register(self, :item)
35
35
 
36
+ # Creates an ItemContent record for feed word count computation when one doesn't exist.
37
+ # The before_save callback on ItemContent will compute feed_word_count from item.content.
38
+ def ensure_feed_content_record
39
+ return if item_content.present?
40
+ return if content.blank?
41
+
42
+ create_item_content!
43
+ end
44
+
36
45
  class << self
37
46
  def ransackable_attributes(_auth_object = nil)
38
47
  %w[title summary url published_at created_at scrape_status]
@@ -91,6 +100,7 @@ module SourceMonitor
91
100
  def cleanup_item_content_if_blank
92
101
  return unless item_content
93
102
  return if item_content.scraped_html.present? || item_content.scraped_content.present?
103
+ return if content.present?
94
104
 
95
105
  if item_content.persisted?
96
106
  item_content.mark_for_destruction
@@ -120,6 +120,7 @@ module SourceMonitor
120
120
  new_item = source.items.new
121
121
  apply_attributes(new_item, attributes)
122
122
  new_item.save!
123
+ new_item.ensure_feed_content_record
123
124
  Result.new(item: new_item, status: :created)
124
125
  rescue ActiveRecord::RecordNotUnique
125
126
  handle_concurrent_duplicate(attributes, raw_guid_present:)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SourceMonitor
4
- VERSION = "0.9.0"
4
+ VERSION = "0.9.1"
5
5
  end
@@ -3,6 +3,20 @@
3
3
  namespace :source_monitor do
4
4
  desc "Backfill word counts for existing item_content records."
5
5
  task backfill_word_counts: :environment do
6
+ # Phase 1: Create ItemContent for items with feed content but no ItemContent
7
+ items_needing_content = SourceMonitor::Item
8
+ .where.not(content: [ nil, "" ])
9
+ .where.missing(:item_content)
10
+
11
+ created = 0
12
+ items_needing_content.find_each do |item|
13
+ item.ensure_feed_content_record
14
+ created += 1
15
+ puts "Created #{created} missing ItemContent records..." if (created % 100).zero?
16
+ end
17
+ puts "Created #{created} ItemContent records for feed-only items." if created > 0
18
+
19
+ # Phase 2: Recompute word counts for all existing ItemContent
6
20
  total = SourceMonitor::ItemContent.count
7
21
  processed = 0
8
22
 
@@ -12,7 +26,7 @@ namespace :source_monitor do
12
26
  puts "Processed #{processed}/#{total} records..." if (processed % 100).zero?
13
27
  end
14
28
 
15
- puts "Done. Backfilled word counts for #{processed} records."
29
+ puts "Done. Backfilled word counts for #{processed} records (#{created} newly created)."
16
30
  end
17
31
 
18
32
  namespace :cleanup do
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: source_monitor
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.0
4
+ version: 0.9.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - dchuk