.vbw-planning/milestones/ui-fixes-and-smart-scraping/phases/01-ui-polish-and-bug-fixes/04-PLAN.md
Define three ransacker blocks on the Source model for the computed columns.
Files:
app/models/source_monitor/source.rb — add ransacker definitionsDetails:
ransacker :new_items_per_day — subquery counting items created in last 30 days divided by 30ransacker :avg_feed_words — subquery averaging feed_word_count from item_contentsransacker :avg_scraped_words — subquery averaging scraped_word_count from item_contentsransacker :avg_feed_words do
Arel.sql("(SELECT AVG(ic.feed_word_count) FROM #{ItemContent.table_name} ic INNER JOIN #{Item.table_name} i ON i.id = ic.item_id WHERE i.source_id = #{table_name}.id AND ic.feed_word_count IS NOT NULL)")
end
Replace the plain <th> headers for the three columns with the table_sort_link pattern.
Files:
app/views/source_monitor/sources/index.html.erb — lines 171-173Details:
<th> with the same structure used by Items/Last Fetch columns:
<th scope="col" class="px-6 py-3" data-sort-column="avg_feed_words" aria-sort="<%= table_sort_aria(@q, :avg_feed_words) %>">
<span class="inline-flex items-center gap-1">
<%= table_sort_link(@q, :avg_feed_words, "Avg Feed Words", frame: "source_monitor_sources_table", default_order: :desc, secondary: ["created_at desc"], html_options: { class: "inline-flex items-center gap-1 text-xs font-semibold uppercase tracking-wide text-slate-600 hover:text-slate-900 focus:outline-none" }) %>
<span class="text-[11px] text-slate-400" aria-hidden="true"><%= table_sort_arrow(@q, :avg_feed_words) %></span>
</span>
</th>
new_items_per_day and avg_scraped_wordsWrite integration tests verifying the sort links work.
Files:
test/controllers/source_monitor/sources_controller_sort_test.rbAcceptance: