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 +4 -4
- data/.claude/skills/sm-domain-model/SKILL.md +1 -1
- data/.claude/skills/sm-domain-model/reference/model-graph.md +5 -4
- data/CHANGELOG.md +8 -0
- data/Gemfile.lock +1 -1
- data/VERSION +1 -1
- data/app/assets/builds/source_monitor/application.css +23 -31
- data/app/models/source_monitor/item.rb +10 -0
- data/lib/source_monitor/items/item_creator.rb +1 -0
- data/lib/source_monitor/version.rb +1 -1
- data/lib/tasks/source_monitor_tasks.rake +15 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c3dd7577c86e15ec9926a631998d423b1d2fd1bc18cbdfc83e8d7dc57b6be365
|
|
4
|
+
data.tar.gz: 65fc2870418c04d3741a98404558ffdd3e8f5a901294681446f337b662dd50f2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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
|
-
|
|
34
|
-
|
|
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` --
|
|
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
|
-
-
|
|
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
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.9.
|
|
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 .
|
|
885
|
-
|
|
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:)
|
|
@@ -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
|