source_monitor 0.7.0 → 0.8.0
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/commands/release.md +45 -22
- data/.claude/skills/sm-configure/SKILL.md +10 -1
- data/.claude/skills/sm-configure/reference/configuration-reference.md +44 -0
- data/.claude/skills/sm-host-setup/reference/initializer-template.md +17 -0
- data/.claude/skills/sm-host-setup/reference/setup-checklist.md +2 -0
- data/.claude/skills/sm-job/reference/job-conventions.md +26 -0
- data/.claude/skills/sm-upgrade/reference/version-history.md +22 -0
- data/.gitignore +10 -0
- data/AGENTS.md +1 -1
- data/CHANGELOG.md +56 -0
- data/CLAUDE.md +11 -5
- data/Gemfile.lock +1 -1
- data/README.md +6 -4
- data/VERSION +1 -1
- data/app/assets/builds/source_monitor/application.css +43 -0
- data/app/assets/builds/source_monitor/application.js +127 -0
- data/app/assets/builds/source_monitor/application.js.map +3 -3
- data/app/assets/javascripts/source_monitor/application.js +2 -0
- data/app/assets/javascripts/source_monitor/controllers/notification_container_controller.js +138 -0
- data/app/assets/javascripts/source_monitor/controllers/notification_controller.js +11 -0
- data/app/controllers/source_monitor/source_favicon_fetches_controller.rb +38 -0
- data/app/controllers/source_monitor/sources_controller.rb +11 -0
- data/app/helpers/source_monitor/application_helper.rb +51 -0
- data/app/jobs/source_monitor/favicon_fetch_job.rb +71 -0
- data/app/jobs/source_monitor/import_opml_job.rb +9 -0
- data/app/jobs/source_monitor/source_health_check_job.rb +10 -0
- data/app/models/source_monitor/source.rb +2 -0
- data/app/views/layouts/source_monitor/application.html.erb +23 -2
- data/app/views/source_monitor/shared/_toast.html.erb +1 -0
- data/app/views/source_monitor/sources/_details.html.erb +34 -5
- data/app/views/source_monitor/sources/_row.html.erb +11 -6
- data/config/routes.rb +1 -0
- data/docs/configuration.md +1 -1
- data/docs/upgrade.md +22 -0
- data/lib/generators/source_monitor/install/templates/source_monitor.rb.tt +15 -1
- data/lib/source_monitor/configuration/favicons_settings.rb +42 -0
- data/lib/source_monitor/configuration/http_settings.rb +1 -1
- data/lib/source_monitor/configuration/scraping_settings.rb +1 -1
- data/lib/source_monitor/configuration.rb +3 -1
- data/lib/source_monitor/favicons/discoverer.rb +196 -0
- data/lib/source_monitor/fetching/feed_fetcher/source_updater.rb +21 -0
- data/lib/source_monitor/fetching/feed_fetcher.rb +1 -0
- data/lib/source_monitor/http.rb +5 -3
- data/lib/source_monitor/version.rb +1 -1
- data/lib/source_monitor.rb +4 -0
- data/lib/tasks/test_fast.rake +11 -0
- data/source_monitor.gemspec +1 -1
- metadata +7 -93
- data/.vbw-planning/PROJECT.md +0 -51
- data/.vbw-planning/ROADMAP.md +0 -32
- data/.vbw-planning/SHIPPED.md +0 -63
- data/.vbw-planning/STATE.md +0 -27
- data/.vbw-planning/codebase/ARCHITECTURE.md +0 -147
- data/.vbw-planning/codebase/CONCERNS.md +0 -99
- data/.vbw-planning/codebase/CONVENTIONS.md +0 -97
- data/.vbw-planning/codebase/DEPENDENCIES.md +0 -100
- data/.vbw-planning/codebase/INDEX.md +0 -86
- data/.vbw-planning/codebase/META.md +0 -42
- data/.vbw-planning/codebase/PATTERNS.md +0 -262
- data/.vbw-planning/codebase/STACK.md +0 -101
- data/.vbw-planning/codebase/STRUCTURE.md +0 -324
- data/.vbw-planning/codebase/TESTING.md +0 -154
- data/.vbw-planning/config.json +0 -53
- data/.vbw-planning/discovery.json +0 -26
- data/.vbw-planning/milestones/default/ROADMAP.md +0 -115
- data/.vbw-planning/milestones/default/STATE.md +0 -82
- data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-01-SUMMARY.md +0 -56
- data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-01.md +0 -187
- data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-02-SUMMARY.md +0 -64
- data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-02.md +0 -137
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-01-SUMMARY.md +0 -67
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-01.md +0 -142
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-02-SUMMARY.md +0 -64
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-02.md +0 -138
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-03-SUMMARY.md +0 -85
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-03.md +0 -147
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-04-SUMMARY.md +0 -63
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-04.md +0 -129
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-05-SUMMARY.md +0 -74
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-05.md +0 -154
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/03-VERIFICATION-wave1.md +0 -303
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/03-VERIFICATION.md +0 -510
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-01-SUMMARY.md +0 -61
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-01.md +0 -161
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-02-SUMMARY.md +0 -66
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-02.md +0 -132
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-03-SUMMARY.md +0 -59
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-03.md +0 -171
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-04-SUMMARY.md +0 -56
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-04.md +0 -152
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/04-CONTEXT.md +0 -33
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-01-SUMMARY.md +0 -42
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-01.md +0 -119
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-02-SUMMARY.md +0 -52
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-02.md +0 -195
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-03-SUMMARY.md +0 -79
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-03.md +0 -130
- data/.vbw-planning/milestones/generator-enhancements/REQUIREMENTS.md +0 -72
- data/.vbw-planning/milestones/generator-enhancements/ROADMAP.md +0 -125
- data/.vbw-planning/milestones/generator-enhancements/SHIPPED.md +0 -40
- data/.vbw-planning/milestones/generator-enhancements/STATE.md +0 -43
- data/.vbw-planning/milestones/generator-enhancements/phases/01-generator-steps/01-CONTEXT.md +0 -33
- data/.vbw-planning/milestones/generator-enhancements/phases/01-generator-steps/01-VERIFICATION.md +0 -86
- data/.vbw-planning/milestones/generator-enhancements/phases/01-generator-steps/PLAN-01-SUMMARY.md +0 -61
- data/.vbw-planning/milestones/generator-enhancements/phases/01-generator-steps/PLAN-01.md +0 -380
- data/.vbw-planning/milestones/generator-enhancements/phases/02-verification/02-VERIFICATION.md +0 -78
- data/.vbw-planning/milestones/generator-enhancements/phases/02-verification/PLAN-01-SUMMARY.md +0 -46
- data/.vbw-planning/milestones/generator-enhancements/phases/02-verification/PLAN-01.md +0 -500
- data/.vbw-planning/milestones/generator-enhancements/phases/03-docs-alignment/03-VERIFICATION.md +0 -89
- data/.vbw-planning/milestones/generator-enhancements/phases/03-docs-alignment/PLAN-01-SUMMARY.md +0 -48
- data/.vbw-planning/milestones/generator-enhancements/phases/03-docs-alignment/PLAN-01.md +0 -456
- data/.vbw-planning/milestones/generator-enhancements/phases/04-dashboard-ux/04-VERIFICATION.md +0 -129
- data/.vbw-planning/milestones/generator-enhancements/phases/04-dashboard-ux/PLAN-01-SUMMARY.md +0 -70
- data/.vbw-planning/milestones/generator-enhancements/phases/04-dashboard-ux/PLAN-01.md +0 -747
- data/.vbw-planning/milestones/generator-enhancements/phases/05-active-storage-images/05-VERIFICATION.md +0 -156
- data/.vbw-planning/milestones/generator-enhancements/phases/05-active-storage-images/PLAN-01-SUMMARY.md +0 -69
- data/.vbw-planning/milestones/generator-enhancements/phases/05-active-storage-images/PLAN-01.md +0 -455
- data/.vbw-planning/milestones/generator-enhancements/phases/05-active-storage-images/PLAN-02-SUMMARY.md +0 -39
- data/.vbw-planning/milestones/generator-enhancements/phases/05-active-storage-images/PLAN-02.md +0 -488
- data/.vbw-planning/milestones/generator-enhancements/phases/06-netflix-feed-fix/06-VERIFICATION.md +0 -100
- data/.vbw-planning/milestones/generator-enhancements/phases/06-netflix-feed-fix/PLAN-01-SUMMARY.md +0 -37
- data/.vbw-planning/milestones/generator-enhancements/phases/06-netflix-feed-fix/PLAN-01.md +0 -345
- data/.vbw-planning/milestones/upgrade-assurance/REQUIREMENTS.md +0 -80
- data/.vbw-planning/milestones/upgrade-assurance/ROADMAP.md +0 -75
- data/.vbw-planning/milestones/upgrade-assurance/STATE.md +0 -29
- data/.vbw-planning/milestones/upgrade-assurance/phases/01-upgrade-command/01-VERIFICATION.md +0 -144
- data/.vbw-planning/milestones/upgrade-assurance/phases/01-upgrade-command/PLAN-01-SUMMARY.md +0 -43
- data/.vbw-planning/milestones/upgrade-assurance/phases/01-upgrade-command/PLAN-01.md +0 -405
- data/.vbw-planning/milestones/upgrade-assurance/phases/02-config-deprecation/PLAN-01-SUMMARY.md +0 -27
- data/.vbw-planning/milestones/upgrade-assurance/phases/02-config-deprecation/PLAN-01.md +0 -303
- data/.vbw-planning/milestones/upgrade-assurance/phases/03-upgrade-skill-docs/03-VERIFICATION.md +0 -380
- data/.vbw-planning/milestones/upgrade-assurance/phases/03-upgrade-skill-docs/PLAN-01-SUMMARY.md +0 -36
- data/.vbw-planning/milestones/upgrade-assurance/phases/03-upgrade-skill-docs/PLAN-01.md +0 -652
- data/.vbw-planning/phases/01-aia-certificate-resolution/.context-dev.md +0 -17
- data/.vbw-planning/phases/01-aia-certificate-resolution/PLAN-01-SUMMARY.md +0 -26
- data/.vbw-planning/phases/01-aia-certificate-resolution/PLAN-01.md +0 -71
- data/.vbw-planning/phases/01-aia-certificate-resolution/PLAN-02-SUMMARY.md +0 -16
- data/.vbw-planning/phases/01-aia-certificate-resolution/PLAN-02.md +0 -56
- data/.vbw-planning/phases/01-aia-certificate-resolution/PLAN-03-SUMMARY.md +0 -17
- data/.vbw-planning/phases/01-aia-certificate-resolution/PLAN-03.md +0 -98
|
@@ -1,747 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
phase: 4
|
|
3
|
-
plan: "01"
|
|
4
|
-
title: dashboard-url-display-and-clickable-links
|
|
5
|
-
type: execute
|
|
6
|
-
wave: 1
|
|
7
|
-
depends_on: []
|
|
8
|
-
cross_phase_deps: []
|
|
9
|
-
autonomous: true
|
|
10
|
-
effort_override: thorough
|
|
11
|
-
skills_used: []
|
|
12
|
-
files_modified:
|
|
13
|
-
- lib/source_monitor/dashboard/recent_activity.rb
|
|
14
|
-
- lib/source_monitor/dashboard/recent_activity_presenter.rb
|
|
15
|
-
- lib/source_monitor/dashboard/queries/recent_activity_query.rb
|
|
16
|
-
- lib/source_monitor/logs/table_presenter.rb
|
|
17
|
-
- app/helpers/source_monitor/application_helper.rb
|
|
18
|
-
- app/views/source_monitor/dashboard/_recent_activity.html.erb
|
|
19
|
-
- app/views/source_monitor/logs/index.html.erb
|
|
20
|
-
- app/views/source_monitor/sources/_row.html.erb
|
|
21
|
-
- app/views/source_monitor/sources/_details.html.erb
|
|
22
|
-
- app/views/source_monitor/items/_details.html.erb
|
|
23
|
-
- test/lib/source_monitor/dashboard/recent_activity_presenter_test.rb
|
|
24
|
-
- test/lib/source_monitor/logs/table_presenter_test.rb
|
|
25
|
-
- test/helpers/source_monitor/application_helper_test.rb
|
|
26
|
-
must_haves:
|
|
27
|
-
truths:
|
|
28
|
-
- "Running `PARALLEL_WORKERS=1 bin/rails test test/lib/source_monitor/dashboard/recent_activity_presenter_test.rb` exits 0 with 0 failures"
|
|
29
|
-
- "Running `PARALLEL_WORKERS=1 bin/rails test test/lib/source_monitor/logs/table_presenter_test.rb` exits 0 with 0 failures"
|
|
30
|
-
- "Running `bin/rails test` exits 0 with 874+ runs and 0 failures"
|
|
31
|
-
- "Running `bin/rubocop` exits 0 with 0 offenses"
|
|
32
|
-
artifacts:
|
|
33
|
-
- path: "lib/source_monitor/dashboard/recent_activity.rb"
|
|
34
|
-
provides: "Event struct with source_feed_url field for domain display"
|
|
35
|
-
contains: "source_feed_url"
|
|
36
|
-
- path: "lib/source_monitor/dashboard/recent_activity_presenter.rb"
|
|
37
|
-
provides: "Fetch events include source domain, scrape events include item URL (REQ-22)"
|
|
38
|
-
contains: "source_domain"
|
|
39
|
-
- path: "lib/source_monitor/dashboard/queries/recent_activity_query.rb"
|
|
40
|
-
provides: "SQL JOINs to pull feed_url for fetch events and item url for scrape events"
|
|
41
|
-
contains: "feed_url"
|
|
42
|
-
- path: "lib/source_monitor/logs/table_presenter.rb"
|
|
43
|
-
provides: "Row#url_label returns domain for fetch rows, item URL for scrape rows"
|
|
44
|
-
contains: "url_label"
|
|
45
|
-
- path: "app/helpers/source_monitor/application_helper.rb"
|
|
46
|
-
provides: "external_link_to helper that adds target=_blank, rel=noopener, and external-link icon"
|
|
47
|
-
contains: "external_link_to"
|
|
48
|
-
- path: "app/views/source_monitor/dashboard/_recent_activity.html.erb"
|
|
49
|
-
provides: "URL info displayed below description for fetch and scrape events"
|
|
50
|
-
contains: "url_display"
|
|
51
|
-
- path: "app/views/source_monitor/sources/_row.html.erb"
|
|
52
|
-
provides: "Feed URL in source row is a clickable external link"
|
|
53
|
-
contains: "external_link_to"
|
|
54
|
-
- path: "app/views/source_monitor/sources/_details.html.erb"
|
|
55
|
-
provides: "Website URL and Feed URL are clickable external links"
|
|
56
|
-
contains: "external_link_to"
|
|
57
|
-
- path: "app/views/source_monitor/items/_details.html.erb"
|
|
58
|
-
provides: "Item URL and Canonical URL are clickable external links"
|
|
59
|
-
contains: "external_link_to"
|
|
60
|
-
key_links:
|
|
61
|
-
- from: "recent_activity_query.rb#fetch_log_sql"
|
|
62
|
-
to: "REQ-22"
|
|
63
|
-
via: "JOIN sources to pull feed_url, displayed as domain on dashboard"
|
|
64
|
-
- from: "recent_activity_query.rb#scrape_log_sql"
|
|
65
|
-
to: "REQ-22"
|
|
66
|
-
via: "JOIN items to pull item url, displayed on dashboard"
|
|
67
|
-
- from: "application_helper.rb#external_link_to"
|
|
68
|
-
to: "REQ-23"
|
|
69
|
-
via: "All external URLs use this helper for target=_blank + external-link icon"
|
|
70
|
-
- from: "sources/_row.html.erb"
|
|
71
|
-
to: "REQ-23"
|
|
72
|
-
via: "Feed URL in source index row is clickable"
|
|
73
|
-
- from: "sources/_details.html.erb"
|
|
74
|
-
to: "REQ-23"
|
|
75
|
-
via: "Website URL and feed URL on source detail page are clickable"
|
|
76
|
-
- from: "items/_details.html.erb"
|
|
77
|
-
to: "REQ-23"
|
|
78
|
-
via: "Item URL and canonical URL are clickable"
|
|
79
|
-
---
|
|
80
|
-
<objective>
|
|
81
|
-
Show source domain (RSS fetch logs) and item URL (scrape logs) in dashboard recent activity and logs table for both success and failure entries (REQ-22). Make all external URLs (feed URLs, website URLs, item URLs) clickable links that open in a new tab with an external-link icon indicator across dashboard, sources index, source detail, and item detail views (REQ-23).
|
|
82
|
-
</objective>
|
|
83
|
-
<context>
|
|
84
|
-
@lib/source_monitor/dashboard/recent_activity.rb -- Event struct with keyword_init. Currently has: type, id, occurred_at, success, items_created, items_updated, scraper_adapter, item_title, item_url, source_name, source_id. Add `source_feed_url` field so the presenter can extract the domain for fetch events. The `item_url` field already exists but is currently NULL for scrape events in the SQL query.
|
|
85
|
-
|
|
86
|
-
@lib/source_monitor/dashboard/recent_activity_presenter.rb -- Transforms Event structs into view-model hashes with keys: label, description, status, type, time, path. `fetch_event` currently shows "N created / N updated" as description. Add `url_display` key to the hash: for fetch events, extract domain from `event.source_feed_url` using `URI.parse(url).host`; for scrape events, use `event.item_url`. Both success and failure events get URL info since the source/item is known regardless of outcome.
|
|
87
|
-
|
|
88
|
-
@lib/source_monitor/dashboard/queries/recent_activity_query.rb -- Raw SQL UNION query. `fetch_log_sql` currently selects `NULL AS source_name` and `NULL AS item_url`. Change: (1) JOIN sources table to fetch_logs and SELECT `feed_url AS source_feed_url` (new column in UNION), (2) For scrape_log_sql, JOIN items table and SELECT `items.url AS item_url` (currently NULL). Add `source_feed_url` to the outer SELECT and `build_event`. All three sub-queries must have matching column count, so add `NULL AS source_feed_url` to scrape_log_sql and item_sql.
|
|
89
|
-
|
|
90
|
-
@lib/source_monitor/logs/table_presenter.rb -- Row class wraps LogEntry records. Add `url_label` method: for fetch rows, extract domain from `entry.source&.feed_url`; for scrape rows, return `entry.item&.url`. This will be displayed in the logs table. The LogEntry model already has `belongs_to :source` and `belongs_to :item` so the associations are available.
|
|
91
|
-
|
|
92
|
-
@app/helpers/source_monitor/application_helper.rb -- Add `external_link_to(label, url, **options)` helper that wraps `link_to` with `target: "_blank"`, `rel: "noopener noreferrer"`, and appends a small external-link SVG icon. This DRYs up the pattern used across all views. The helper should handle nil/blank URLs gracefully (return label as plain text). Include Tailwind classes for consistent styling.
|
|
93
|
-
|
|
94
|
-
@app/views/source_monitor/dashboard/_recent_activity.html.erb -- Currently shows `event[:description]` as text. Add `event[:url_display]` rendering below the description as a smaller, muted line showing the URL/domain. Use the external_link_to helper to make it clickable. Only render if url_display is present.
|
|
95
|
-
|
|
96
|
-
@app/views/source_monitor/logs/index.html.erb -- The table has columns: Started, Type, Subject, Source, HTTP/Adapter, Result, Metrics, detail link. The URL info fits naturally into the existing Subject column as a second line below the primary_label, similar to how the sources row shows feed_url below the name. Use `row.url_label` for this.
|
|
97
|
-
|
|
98
|
-
@app/views/source_monitor/sources/_row.html.erb -- Line 32: `<div class="text-xs text-slate-500 truncate max-w-xs"><%= source.feed_url %></div>` -- plain text. Replace with `external_link_to` helper call, truncating the display text.
|
|
99
|
-
|
|
100
|
-
@app/views/source_monitor/sources/_details.html.erb -- Line 28: `Feed URL: <%= source.feed_url %>` -- plain text. Replace with external_link_to. Line 140: `"Website" => (source.website_url.presence || "-")` -- plain text in details hash. Replace with external_link_to call for the value.
|
|
101
|
-
|
|
102
|
-
@app/views/source_monitor/items/_details.html.erb -- Lines 56-57: `"URL" => item.url` and `"Canonical URL" => item.canonical_url || "-"` -- plain text in details hash. Replace both with external_link_to helper calls.
|
|
103
|
-
|
|
104
|
-
@test/lib/source_monitor/dashboard/recent_activity_presenter_test.rb -- 2 existing tests. Add tests for: (a) fetch event includes url_display with domain from source_feed_url, (b) scrape event includes url_display with item URL, (c) fetch event with nil source_feed_url omits url_display, (d) failure fetch event still includes url_display.
|
|
105
|
-
|
|
106
|
-
@test/lib/source_monitor/logs/table_presenter_test.rb -- 1 existing test with comprehensive assertions. Add assertions for `url_label` on fetch_row (domain from source feed_url) and scrape_row (item URL).
|
|
107
|
-
|
|
108
|
-
**Rationale:** The dashboard is the primary monitoring surface. When a fetch fails, operators need to immediately see which feed URL was involved without clicking through. Similarly, scrape failures should show the item URL. Making all external URLs clickable with new-tab behavior follows standard UX conventions for dashboards that reference external resources.
|
|
109
|
-
</context>
|
|
110
|
-
<tasks>
|
|
111
|
-
<task type="auto">
|
|
112
|
-
<name>add-external-link-helper-and-tests</name>
|
|
113
|
-
<files>
|
|
114
|
-
app/helpers/source_monitor/application_helper.rb
|
|
115
|
-
test/helpers/source_monitor/application_helper_test.rb
|
|
116
|
-
</files>
|
|
117
|
-
<action>
|
|
118
|
-
**Add `external_link_to` helper to `app/helpers/source_monitor/application_helper.rb`:**
|
|
119
|
-
|
|
120
|
-
Add the following public method before the `private` keyword (around line 215):
|
|
121
|
-
|
|
122
|
-
```ruby
|
|
123
|
-
# Renders a clickable link that opens in a new tab with an external-link icon.
|
|
124
|
-
# Returns the label as plain text if the URL is blank.
|
|
125
|
-
def external_link_to(label, url, **options)
|
|
126
|
-
return label if url.blank?
|
|
127
|
-
|
|
128
|
-
css = options.delete(:class) || "text-blue-600 hover:text-blue-500"
|
|
129
|
-
link_to(url, target: "_blank", rel: "noopener noreferrer", class: css, title: url, **options) do
|
|
130
|
-
safe_join([label, " ", external_link_icon])
|
|
131
|
-
end
|
|
132
|
-
end
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
Also add a private `external_link_icon` method after the `private` keyword:
|
|
136
|
-
|
|
137
|
-
```ruby
|
|
138
|
-
def external_link_icon
|
|
139
|
-
tag.svg(
|
|
140
|
-
class: "inline-block h-3 w-3 text-slate-400",
|
|
141
|
-
xmlns: "http://www.w3.org/2000/svg",
|
|
142
|
-
fill: "none",
|
|
143
|
-
viewBox: "0 0 24 24",
|
|
144
|
-
stroke_width: "2",
|
|
145
|
-
stroke: "currentColor",
|
|
146
|
-
aria: { hidden: "true" }
|
|
147
|
-
) do
|
|
148
|
-
safe_join([
|
|
149
|
-
tag.path(
|
|
150
|
-
stroke_linecap: "round",
|
|
151
|
-
stroke_linejoin: "round",
|
|
152
|
-
d: "M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25"
|
|
153
|
-
)
|
|
154
|
-
])
|
|
155
|
-
end
|
|
156
|
-
end
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
Also add a public `domain_from_url(url)` helper (used by presenters and views) before the `private` keyword:
|
|
160
|
-
|
|
161
|
-
```ruby
|
|
162
|
-
# Extracts the domain from a URL, returning nil if parsing fails.
|
|
163
|
-
def domain_from_url(url)
|
|
164
|
-
return nil if url.blank?
|
|
165
|
-
|
|
166
|
-
URI.parse(url.to_s).host
|
|
167
|
-
rescue URI::InvalidURIError
|
|
168
|
-
nil
|
|
169
|
-
end
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
**Add/update test file `test/helpers/source_monitor/application_helper_test.rb`:**
|
|
173
|
-
|
|
174
|
-
Create or update the test file with tests for `external_link_to` and `domain_from_url`:
|
|
175
|
-
|
|
176
|
-
```ruby
|
|
177
|
-
# frozen_string_literal: true
|
|
178
|
-
|
|
179
|
-
require "test_helper"
|
|
180
|
-
|
|
181
|
-
module SourceMonitor
|
|
182
|
-
class ApplicationHelperTest < ActionView::TestCase
|
|
183
|
-
include SourceMonitor::ApplicationHelper
|
|
184
|
-
|
|
185
|
-
test "external_link_to renders link with target blank and icon" do
|
|
186
|
-
result = external_link_to("Example", "https://example.com")
|
|
187
|
-
assert_includes result, 'target="_blank"'
|
|
188
|
-
assert_includes result, 'rel="noopener noreferrer"'
|
|
189
|
-
assert_includes result, "Example"
|
|
190
|
-
assert_includes result, "<svg"
|
|
191
|
-
end
|
|
192
|
-
|
|
193
|
-
test "external_link_to returns plain label when url is blank" do
|
|
194
|
-
result = external_link_to("No URL", nil)
|
|
195
|
-
assert_equal "No URL", result
|
|
196
|
-
end
|
|
197
|
-
|
|
198
|
-
test "external_link_to returns plain label when url is empty string" do
|
|
199
|
-
result = external_link_to("No URL", "")
|
|
200
|
-
assert_equal "No URL", result
|
|
201
|
-
end
|
|
202
|
-
|
|
203
|
-
test "external_link_to accepts custom css class" do
|
|
204
|
-
result = external_link_to("Link", "https://example.com", class: "custom-class")
|
|
205
|
-
assert_includes result, "custom-class"
|
|
206
|
-
end
|
|
207
|
-
|
|
208
|
-
test "domain_from_url extracts host from valid URL" do
|
|
209
|
-
assert_equal "example.com", domain_from_url("https://example.com/path")
|
|
210
|
-
assert_equal "blog.example.org", domain_from_url("https://blog.example.org/feed.xml")
|
|
211
|
-
end
|
|
212
|
-
|
|
213
|
-
test "domain_from_url returns nil for blank URL" do
|
|
214
|
-
assert_nil domain_from_url(nil)
|
|
215
|
-
assert_nil domain_from_url("")
|
|
216
|
-
end
|
|
217
|
-
|
|
218
|
-
test "domain_from_url returns nil for invalid URL" do
|
|
219
|
-
assert_nil domain_from_url("not a url %%%")
|
|
220
|
-
end
|
|
221
|
-
end
|
|
222
|
-
end
|
|
223
|
-
```
|
|
224
|
-
</action>
|
|
225
|
-
<verify>
|
|
226
|
-
Run `PARALLEL_WORKERS=1 bin/rails test test/helpers/source_monitor/application_helper_test.rb` -- all tests pass. Run `bin/rubocop app/helpers/source_monitor/application_helper.rb test/helpers/source_monitor/application_helper_test.rb` -- 0 offenses.
|
|
227
|
-
</verify>
|
|
228
|
-
<done>
|
|
229
|
-
external_link_to helper renders links with target=_blank, rel=noopener noreferrer, and external-link SVG icon. domain_from_url extracts hostnames from URLs. Both handle nil/blank gracefully. 7 tests pass. REQ-23 foundation established.
|
|
230
|
-
</done>
|
|
231
|
-
</task>
|
|
232
|
-
<task type="auto">
|
|
233
|
-
<name>add-url-info-to-recent-activity-query-and-presenter</name>
|
|
234
|
-
<files>
|
|
235
|
-
lib/source_monitor/dashboard/recent_activity.rb
|
|
236
|
-
lib/source_monitor/dashboard/queries/recent_activity_query.rb
|
|
237
|
-
lib/source_monitor/dashboard/recent_activity_presenter.rb
|
|
238
|
-
test/lib/source_monitor/dashboard/recent_activity_presenter_test.rb
|
|
239
|
-
</files>
|
|
240
|
-
<action>
|
|
241
|
-
**Step 1: Add `source_feed_url` to the Event struct in `lib/source_monitor/dashboard/recent_activity.rb`:**
|
|
242
|
-
|
|
243
|
-
Add `:source_feed_url` to the Struct fields, after `:source_id`:
|
|
244
|
-
|
|
245
|
-
```ruby
|
|
246
|
-
Event = Struct.new(
|
|
247
|
-
:type,
|
|
248
|
-
:id,
|
|
249
|
-
:occurred_at,
|
|
250
|
-
:success,
|
|
251
|
-
:items_created,
|
|
252
|
-
:items_updated,
|
|
253
|
-
:scraper_adapter,
|
|
254
|
-
:item_title,
|
|
255
|
-
:item_url,
|
|
256
|
-
:source_name,
|
|
257
|
-
:source_id,
|
|
258
|
-
:source_feed_url,
|
|
259
|
-
keyword_init: true
|
|
260
|
-
)
|
|
261
|
-
```
|
|
262
|
-
|
|
263
|
-
**Step 2: Update `lib/source_monitor/dashboard/queries/recent_activity_query.rb`:**
|
|
264
|
-
|
|
265
|
-
(a) Add `source_feed_url` to the outer SELECT in `unified_sql_template`:
|
|
266
|
-
```ruby
|
|
267
|
-
SELECT resource_type,
|
|
268
|
-
resource_id,
|
|
269
|
-
occurred_at,
|
|
270
|
-
success_flag,
|
|
271
|
-
items_created,
|
|
272
|
-
items_updated,
|
|
273
|
-
scraper_adapter,
|
|
274
|
-
item_title,
|
|
275
|
-
item_url,
|
|
276
|
-
source_name,
|
|
277
|
-
source_id,
|
|
278
|
-
source_feed_url
|
|
279
|
-
FROM (
|
|
280
|
-
```
|
|
281
|
-
|
|
282
|
-
(b) Update `fetch_log_sql` to JOIN sources and select feed_url:
|
|
283
|
-
```ruby
|
|
284
|
-
def fetch_log_sql
|
|
285
|
-
<<~SQL
|
|
286
|
-
SELECT
|
|
287
|
-
'#{EVENT_TYPE_FETCH}' AS resource_type,
|
|
288
|
-
#{SourceMonitor::FetchLog.quoted_table_name}.id AS resource_id,
|
|
289
|
-
#{SourceMonitor::FetchLog.quoted_table_name}.started_at AS occurred_at,
|
|
290
|
-
CASE WHEN #{SourceMonitor::FetchLog.quoted_table_name}.success THEN 1 ELSE 0 END AS success_flag,
|
|
291
|
-
#{SourceMonitor::FetchLog.quoted_table_name}.items_created AS items_created,
|
|
292
|
-
#{SourceMonitor::FetchLog.quoted_table_name}.items_updated AS items_updated,
|
|
293
|
-
NULL AS scraper_adapter,
|
|
294
|
-
NULL AS item_title,
|
|
295
|
-
NULL AS item_url,
|
|
296
|
-
#{SourceMonitor::Source.quoted_table_name}.#{quoted_source_name} AS source_name,
|
|
297
|
-
#{SourceMonitor::FetchLog.quoted_table_name}.source_id AS source_id,
|
|
298
|
-
#{SourceMonitor::Source.quoted_table_name}.feed_url AS source_feed_url
|
|
299
|
-
FROM #{SourceMonitor::FetchLog.quoted_table_name}
|
|
300
|
-
LEFT JOIN #{SourceMonitor::Source.quoted_table_name}
|
|
301
|
-
ON #{SourceMonitor::Source.quoted_table_name}.id = #{SourceMonitor::FetchLog.quoted_table_name}.source_id
|
|
302
|
-
SQL
|
|
303
|
-
end
|
|
304
|
-
```
|
|
305
|
-
|
|
306
|
-
(c) Update `scrape_log_sql` to also JOIN items and select item url, plus add NULL source_feed_url:
|
|
307
|
-
```ruby
|
|
308
|
-
def scrape_log_sql
|
|
309
|
-
<<~SQL
|
|
310
|
-
SELECT
|
|
311
|
-
'#{EVENT_TYPE_SCRAPE}' AS resource_type,
|
|
312
|
-
#{SourceMonitor::ScrapeLog.quoted_table_name}.id AS resource_id,
|
|
313
|
-
#{SourceMonitor::ScrapeLog.quoted_table_name}.started_at AS occurred_at,
|
|
314
|
-
CASE WHEN #{SourceMonitor::ScrapeLog.quoted_table_name}.success THEN 1 ELSE 0 END AS success_flag,
|
|
315
|
-
NULL AS items_created,
|
|
316
|
-
NULL AS items_updated,
|
|
317
|
-
#{SourceMonitor::ScrapeLog.quoted_table_name}.scraper_adapter AS scraper_adapter,
|
|
318
|
-
NULL AS item_title,
|
|
319
|
-
#{SourceMonitor::Item.quoted_table_name}.url AS item_url,
|
|
320
|
-
#{SourceMonitor::Source.quoted_table_name}.#{quoted_source_name} AS source_name,
|
|
321
|
-
#{SourceMonitor::ScrapeLog.quoted_table_name}.source_id AS source_id,
|
|
322
|
-
NULL AS source_feed_url
|
|
323
|
-
FROM #{SourceMonitor::ScrapeLog.quoted_table_name}
|
|
324
|
-
LEFT JOIN #{SourceMonitor::Source.quoted_table_name}
|
|
325
|
-
ON #{SourceMonitor::Source.quoted_table_name}.id = #{SourceMonitor::ScrapeLog.quoted_table_name}.source_id
|
|
326
|
-
LEFT JOIN #{SourceMonitor::Item.quoted_table_name}
|
|
327
|
-
ON #{SourceMonitor::Item.quoted_table_name}.id = #{SourceMonitor::ScrapeLog.quoted_table_name}.item_id
|
|
328
|
-
SQL
|
|
329
|
-
end
|
|
330
|
-
```
|
|
331
|
-
|
|
332
|
-
(d) Update `item_sql` to add NULL source_feed_url:
|
|
333
|
-
```ruby
|
|
334
|
-
# Add after the source_id line:
|
|
335
|
-
NULL AS source_feed_url
|
|
336
|
-
```
|
|
337
|
-
|
|
338
|
-
(e) Update `build_event` to include the new field:
|
|
339
|
-
```ruby
|
|
340
|
-
source_feed_url: row["source_feed_url"]
|
|
341
|
-
```
|
|
342
|
-
|
|
343
|
-
**Step 3: Update `lib/source_monitor/dashboard/recent_activity_presenter.rb`:**
|
|
344
|
-
|
|
345
|
-
(a) Add `url_display` and `url_href` keys to `fetch_event`:
|
|
346
|
-
```ruby
|
|
347
|
-
def fetch_event(event)
|
|
348
|
-
domain = source_domain(event.source_feed_url)
|
|
349
|
-
{
|
|
350
|
-
label: "Fetch ##{event.id}",
|
|
351
|
-
description: "#{event.items_created.to_i} created / #{event.items_updated.to_i} updated",
|
|
352
|
-
status: event.success? ? :success : :failure,
|
|
353
|
-
type: :fetch,
|
|
354
|
-
time: event.occurred_at,
|
|
355
|
-
path: url_helpers.fetch_log_path(event.id),
|
|
356
|
-
url_display: domain,
|
|
357
|
-
url_href: event.source_feed_url
|
|
358
|
-
}
|
|
359
|
-
end
|
|
360
|
-
```
|
|
361
|
-
|
|
362
|
-
(b) Add `url_display` and `url_href` keys to `scrape_event`:
|
|
363
|
-
```ruby
|
|
364
|
-
def scrape_event(event)
|
|
365
|
-
{
|
|
366
|
-
label: "Scrape ##{event.id}",
|
|
367
|
-
description: (event.scraper_adapter.presence || "Scraper"),
|
|
368
|
-
status: event.success? ? :success : :failure,
|
|
369
|
-
type: :scrape,
|
|
370
|
-
time: event.occurred_at,
|
|
371
|
-
path: url_helpers.scrape_log_path(event.id),
|
|
372
|
-
url_display: event.item_url,
|
|
373
|
-
url_href: event.item_url
|
|
374
|
-
}
|
|
375
|
-
end
|
|
376
|
-
```
|
|
377
|
-
|
|
378
|
-
(c) Add private `source_domain` method:
|
|
379
|
-
```ruby
|
|
380
|
-
def source_domain(feed_url)
|
|
381
|
-
return nil if feed_url.blank?
|
|
382
|
-
|
|
383
|
-
URI.parse(feed_url.to_s).host
|
|
384
|
-
rescue URI::InvalidURIError
|
|
385
|
-
nil
|
|
386
|
-
end
|
|
387
|
-
```
|
|
388
|
-
|
|
389
|
-
**Step 4: Update tests in `test/lib/source_monitor/dashboard/recent_activity_presenter_test.rb`:**
|
|
390
|
-
|
|
391
|
-
Add the following tests after the existing ones:
|
|
392
|
-
|
|
393
|
-
```ruby
|
|
394
|
-
test "fetch event includes source domain as url_display" do
|
|
395
|
-
event = SourceMonitor::Dashboard::RecentActivity::Event.new(
|
|
396
|
-
type: :fetch_log,
|
|
397
|
-
id: 10,
|
|
398
|
-
occurred_at: Time.current,
|
|
399
|
-
success: true,
|
|
400
|
-
items_created: 2,
|
|
401
|
-
items_updated: 0,
|
|
402
|
-
source_feed_url: "https://blog.example.com/feed.xml"
|
|
403
|
-
)
|
|
404
|
-
|
|
405
|
-
presenter = SourceMonitor::Dashboard::RecentActivityPresenter.new(
|
|
406
|
-
[event],
|
|
407
|
-
url_helpers: SourceMonitor::Engine.routes.url_helpers
|
|
408
|
-
)
|
|
409
|
-
|
|
410
|
-
result = presenter.to_a.first
|
|
411
|
-
assert_equal "blog.example.com", result[:url_display]
|
|
412
|
-
assert_equal "https://blog.example.com/feed.xml", result[:url_href]
|
|
413
|
-
end
|
|
414
|
-
|
|
415
|
-
test "fetch event with nil source_feed_url has nil url_display" do
|
|
416
|
-
event = SourceMonitor::Dashboard::RecentActivity::Event.new(
|
|
417
|
-
type: :fetch_log,
|
|
418
|
-
id: 11,
|
|
419
|
-
occurred_at: Time.current,
|
|
420
|
-
success: false,
|
|
421
|
-
items_created: 0,
|
|
422
|
-
items_updated: 0,
|
|
423
|
-
source_feed_url: nil
|
|
424
|
-
)
|
|
425
|
-
|
|
426
|
-
presenter = SourceMonitor::Dashboard::RecentActivityPresenter.new(
|
|
427
|
-
[event],
|
|
428
|
-
url_helpers: SourceMonitor::Engine.routes.url_helpers
|
|
429
|
-
)
|
|
430
|
-
|
|
431
|
-
result = presenter.to_a.first
|
|
432
|
-
assert_nil result[:url_display]
|
|
433
|
-
assert_equal :failure, result[:status]
|
|
434
|
-
end
|
|
435
|
-
|
|
436
|
-
test "failure fetch event still includes url_display" do
|
|
437
|
-
event = SourceMonitor::Dashboard::RecentActivity::Event.new(
|
|
438
|
-
type: :fetch_log,
|
|
439
|
-
id: 12,
|
|
440
|
-
occurred_at: Time.current,
|
|
441
|
-
success: false,
|
|
442
|
-
items_created: 0,
|
|
443
|
-
items_updated: 0,
|
|
444
|
-
source_feed_url: "https://failing-feed.example.org/rss"
|
|
445
|
-
)
|
|
446
|
-
|
|
447
|
-
presenter = SourceMonitor::Dashboard::RecentActivityPresenter.new(
|
|
448
|
-
[event],
|
|
449
|
-
url_helpers: SourceMonitor::Engine.routes.url_helpers
|
|
450
|
-
)
|
|
451
|
-
|
|
452
|
-
result = presenter.to_a.first
|
|
453
|
-
assert_equal "failing-feed.example.org", result[:url_display]
|
|
454
|
-
assert_equal :failure, result[:status]
|
|
455
|
-
end
|
|
456
|
-
|
|
457
|
-
test "scrape event includes item url as url_display" do
|
|
458
|
-
event = SourceMonitor::Dashboard::RecentActivity::Event.new(
|
|
459
|
-
type: :scrape_log,
|
|
460
|
-
id: 20,
|
|
461
|
-
occurred_at: Time.current,
|
|
462
|
-
success: true,
|
|
463
|
-
scraper_adapter: "readability",
|
|
464
|
-
item_url: "https://example.com/articles/42"
|
|
465
|
-
)
|
|
466
|
-
|
|
467
|
-
presenter = SourceMonitor::Dashboard::RecentActivityPresenter.new(
|
|
468
|
-
[event],
|
|
469
|
-
url_helpers: SourceMonitor::Engine.routes.url_helpers
|
|
470
|
-
)
|
|
471
|
-
|
|
472
|
-
result = presenter.to_a.first
|
|
473
|
-
assert_equal "https://example.com/articles/42", result[:url_display]
|
|
474
|
-
assert_equal "https://example.com/articles/42", result[:url_href]
|
|
475
|
-
end
|
|
476
|
-
```
|
|
477
|
-
</action>
|
|
478
|
-
<verify>
|
|
479
|
-
Run `PARALLEL_WORKERS=1 bin/rails test test/lib/source_monitor/dashboard/recent_activity_presenter_test.rb` -- all 6 tests pass. Run `bin/rubocop lib/source_monitor/dashboard/recent_activity.rb lib/source_monitor/dashboard/recent_activity_presenter.rb lib/source_monitor/dashboard/queries/recent_activity_query.rb` -- 0 offenses.
|
|
480
|
-
</verify>
|
|
481
|
-
<done>
|
|
482
|
-
RecentActivityQuery now JOINs sources for fetch logs (pulling feed_url) and JOINs items for scrape logs (pulling item url). Presenter extracts domain for fetch events and passes through item URL for scrape events. Both success and failure events include URL info. 6 tests pass. REQ-22 core data layer complete.
|
|
483
|
-
</done>
|
|
484
|
-
</task>
|
|
485
|
-
<task type="auto">
|
|
486
|
-
<name>add-url-to-logs-table-presenter</name>
|
|
487
|
-
<files>
|
|
488
|
-
lib/source_monitor/logs/table_presenter.rb
|
|
489
|
-
test/lib/source_monitor/logs/table_presenter_test.rb
|
|
490
|
-
</files>
|
|
491
|
-
<action>
|
|
492
|
-
**Step 1: Add `url_label` and `url_href` methods to `lib/source_monitor/logs/table_presenter.rb` Row class:**
|
|
493
|
-
|
|
494
|
-
Add these public methods after the `primary_path` method (around line 62):
|
|
495
|
-
|
|
496
|
-
```ruby
|
|
497
|
-
def url_label
|
|
498
|
-
if fetch?
|
|
499
|
-
domain_from_feed_url
|
|
500
|
-
elsif scrape?
|
|
501
|
-
entry.item&.url
|
|
502
|
-
end
|
|
503
|
-
end
|
|
504
|
-
|
|
505
|
-
def url_href
|
|
506
|
-
if fetch?
|
|
507
|
-
entry.source&.feed_url
|
|
508
|
-
elsif scrape?
|
|
509
|
-
entry.item&.url
|
|
510
|
-
end
|
|
511
|
-
end
|
|
512
|
-
```
|
|
513
|
-
|
|
514
|
-
Add a private helper method after the existing `attr_reader` line (around line 144):
|
|
515
|
-
|
|
516
|
-
```ruby
|
|
517
|
-
def domain_from_feed_url
|
|
518
|
-
feed_url = entry.source&.feed_url
|
|
519
|
-
return nil if feed_url.blank?
|
|
520
|
-
|
|
521
|
-
URI.parse(feed_url.to_s).host
|
|
522
|
-
rescue URI::InvalidURIError
|
|
523
|
-
nil
|
|
524
|
-
end
|
|
525
|
-
```
|
|
526
|
-
|
|
527
|
-
**Step 2: Update tests in `test/lib/source_monitor/logs/table_presenter_test.rb`:**
|
|
528
|
-
|
|
529
|
-
Add assertions for `url_label` and `url_href` inside the existing "builds typed row view models" test:
|
|
530
|
-
|
|
531
|
-
After the fetch_row assertions block (around line 91), add:
|
|
532
|
-
```ruby
|
|
533
|
-
assert_match(/presenter-source\.test/, fetch_row.url_label) if @source.feed_url.present?
|
|
534
|
-
assert_equal @source.feed_url, fetch_row.url_href
|
|
535
|
-
```
|
|
536
|
-
|
|
537
|
-
Wait -- the test creates a source via `create_source!(name: "Presenter Source")`. Check what feed_url that gives. The factory likely sets a default feed_url. Add these assertions:
|
|
538
|
-
|
|
539
|
-
For the fetch_row section, after `assert_equal source_path(@source), fetch_row.primary_path`, add:
|
|
540
|
-
```ruby
|
|
541
|
-
assert_equal URI.parse(@source.feed_url).host, fetch_row.url_label
|
|
542
|
-
assert_equal @source.feed_url, fetch_row.url_href
|
|
543
|
-
```
|
|
544
|
-
|
|
545
|
-
For the scrape_row section, after `assert_equal item_path(@item), scrape_row.primary_path`, add:
|
|
546
|
-
```ruby
|
|
547
|
-
assert_equal "https://example.com/articles/presenter", scrape_row.url_label
|
|
548
|
-
assert_equal "https://example.com/articles/presenter", scrape_row.url_href
|
|
549
|
-
```
|
|
550
|
-
|
|
551
|
-
For the health_row section, after the last assertion, add:
|
|
552
|
-
```ruby
|
|
553
|
-
assert_nil health_row.url_label
|
|
554
|
-
assert_nil health_row.url_href
|
|
555
|
-
```
|
|
556
|
-
</action>
|
|
557
|
-
<verify>
|
|
558
|
-
Run `PARALLEL_WORKERS=1 bin/rails test test/lib/source_monitor/logs/table_presenter_test.rb` -- all tests pass. Run `bin/rubocop lib/source_monitor/logs/table_presenter.rb` -- 0 offenses.
|
|
559
|
-
</verify>
|
|
560
|
-
<done>
|
|
561
|
-
TablePresenter::Row now provides url_label (domain for fetches, full URL for scrapes) and url_href for linking. Health check rows return nil. Tests updated with assertions for all three row types. REQ-22 logs table data layer complete.
|
|
562
|
-
</done>
|
|
563
|
-
</task>
|
|
564
|
-
<task type="auto">
|
|
565
|
-
<name>update-dashboard-and-logs-views-with-url-display</name>
|
|
566
|
-
<files>
|
|
567
|
-
app/views/source_monitor/dashboard/_recent_activity.html.erb
|
|
568
|
-
app/views/source_monitor/logs/index.html.erb
|
|
569
|
-
</files>
|
|
570
|
-
<action>
|
|
571
|
-
**Step 1: Update `app/views/source_monitor/dashboard/_recent_activity.html.erb`:**
|
|
572
|
-
|
|
573
|
-
After the description line (line 22-23), add a URL display line. Replace the description block:
|
|
574
|
-
|
|
575
|
-
Current (lines 21-23):
|
|
576
|
-
```erb
|
|
577
|
-
<div class="mt-1 text-xs text-slate-500">
|
|
578
|
-
<%= event[:description].presence || "No additional details recorded." %>
|
|
579
|
-
</div>
|
|
580
|
-
```
|
|
581
|
-
|
|
582
|
-
Replace with:
|
|
583
|
-
```erb
|
|
584
|
-
<div class="mt-1 text-xs text-slate-500">
|
|
585
|
-
<%= event[:description].presence || "No additional details recorded." %>
|
|
586
|
-
</div>
|
|
587
|
-
<% if event[:url_display].present? %>
|
|
588
|
-
<div class="mt-0.5 text-xs text-slate-400 truncate max-w-sm" data-testid="event-url-display">
|
|
589
|
-
<% if event[:url_href].present? %>
|
|
590
|
-
<%= external_link_to event[:url_display], event[:url_href], class: "text-slate-400 hover:text-blue-500" %>
|
|
591
|
-
<% else %>
|
|
592
|
-
<%= event[:url_display] %>
|
|
593
|
-
<% end %>
|
|
594
|
-
</div>
|
|
595
|
-
<% end %>
|
|
596
|
-
```
|
|
597
|
-
|
|
598
|
-
**Step 2: Update `app/views/source_monitor/logs/index.html.erb`:**
|
|
599
|
-
|
|
600
|
-
In the table body, update the Subject column (lines 131-136) to also show the URL below the primary label:
|
|
601
|
-
|
|
602
|
-
Current:
|
|
603
|
-
```erb
|
|
604
|
-
<td class="px-6 py-4 text-sm">
|
|
605
|
-
<% if row.primary_path %>
|
|
606
|
-
<%= link_to row.primary_label, row.primary_path, class: "text-blue-600 hover:text-blue-500" %>
|
|
607
|
-
<% else %>
|
|
608
|
-
<%= row.primary_label %>
|
|
609
|
-
<% end %>
|
|
610
|
-
</td>
|
|
611
|
-
```
|
|
612
|
-
|
|
613
|
-
Replace with:
|
|
614
|
-
```erb
|
|
615
|
-
<td class="px-6 py-4 text-sm">
|
|
616
|
-
<% if row.primary_path %>
|
|
617
|
-
<%= link_to row.primary_label, row.primary_path, class: "text-blue-600 hover:text-blue-500" %>
|
|
618
|
-
<% else %>
|
|
619
|
-
<%= row.primary_label %>
|
|
620
|
-
<% end %>
|
|
621
|
-
<% if row.url_label.present? %>
|
|
622
|
-
<div class="mt-0.5 text-xs text-slate-400 truncate max-w-xs">
|
|
623
|
-
<% if row.url_href.present? %>
|
|
624
|
-
<%= external_link_to row.url_label, row.url_href, class: "text-slate-400 hover:text-blue-500" %>
|
|
625
|
-
<% else %>
|
|
626
|
-
<%= row.url_label %>
|
|
627
|
-
<% end %>
|
|
628
|
-
</div>
|
|
629
|
-
<% end %>
|
|
630
|
-
</td>
|
|
631
|
-
```
|
|
632
|
-
|
|
633
|
-
This preserves the existing layout while adding URL context below the subject line, using the same pattern as the sources index row (feed URL below source name).
|
|
634
|
-
</action>
|
|
635
|
-
<verify>
|
|
636
|
-
Run `bin/rubocop` on the modified .erb files (RuboCop may not lint .erb but confirm no syntax errors). Run `bin/rails test` to ensure no rendering errors. Visually inspect: the URL line should appear below the description/subject in both the dashboard recent activity panel and the logs table.
|
|
637
|
-
</verify>
|
|
638
|
-
<done>
|
|
639
|
-
Dashboard recent activity shows source domain for fetch events and item URL for scrape events. Logs table shows URL info below the subject column. Both use external_link_to for clickable links with new-tab behavior. Layout preserved with muted styling. REQ-22 view layer complete.
|
|
640
|
-
</done>
|
|
641
|
-
</task>
|
|
642
|
-
<task type="auto">
|
|
643
|
-
<name>make-external-urls-clickable-across-views</name>
|
|
644
|
-
<files>
|
|
645
|
-
app/views/source_monitor/sources/_row.html.erb
|
|
646
|
-
app/views/source_monitor/sources/_details.html.erb
|
|
647
|
-
app/views/source_monitor/items/_details.html.erb
|
|
648
|
-
</files>
|
|
649
|
-
<action>
|
|
650
|
-
**Step 1: Update `app/views/source_monitor/sources/_row.html.erb`:**
|
|
651
|
-
|
|
652
|
-
Replace line 32:
|
|
653
|
-
```erb
|
|
654
|
-
<div class="text-xs text-slate-500 truncate max-w-xs"><%= source.feed_url %></div>
|
|
655
|
-
```
|
|
656
|
-
|
|
657
|
-
With:
|
|
658
|
-
```erb
|
|
659
|
-
<div class="text-xs text-slate-500 truncate max-w-xs"><%= external_link_to source.feed_url, source.feed_url, class: "text-slate-500 hover:text-blue-500" %></div>
|
|
660
|
-
```
|
|
661
|
-
|
|
662
|
-
**Step 2: Update `app/views/source_monitor/sources/_details.html.erb`:**
|
|
663
|
-
|
|
664
|
-
(a) Replace line 28:
|
|
665
|
-
```erb
|
|
666
|
-
<p class="mt-2 text-sm text-slate-500">Feed URL: <%= source.feed_url %></p>
|
|
667
|
-
```
|
|
668
|
-
|
|
669
|
-
With:
|
|
670
|
-
```erb
|
|
671
|
-
<p class="mt-2 text-sm text-slate-500">Feed URL: <%= external_link_to source.feed_url, source.feed_url, class: "text-slate-500 hover:text-blue-500" %></p>
|
|
672
|
-
```
|
|
673
|
-
|
|
674
|
-
(b) In the details hash (around line 140), replace:
|
|
675
|
-
```ruby
|
|
676
|
-
"Website" => (source.website_url.presence || "\u2014"),
|
|
677
|
-
```
|
|
678
|
-
|
|
679
|
-
With:
|
|
680
|
-
```ruby
|
|
681
|
-
"Website" => (source.website_url.present? ? external_link_to(source.website_url, source.website_url, class: "text-slate-900 hover:text-blue-500") : "\u2014"),
|
|
682
|
-
```
|
|
683
|
-
|
|
684
|
-
Note: Since the details hash values are rendered via `<%= value %>`, and external_link_to returns an html_safe string from link_to, this will work correctly. However, you may need to use `raw` or ensure the helper returns `html_safe` content. Since `link_to` already returns safe HTML, this should work.
|
|
685
|
-
|
|
686
|
-
**Step 3: Update `app/views/source_monitor/items/_details.html.erb`:**
|
|
687
|
-
|
|
688
|
-
In the details hash (around lines 56-57), replace:
|
|
689
|
-
```ruby
|
|
690
|
-
"URL" => item.url,
|
|
691
|
-
"Canonical URL" => item.canonical_url || "\u2014",
|
|
692
|
-
```
|
|
693
|
-
|
|
694
|
-
With:
|
|
695
|
-
```ruby
|
|
696
|
-
"URL" => (item.url.present? ? external_link_to(item.url, item.url, class: "text-slate-900 hover:text-blue-500") : "\u2014"),
|
|
697
|
-
"Canonical URL" => (item.canonical_url.present? ? external_link_to(item.canonical_url, item.canonical_url, class: "text-slate-900 hover:text-blue-500") : "\u2014"),
|
|
698
|
-
```
|
|
699
|
-
|
|
700
|
-
**Step 4: Full suite verification:**
|
|
701
|
-
|
|
702
|
-
Run `bin/rails test` -- all 874+ tests pass with 0 failures.
|
|
703
|
-
Run `bin/rubocop` -- 0 offenses.
|
|
704
|
-
|
|
705
|
-
Check that no existing test assertions break due to the HTML changes (controller integration tests that assert on response body content may need attention if they check for exact text matches on URLs).
|
|
706
|
-
</action>
|
|
707
|
-
<verify>
|
|
708
|
-
Run `bin/rails test` -- all tests pass. Run `bin/rubocop` -- 0 offenses. Grep for `external_link_to` in the three modified view files to confirm all external URLs are now wrapped. Grep for `target="_blank"` in the rendered output would confirm new-tab behavior.
|
|
709
|
-
</verify>
|
|
710
|
-
<done>
|
|
711
|
-
All external URLs are now clickable across source index rows (feed URL), source detail page (feed URL, website URL), and item detail page (URL, canonical URL). Links open in new tab with external-link icon indicator. REQ-23 fully satisfied. Full test suite passes, RuboCop clean.
|
|
712
|
-
</done>
|
|
713
|
-
</task>
|
|
714
|
-
</tasks>
|
|
715
|
-
<verification>
|
|
716
|
-
1. `PARALLEL_WORKERS=1 bin/rails test test/helpers/source_monitor/application_helper_test.rb` -- 7 tests pass
|
|
717
|
-
2. `PARALLEL_WORKERS=1 bin/rails test test/lib/source_monitor/dashboard/recent_activity_presenter_test.rb` -- 6 tests pass
|
|
718
|
-
3. `PARALLEL_WORKERS=1 bin/rails test test/lib/source_monitor/logs/table_presenter_test.rb` -- tests pass with url_label/url_href assertions
|
|
719
|
-
4. `bin/rails test` -- 874+ runs, 0 failures
|
|
720
|
-
5. `bin/rubocop` -- 0 offenses
|
|
721
|
-
6. `grep -n 'external_link_to' app/helpers/source_monitor/application_helper.rb` -- method defined
|
|
722
|
-
7. `grep -n 'source_feed_url' lib/source_monitor/dashboard/recent_activity.rb` -- field in Event struct
|
|
723
|
-
8. `grep -n 'url_display' lib/source_monitor/dashboard/recent_activity_presenter.rb` -- key in view model hash
|
|
724
|
-
9. `grep -n 'feed_url' lib/source_monitor/dashboard/queries/recent_activity_query.rb` -- SELECT in fetch_log_sql
|
|
725
|
-
10. `grep -n 'url_label' lib/source_monitor/logs/table_presenter.rb` -- method on Row
|
|
726
|
-
11. `grep -n 'external_link_to' app/views/source_monitor/sources/_row.html.erb` -- feed URL clickable
|
|
727
|
-
12. `grep -n 'external_link_to' app/views/source_monitor/sources/_details.html.erb` -- website/feed URL clickable
|
|
728
|
-
13. `grep -n 'external_link_to' app/views/source_monitor/items/_details.html.erb` -- item URL clickable
|
|
729
|
-
14. `grep -rn 'target="_blank"' app/views/source_monitor/dashboard/_recent_activity.html.erb` -- new-tab links via helper
|
|
730
|
-
</verification>
|
|
731
|
-
<success_criteria>
|
|
732
|
-
- Fetch log events on the dashboard display the source domain extracted from feed_url (REQ-22)
|
|
733
|
-
- Scrape log events on the dashboard display the item URL being scraped (REQ-22)
|
|
734
|
-
- Both success and failure fetch/scrape events show URL info (REQ-22)
|
|
735
|
-
- Logs table shows URL info below the subject column for fetch and scrape entries (REQ-22)
|
|
736
|
-
- external_link_to helper renders links with target=_blank, rel=noopener noreferrer, external-link SVG icon (REQ-23)
|
|
737
|
-
- Source index row feed URLs are clickable external links (REQ-23)
|
|
738
|
-
- Source detail page feed URL and website URL are clickable external links (REQ-23)
|
|
739
|
-
- Item detail page URL and canonical URL are clickable external links (REQ-23)
|
|
740
|
-
- Existing dashboard layout is preserved (no structural changes to grid/flex layout)
|
|
741
|
-
- All existing tests pass with no regressions
|
|
742
|
-
- New tests cover external_link_to, domain_from_url, presenter url_display, and table presenter url_label
|
|
743
|
-
- RuboCop clean
|
|
744
|
-
</success_criteria>
|
|
745
|
-
<output>
|
|
746
|
-
.vbw-planning/phases/04-dashboard-ux/PLAN-01-SUMMARY.md
|
|
747
|
-
</output>
|