.vbw-planning/milestones/ui-fixes-and-smart-scraping/phases/01-ui-polish-and-bug-fixes/01-RESEARCH.md
Key files:
app/models/source_monitor/import_history.rb — model with user_id, imported_sources, failed_sources, skipped_duplicates, started_at, completed_atapp/views/source_monitor/sources/_import_history_panel.html.erb — renders banner on sources index showing latest import statsapp/controllers/source_monitor/sources_controller.rb:45 — loads @recent_import_histories via ImportHistory.recent_for(user_id).limit(5)db/migrate/20251125094500_create_import_histories.rb — schema: user_id, jsonb columns, timestamps. No dismissed_at column yet.config/routes.rb — no dedicated route for import_histories dismissalCurrent behavior: Banner always shows the latest import. No dismiss mechanism exists. The panel is rendered inside sources/index.html.erb line 44.
What's needed: Migration to add dismissed_at to import_histories, a route/endpoint to PATCH dismiss, Turbo Stream to remove the panel element #source_monitor_import_history_panel.
Key files:
lib/source_monitor/favicons/discoverer.rb — downloads favicons, checks allowed_content_typeslib/source_monitor/configuration/favicons_settings.rb — DEFAULT_ALLOWED_CONTENT_TYPES includes image/svg+xmlapp/helpers/source_monitor/application_helper.rb:242-323 — source_favicon_tag renders favicon or placeholderapp/helpers/source_monitor/application_helper.rb:297-308 — favicon_image_tag uses rails_blob_path directly (no variant processing)app/jobs/source_monitor/favicon_fetch_job.rb — async favicon fetchinglib/source_monitor/fetching/feed_fetcher/source_updater.rb — triggers favicon fetch during feed processingCurrent behavior: SVG favicons are downloaded and stored as-is via Active Storage. They're rendered as `` tags. SVGs may not render correctly as small favicons and pose XSS risk if rendered inline.
What's needed: After downloading, detect SVG content type and convert to PNG using MiniMagick before attaching to Active Storage. The Discoverer or a post-processing step should handle conversion.
Key files:
app/views/source_monitor/dashboard/_recent_activity.html.erb — renders event listlib/source_monitor/dashboard/recent_activity_presenter.rb — builds view models for eventsCurrent heading structure (fetch events):
event[:label] = "Fetch #2210" (linked, bold)event[:type].to_s.humanize = "Fetch"event[:description] = "3 created / 0 updated"event[:url_display] shown below as small gray text (domain only)Presenter fetch_event (line 32-43):
label: "Fetch ##{event.id}"url_display: domain (extracted from feed_url)url_href: event.source_feed_urlWhat's needed per decision: URL should lead the heading row: "fhur.me -- Fetch #2210 FETCH". Source name line removed. Change the presenter to put domain in label, or restructure the view to lead with URL.
Key files:
app/views/source_monitor/sources/index.html.erb:110-194 — table with sortable Name, Fetch Interval, Items, Last Fetch columnsapp/helpers/source_monitor/table_sort_helper.rb — table_sort_link, table_sort_arrow, table_sort_aria helpersapp/controllers/concerns/source_monitor/sanitizes_search_params.rb — searchable_with, build_search_query using Ransackapp/controllers/source_monitor/sources_controller.rb:12 — searchable_with scope: -> { Source.all }, default_sorts: ["created_at desc"]Existing sort pattern (e.g., Items column, lines 152-170):
<th scope="col" class="px-6 py-3" data-sort-column="items_count" aria-sort="<%= table_sort_aria(@q, :items_count) %>">
<span class="inline-flex items-center gap-1">
<%= table_sort_link(@q, :items_count, "Items", frame: "source_monitor_sources_table", default_order: :desc, secondary: ["created_at desc"], html_options: { class: "..." }) %>
<span class="text-[11px] text-slate-400" aria-hidden="true"><%= table_sort_arrow(@q, :items_count) %></span>
</span>
</th>
Non-sortable columns (lines 171-173):
<th scope="col" class="px-6 py-3">New Items / Day</th>
<th scope="col" class="px-6 py-3">Avg Feed Words</th>
<th scope="col" class="px-6 py-3">Avg Scraped Words</th>
Challenge: These columns are computed values (aggregates from ItemContent), not direct Source attributes. Ransack sorts on model attributes. Options:
ransacker to define custom sort columnsThe existing data is computed in the controller (lines 52-64) using ItemContent.joins(:item).group(...).average(...). For Ransack sorting, ransacker definitions would allow sorting by subquery.
SourcesController#destroy via TurboStreams::StreamResponder — removes DOM elements and shows toastSanitizesSearchParams concern with searchable_with + build_search_query. Uses sort_link from Ransack gem through table_sort_link helperhas_one_attached :favicon (guarded with if defined?(ActiveStorage))#source_monitor_import_history_panel needs a unique target. Currently has this ID, so Turbo Stream removal should work cleanly.dismissed_at. Create ImportHistoryDismissals controller (REST: POST to mark dismissed). Use turbo_stream.remove targeting the panel div._recent_activity.html.erb to show "domain -- Fetch #N" as the main label. Move the domain from url_display into the heading.ransacker on Source model for the three computed columns. Each ransacker defines a subquery. Follow the exact same table_sort_link pattern as Items/Last Fetch.