source_monitor 0.1.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 +7 -0
- data/.gitignore +16 -0
- data/.rubocop.yml +12 -0
- data/.ruby-version +1 -0
- data/AGENTS.md +132 -0
- data/CHANGELOG.md +66 -0
- data/CONTRIBUTING.md +31 -0
- data/Gemfile +30 -0
- data/Gemfile.lock +411 -0
- data/MIT-LICENSE +20 -0
- data/README.md +108 -0
- data/Rakefile +8 -0
- data/app/assets/builds/.keep +0 -0
- data/app/assets/config/source_monitor_manifest.js +4 -0
- data/app/assets/images/source_monitor/.keep +0 -0
- data/app/assets/javascripts/source_monitor/application.js +20 -0
- data/app/assets/javascripts/source_monitor/controllers/async_submit_controller.js +36 -0
- data/app/assets/javascripts/source_monitor/controllers/dropdown_controller.js +109 -0
- data/app/assets/javascripts/source_monitor/controllers/modal_controller.js +56 -0
- data/app/assets/javascripts/source_monitor/controllers/notification_controller.js +53 -0
- data/app/assets/javascripts/source_monitor/turbo_actions.js +13 -0
- data/app/assets/stylesheets/source_monitor/application.tailwind.css +13 -0
- data/app/assets/svgs/source_monitor/.keep +0 -0
- data/app/controllers/concerns/.keep +0 -0
- data/app/controllers/concerns/source_monitor/sanitizes_search_params.rb +81 -0
- data/app/controllers/source_monitor/application_controller.rb +62 -0
- data/app/controllers/source_monitor/dashboard_controller.rb +27 -0
- data/app/controllers/source_monitor/fetch_logs_controller.rb +9 -0
- data/app/controllers/source_monitor/health_controller.rb +10 -0
- data/app/controllers/source_monitor/items_controller.rb +116 -0
- data/app/controllers/source_monitor/logs_controller.rb +15 -0
- data/app/controllers/source_monitor/scrape_logs_controller.rb +9 -0
- data/app/controllers/source_monitor/source_bulk_scrapes_controller.rb +35 -0
- data/app/controllers/source_monitor/source_fetches_controller.rb +22 -0
- data/app/controllers/source_monitor/source_health_checks_controller.rb +34 -0
- data/app/controllers/source_monitor/source_health_resets_controller.rb +27 -0
- data/app/controllers/source_monitor/source_retries_controller.rb +22 -0
- data/app/controllers/source_monitor/source_turbo_responses.rb +115 -0
- data/app/controllers/source_monitor/sources_controller.rb +179 -0
- data/app/helpers/source_monitor/application_helper.rb +327 -0
- data/app/jobs/source_monitor/application_job.rb +13 -0
- data/app/jobs/source_monitor/fetch_feed_job.rb +117 -0
- data/app/jobs/source_monitor/item_cleanup_job.rb +48 -0
- data/app/jobs/source_monitor/log_cleanup_job.rb +47 -0
- data/app/jobs/source_monitor/schedule_fetches_job.rb +29 -0
- data/app/jobs/source_monitor/scrape_item_job.rb +47 -0
- data/app/jobs/source_monitor/source_health_check_job.rb +77 -0
- data/app/mailers/source_monitor/application_mailer.rb +17 -0
- data/app/models/concerns/.keep +0 -0
- data/app/models/concerns/source_monitor/loggable.rb +18 -0
- data/app/models/source_monitor/application_record.rb +5 -0
- data/app/models/source_monitor/fetch_log.rb +31 -0
- data/app/models/source_monitor/health_check_log.rb +28 -0
- data/app/models/source_monitor/item.rb +102 -0
- data/app/models/source_monitor/item_content.rb +11 -0
- data/app/models/source_monitor/log_entry.rb +56 -0
- data/app/models/source_monitor/scrape_log.rb +31 -0
- data/app/models/source_monitor/source.rb +115 -0
- data/app/views/layouts/source_monitor/application.html.erb +54 -0
- data/app/views/source_monitor/dashboard/_fetch_schedule.html.erb +90 -0
- data/app/views/source_monitor/dashboard/_job_metrics.html.erb +82 -0
- data/app/views/source_monitor/dashboard/_recent_activity.html.erb +39 -0
- data/app/views/source_monitor/dashboard/_stat_card.html.erb +6 -0
- data/app/views/source_monitor/dashboard/_stats.html.erb +9 -0
- data/app/views/source_monitor/dashboard/index.html.erb +48 -0
- data/app/views/source_monitor/fetch_logs/show.html.erb +90 -0
- data/app/views/source_monitor/items/_details.html.erb +234 -0
- data/app/views/source_monitor/items/_details_wrapper.html.erb +3 -0
- data/app/views/source_monitor/items/index.html.erb +147 -0
- data/app/views/source_monitor/items/show.html.erb +3 -0
- data/app/views/source_monitor/logs/index.html.erb +208 -0
- data/app/views/source_monitor/scrape_logs/show.html.erb +73 -0
- data/app/views/source_monitor/shared/_toast.html.erb +34 -0
- data/app/views/source_monitor/sources/_bulk_scrape_form.html.erb +64 -0
- data/app/views/source_monitor/sources/_bulk_scrape_modal.html.erb +53 -0
- data/app/views/source_monitor/sources/_details.html.erb +302 -0
- data/app/views/source_monitor/sources/_details_wrapper.html.erb +3 -0
- data/app/views/source_monitor/sources/_empty_state_row.html.erb +5 -0
- data/app/views/source_monitor/sources/_fetch_interval_heatmap.html.erb +46 -0
- data/app/views/source_monitor/sources/_form.html.erb +143 -0
- data/app/views/source_monitor/sources/_health_status_badge.html.erb +46 -0
- data/app/views/source_monitor/sources/_row.html.erb +102 -0
- data/app/views/source_monitor/sources/edit.html.erb +28 -0
- data/app/views/source_monitor/sources/index.html.erb +153 -0
- data/app/views/source_monitor/sources/new.html.erb +22 -0
- data/app/views/source_monitor/sources/show.html.erb +3 -0
- data/config/coverage_baseline.json +2010 -0
- data/config/initializers/feedjira.rb +19 -0
- data/config/routes.rb +18 -0
- data/config/tailwind.config.js +17 -0
- data/db/migrate/20241008120000_create_source_monitor_sources.rb +40 -0
- data/db/migrate/20241008121000_create_source_monitor_items.rb +44 -0
- data/db/migrate/20241008122000_create_source_monitor_fetch_logs.rb +32 -0
- data/db/migrate/20241008123000_create_source_monitor_scrape_logs.rb +25 -0
- data/db/migrate/20251008183000_change_fetch_interval_to_minutes.rb +23 -0
- data/db/migrate/20251009090000_create_source_monitor_item_contents.rb +38 -0
- data/db/migrate/20251009103000_add_feed_content_readability_to_sources.rb +5 -0
- data/db/migrate/20251010090000_add_adaptive_fetching_toggle_to_sources.rb +7 -0
- data/db/migrate/20251010123000_add_deleted_at_to_source_monitor_items.rb +8 -0
- data/db/migrate/20251010153000_add_type_to_source_monitor_sources.rb +8 -0
- data/db/migrate/20251010154500_add_fetch_status_to_source_monitor_sources.rb +9 -0
- data/db/migrate/20251010160000_create_solid_cable_messages.rb +16 -0
- data/db/migrate/20251011090000_add_fetch_retry_state_to_sources.rb +14 -0
- data/db/migrate/20251012090000_add_health_fields_to_sources.rb +17 -0
- data/db/migrate/20251012100000_optimize_source_monitor_database_performance.rb +13 -0
- data/db/migrate/20251014064947_add_not_null_constraints_to_items.rb +30 -0
- data/db/migrate/20251014171659_add_performance_indexes.rb +29 -0
- data/db/migrate/20251014172525_add_fetch_status_check_constraint.rb +18 -0
- data/db/migrate/20251015100000_create_source_monitor_log_entries.rb +89 -0
- data/db/migrate/20251022100000_create_source_monitor_health_check_logs.rb +22 -0
- data/db/migrate/20251108120116_refresh_fetch_status_constraint.rb +29 -0
- data/docs/configuration.md +170 -0
- data/docs/deployment.md +63 -0
- data/docs/gh-cli-workflow.md +44 -0
- data/docs/installation.md +144 -0
- data/docs/troubleshooting.md +76 -0
- data/eslint.config.mjs +27 -0
- data/lib/generators/source_monitor/install/install_generator.rb +59 -0
- data/lib/generators/source_monitor/install/templates/source_monitor.rb.tt +155 -0
- data/lib/source_monitor/analytics/source_activity_rates.rb +53 -0
- data/lib/source_monitor/analytics/source_fetch_interval_distribution.rb +57 -0
- data/lib/source_monitor/analytics/sources_index_metrics.rb +92 -0
- data/lib/source_monitor/assets/bundler.rb +49 -0
- data/lib/source_monitor/assets.rb +6 -0
- data/lib/source_monitor/configuration.rb +654 -0
- data/lib/source_monitor/dashboard/queries.rb +356 -0
- data/lib/source_monitor/dashboard/quick_action.rb +7 -0
- data/lib/source_monitor/dashboard/quick_actions_presenter.rb +26 -0
- data/lib/source_monitor/dashboard/recent_activity.rb +30 -0
- data/lib/source_monitor/dashboard/recent_activity_presenter.rb +77 -0
- data/lib/source_monitor/dashboard/turbo_broadcaster.rb +87 -0
- data/lib/source_monitor/dashboard/upcoming_fetch_schedule.rb +126 -0
- data/lib/source_monitor/engine.rb +107 -0
- data/lib/source_monitor/events.rb +110 -0
- data/lib/source_monitor/feedjira_extensions.rb +103 -0
- data/lib/source_monitor/fetching/advisory_lock.rb +54 -0
- data/lib/source_monitor/fetching/completion/event_publisher.rb +22 -0
- data/lib/source_monitor/fetching/completion/follow_up_handler.rb +37 -0
- data/lib/source_monitor/fetching/completion/retention_handler.rb +30 -0
- data/lib/source_monitor/fetching/feed_fetcher.rb +627 -0
- data/lib/source_monitor/fetching/fetch_error.rb +88 -0
- data/lib/source_monitor/fetching/fetch_runner.rb +142 -0
- data/lib/source_monitor/fetching/retry_policy.rb +85 -0
- data/lib/source_monitor/fetching/stalled_fetch_reconciler.rb +146 -0
- data/lib/source_monitor/health/source_health_check.rb +100 -0
- data/lib/source_monitor/health/source_health_monitor.rb +210 -0
- data/lib/source_monitor/health/source_health_reset.rb +68 -0
- data/lib/source_monitor/health.rb +46 -0
- data/lib/source_monitor/http.rb +85 -0
- data/lib/source_monitor/instrumentation.rb +52 -0
- data/lib/source_monitor/items/item_creator.rb +601 -0
- data/lib/source_monitor/items/retention_pruner.rb +146 -0
- data/lib/source_monitor/items/retention_strategies/destroy.rb +26 -0
- data/lib/source_monitor/items/retention_strategies/soft_delete.rb +50 -0
- data/lib/source_monitor/items/retention_strategies.rb +9 -0
- data/lib/source_monitor/jobs/cleanup_options.rb +85 -0
- data/lib/source_monitor/jobs/fetch_failure_subscriber.rb +129 -0
- data/lib/source_monitor/jobs/solid_queue_metrics.rb +199 -0
- data/lib/source_monitor/jobs/visibility.rb +133 -0
- data/lib/source_monitor/logs/entry_sync.rb +69 -0
- data/lib/source_monitor/logs/filter_set.rb +163 -0
- data/lib/source_monitor/logs/query.rb +81 -0
- data/lib/source_monitor/logs/table_presenter.rb +161 -0
- data/lib/source_monitor/metrics.rb +77 -0
- data/lib/source_monitor/model_extensions.rb +109 -0
- data/lib/source_monitor/models/sanitizable.rb +76 -0
- data/lib/source_monitor/models/url_normalizable.rb +84 -0
- data/lib/source_monitor/pagination/paginator.rb +90 -0
- data/lib/source_monitor/realtime/adapter.rb +97 -0
- data/lib/source_monitor/realtime/broadcaster.rb +237 -0
- data/lib/source_monitor/realtime.rb +17 -0
- data/lib/source_monitor/release/changelog.rb +59 -0
- data/lib/source_monitor/release/runner.rb +73 -0
- data/lib/source_monitor/scheduler.rb +82 -0
- data/lib/source_monitor/scrapers/base.rb +105 -0
- data/lib/source_monitor/scrapers/fetchers/http_fetcher.rb +97 -0
- data/lib/source_monitor/scrapers/parsers/readability_parser.rb +101 -0
- data/lib/source_monitor/scrapers/readability.rb +156 -0
- data/lib/source_monitor/scraping/bulk_result_presenter.rb +85 -0
- data/lib/source_monitor/scraping/bulk_source_scraper.rb +233 -0
- data/lib/source_monitor/scraping/enqueuer.rb +125 -0
- data/lib/source_monitor/scraping/item_scraper/adapter_resolver.rb +44 -0
- data/lib/source_monitor/scraping/item_scraper/persistence.rb +189 -0
- data/lib/source_monitor/scraping/item_scraper.rb +84 -0
- data/lib/source_monitor/scraping/scheduler.rb +43 -0
- data/lib/source_monitor/scraping/state.rb +79 -0
- data/lib/source_monitor/security/authentication.rb +85 -0
- data/lib/source_monitor/security/parameter_sanitizer.rb +42 -0
- data/lib/source_monitor/sources/turbo_stream_presenter.rb +54 -0
- data/lib/source_monitor/turbo_streams/stream_responder.rb +95 -0
- data/lib/source_monitor/version.rb +3 -0
- data/lib/source_monitor.rb +149 -0
- data/lib/tasks/recover_stalled_fetches.rake +16 -0
- data/lib/tasks/source_monitor_assets.rake +28 -0
- data/lib/tasks/source_monitor_tasks.rake +29 -0
- data/lib/tasks/test_smoke.rake +12 -0
- data/package-lock.json +3997 -0
- data/package.json +29 -0
- data/postcss.config.js +6 -0
- data/source_monitor.gemspec +46 -0
- data/stylelint.config.js +12 -0
- metadata +469 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
<% item = local_assigns.fetch(:item) %>
|
|
2
|
+
<% source = item.source %>
|
|
3
|
+
<% manual_scrape_disabled = !source&.scraping_enabled? %>
|
|
4
|
+
<% scrape_badge = item_scrape_status_badge(item: item, source: source) %>
|
|
5
|
+
<% inflight_scrape = %w[pending processing].include?(scrape_badge[:status]) %>
|
|
6
|
+
<% recent_scrape_logs = item.scrape_logs.order(started_at: :desc).limit(5) %>
|
|
7
|
+
<% latest_scrape_log = recent_scrape_logs.first %>
|
|
8
|
+
|
|
9
|
+
<div class="space-y-8">
|
|
10
|
+
<div class="flex flex-col justify-between gap-4 sm:flex-row sm:items-start">
|
|
11
|
+
<div>
|
|
12
|
+
<h1 class="text-3xl font-semibold text-slate-900"><%= item.title.presence || "(untitled)" %></h1>
|
|
13
|
+
<div class="mt-2 flex flex-wrap items-center gap-2 text-xs">
|
|
14
|
+
<span
|
|
15
|
+
data-testid="scrape-status-badge"
|
|
16
|
+
data-status="<%= scrape_badge[:status] %>"
|
|
17
|
+
class="inline-flex items-center gap-1 rounded-full px-3 py-1 font-semibold <%= scrape_badge[:classes] %>"
|
|
18
|
+
>
|
|
19
|
+
<%= loading_spinner_svg if scrape_badge[:show_spinner] %>
|
|
20
|
+
<%= scrape_badge[:label] %>
|
|
21
|
+
</span>
|
|
22
|
+
<% if item.scraped_at.present? %>
|
|
23
|
+
<span class="rounded-full bg-slate-100 px-3 py-1 font-semibold text-slate-600">
|
|
24
|
+
Last scraped <%= time_ago_in_words(item.scraped_at) %> ago
|
|
25
|
+
</span>
|
|
26
|
+
<% else %>
|
|
27
|
+
<span class="rounded-full bg-slate-100 px-3 py-1 font-semibold text-slate-600">
|
|
28
|
+
No scrape recorded
|
|
29
|
+
</span>
|
|
30
|
+
<% end %>
|
|
31
|
+
</div>
|
|
32
|
+
<p class="mt-2 text-sm text-slate-500">
|
|
33
|
+
<% if source %>
|
|
34
|
+
From <%= link_to source.name, source_monitor.source_path(source), class: "text-blue-600 hover:text-blue-500" %>
|
|
35
|
+
<% else %>
|
|
36
|
+
<span class="text-slate-400">No source</span>
|
|
37
|
+
<% end %>
|
|
38
|
+
·
|
|
39
|
+
<%= item.published_at ? item.published_at.strftime("%b %d, %Y %H:%M %Z") : "Unpublished" %>
|
|
40
|
+
</p>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<div class="grid gap-6 xl:grid-cols-3 xl:items-start">
|
|
45
|
+
<div class="space-y-6 xl:col-span-1">
|
|
46
|
+
<div class="rounded-lg border border-slate-200 bg-white shadow-sm">
|
|
47
|
+
<div class="border-b border-slate-200 px-5 py-4">
|
|
48
|
+
<h2 class="text-lg font-medium">Item Details</h2>
|
|
49
|
+
</div>
|
|
50
|
+
<dl class="divide-y divide-slate-100">
|
|
51
|
+
<% categories_list = Array(item.categories).filter_map { |value| value.to_s.strip.presence } %>
|
|
52
|
+
<% tags_list = Array(item.tags).filter_map { |value| value.to_s.strip.presence } %>
|
|
53
|
+
<% details = {
|
|
54
|
+
"GUID" => item.guid,
|
|
55
|
+
"Content Fingerprint" => item.content_fingerprint || "—",
|
|
56
|
+
"URL" => item.url,
|
|
57
|
+
"Canonical URL" => item.canonical_url || "—",
|
|
58
|
+
"Author" => item.author || "—",
|
|
59
|
+
"Published At" => (item.published_at&.strftime("%b %d, %Y %H:%M %Z") || "—"),
|
|
60
|
+
"Updated At (Source)" => (item.updated_at_source&.strftime("%b %d, %Y %H:%M %Z") || "—"),
|
|
61
|
+
"Language" => item.language || "—",
|
|
62
|
+
"Categories" => categories_list.present? ? categories_list.join(", ") : "—",
|
|
63
|
+
"Tags" => tags_list.present? ? tags_list.join(", ") : "—"
|
|
64
|
+
} %>
|
|
65
|
+
<% details.each do |label, value| %>
|
|
66
|
+
<div class="flex items-center justify-between px-5 py-3">
|
|
67
|
+
<dt class="text-sm font-medium text-slate-600"><%= label %></dt>
|
|
68
|
+
<dd class="max-w-[60%] break-words text-sm text-slate-900"><%= value %></dd>
|
|
69
|
+
</div>
|
|
70
|
+
<% end %>
|
|
71
|
+
</dl>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
<div class="rounded-lg border border-slate-200 bg-white shadow-sm">
|
|
75
|
+
<div class="border-b border-slate-200 px-5 py-4">
|
|
76
|
+
<h2 class="text-lg font-medium">Scraping</h2>
|
|
77
|
+
<p class="mt-1 text-xs text-slate-500">Manual results and recent adapter history.</p>
|
|
78
|
+
</div>
|
|
79
|
+
<div class="space-y-4 px-5 py-5 text-sm text-slate-700">
|
|
80
|
+
<div class="grid gap-3 text-xs text-slate-500">
|
|
81
|
+
<div class="flex justify-between">
|
|
82
|
+
<span class="font-medium text-slate-600">Adapter</span>
|
|
83
|
+
<span><%= source&.scraper_adapter || "—" %></span>
|
|
84
|
+
</div>
|
|
85
|
+
<div class="flex justify-between">
|
|
86
|
+
<span class="font-medium text-slate-600">HTTP Status</span>
|
|
87
|
+
<span><%= latest_scrape_log&.http_status || "—" %></span>
|
|
88
|
+
</div>
|
|
89
|
+
<div class="flex justify-between">
|
|
90
|
+
<span class="font-medium text-slate-600">Content Length</span>
|
|
91
|
+
<span><%= latest_scrape_log&.content_length || "—" %></span>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
<% if latest_scrape_log&.error_message.present? %>
|
|
96
|
+
<div class="rounded border border-rose-200 bg-rose-50 px-3 py-2 text-xs text-rose-700">
|
|
97
|
+
<p class="font-semibold">Last Error</p>
|
|
98
|
+
<p class="mt-1"><%= latest_scrape_log.error_message %></p>
|
|
99
|
+
</div>
|
|
100
|
+
<% end %>
|
|
101
|
+
|
|
102
|
+
<% if recent_scrape_logs.any? %>
|
|
103
|
+
<div>
|
|
104
|
+
<p class="text-xs font-semibold tracking-wide text-slate-500">Recent Attempts</p>
|
|
105
|
+
<ul class="mt-2 space-y-2 text-xs text-slate-500">
|
|
106
|
+
<% recent_scrape_logs.first(3).each do |log| %>
|
|
107
|
+
<li class="flex items-center justify-between">
|
|
108
|
+
<span><%= log.started_at&.strftime("%b %d, %H:%M") || "Unknown" %></span>
|
|
109
|
+
<span class="ml-3 inline-flex items-center rounded-full px-2 py-0.5 font-semibold <%= log.success? ? "bg-green-100 text-green-700" : "bg-rose-100 text-rose-700" %>">
|
|
110
|
+
<%= log.success? ? "Success" : "Failure" %>
|
|
111
|
+
</span>
|
|
112
|
+
</li>
|
|
113
|
+
<% end %>
|
|
114
|
+
</ul>
|
|
115
|
+
<%= link_to "View all scrape logs", source_monitor.logs_path(log_type: "scrape", item_id: item.id), class: "mt-3 inline-block text-xs font-medium text-blue-600 hover:text-blue-500" %>
|
|
116
|
+
</div>
|
|
117
|
+
<% else %>
|
|
118
|
+
<p class="text-xs text-slate-500">No scrape history yet.</p>
|
|
119
|
+
<% end %>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<div class="rounded-lg border border-slate-200 bg-white shadow-sm">
|
|
124
|
+
<div class="border-b border-slate-200 px-5 py-4">
|
|
125
|
+
<h2 class="text-lg font-medium">Counts & Metrics</h2>
|
|
126
|
+
</div>
|
|
127
|
+
<div class="space-y-2 px-5 py-4 text-sm text-slate-700">
|
|
128
|
+
<p><span class="font-medium text-slate-600">Comments:</span> <%= item.comments_count || 0 %></p>
|
|
129
|
+
<p><span class="font-medium text-slate-600">Feed Items in Source:</span> <%= source&.items_count || "—" %></p>
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
|
|
133
|
+
<div class="rounded-lg border border-slate-200 bg-white shadow-sm">
|
|
134
|
+
<div class="border-b border-slate-200 px-5 py-4">
|
|
135
|
+
<h2 class="text-lg font-medium">Metadata</h2>
|
|
136
|
+
</div>
|
|
137
|
+
<div class="px-5 py-4 text-sm text-slate-700">
|
|
138
|
+
<% if item.metadata.present? %>
|
|
139
|
+
<pre class="whitespace-pre-wrap break-words text-xs text-slate-600"><%= JSON.pretty_generate(item.metadata) %></pre>
|
|
140
|
+
<% else %>
|
|
141
|
+
<p class="text-slate-500">No metadata recorded.</p>
|
|
142
|
+
<% end %>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
|
|
147
|
+
<div class="space-y-6 xl:col-span-2">
|
|
148
|
+
<div class="rounded-lg border border-slate-200 bg-white shadow-sm">
|
|
149
|
+
<div class="flex flex-col gap-4 border-b border-slate-200 px-5 py-4 sm:flex-row sm:items-center sm:justify-between">
|
|
150
|
+
<div>
|
|
151
|
+
<h2 class="text-lg font-medium">Content Comparison</h2>
|
|
152
|
+
<p class="mt-1 text-xs text-slate-500">Review feed-provided content alongside scraped output.</p>
|
|
153
|
+
</div>
|
|
154
|
+
<div class="flex flex-shrink-0 flex-wrap gap-3">
|
|
155
|
+
<% if item.url.present? %>
|
|
156
|
+
<%= link_to "View Original", item.url, target: "_blank", rel: "noopener", class: "inline-flex items-center rounded-md bg-blue-600 px-3 py-2 text-sm font-medium text-white shadow hover:bg-blue-500" %>
|
|
157
|
+
<% end %>
|
|
158
|
+
<%= button_to "Manual Scrape",
|
|
159
|
+
source_monitor.scrape_item_path(item),
|
|
160
|
+
method: :post,
|
|
161
|
+
class: [
|
|
162
|
+
"inline-flex items-center rounded-md px-3 py-2 text-sm font-medium shadow",
|
|
163
|
+
manual_scrape_disabled || inflight_scrape ? "cursor-not-allowed bg-slate-200 text-slate-500" : "bg-slate-800 text-white hover:bg-slate-700"
|
|
164
|
+
].join(" "),
|
|
165
|
+
disabled: manual_scrape_disabled || inflight_scrape,
|
|
166
|
+
data: {
|
|
167
|
+
"async-submit-target": "button",
|
|
168
|
+
"async-submit-loading-text-value": "Queuing..."
|
|
169
|
+
},
|
|
170
|
+
form: {
|
|
171
|
+
class: "inline",
|
|
172
|
+
data: {
|
|
173
|
+
controller: "async-submit",
|
|
174
|
+
action: "turbo:submit-start->async-submit#start turbo:submit-end->async-submit#finish"
|
|
175
|
+
}
|
|
176
|
+
} %>
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
<div class="grid gap-6 px-5 py-5 text-sm text-slate-700 lg:grid-cols-2">
|
|
180
|
+
<div class="space-y-4">
|
|
181
|
+
<div>
|
|
182
|
+
<h3 class="text-sm font-semibold tracking-wide text-slate-500">Feed Summary</h3>
|
|
183
|
+
<% if item.summary.present? %>
|
|
184
|
+
<div class="mt-2 rounded border border-slate-200 bg-slate-50 p-3 text-slate-700">
|
|
185
|
+
<%= simple_format(item.summary) %>
|
|
186
|
+
</div>
|
|
187
|
+
<% else %>
|
|
188
|
+
<p class="mt-2 text-slate-500">No summary available.</p>
|
|
189
|
+
<% end %>
|
|
190
|
+
</div>
|
|
191
|
+
|
|
192
|
+
<div>
|
|
193
|
+
<h3 class="text-sm font-semibold tracking-wide text-slate-500">Feed Content</h3>
|
|
194
|
+
<% if item.content.present? %>
|
|
195
|
+
<div class="mt-2 rounded border border-slate-200 bg-white p-3 shadow-inner">
|
|
196
|
+
<%= simple_format(item.content) %>
|
|
197
|
+
</div>
|
|
198
|
+
<% else %>
|
|
199
|
+
<p class="mt-2 text-slate-500">No content captured from feed.</p>
|
|
200
|
+
<% end %>
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
|
|
204
|
+
<div class="space-y-4">
|
|
205
|
+
<div>
|
|
206
|
+
<h3 class="text-sm font-semibold tracking-wide text-slate-500">Scraped Content</h3>
|
|
207
|
+
<% if item.scraped_content.present? %>
|
|
208
|
+
<div class="mt-2 rounded border border-emerald-200 bg-emerald-50 p-3 text-slate-700">
|
|
209
|
+
<%= simple_format(item.scraped_content) %>
|
|
210
|
+
</div>
|
|
211
|
+
<% else %>
|
|
212
|
+
<p class="mt-2 text-slate-500">Scraper has not produced any content yet.</p>
|
|
213
|
+
<% end %>
|
|
214
|
+
</div>
|
|
215
|
+
|
|
216
|
+
<div>
|
|
217
|
+
<h3 class="text-sm font-semibold tracking-wide text-slate-500">Scraped HTML</h3>
|
|
218
|
+
<% if item.scraped_html.present? %>
|
|
219
|
+
<details class="mt-2 rounded border border-slate-200 bg-white text-xs shadow-sm">
|
|
220
|
+
<summary class="cursor-pointer px-3 py-2 font-medium text-slate-700">View raw HTML</summary>
|
|
221
|
+
<div class="px-3 pb-3 pt-2">
|
|
222
|
+
<pre class="max-h-96 overflow-auto whitespace-pre-wrap break-words text-slate-600"><%= ERB::Util.h(item.scraped_html) %></pre>
|
|
223
|
+
</div>
|
|
224
|
+
</details>
|
|
225
|
+
<% else %>
|
|
226
|
+
<p class="mt-2 text-slate-500">No scraped HTML available.</p>
|
|
227
|
+
<% end %>
|
|
228
|
+
</div>
|
|
229
|
+
</div>
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
</div>
|
|
233
|
+
</div>
|
|
234
|
+
</div>
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
<div class="space-y-6">
|
|
2
|
+
<div class="flex flex-wrap items-center justify-between gap-3">
|
|
3
|
+
<div>
|
|
4
|
+
<h1 class="text-3xl font-semibold text-slate-900">Items</h1>
|
|
5
|
+
<p class="mt-1 text-sm text-slate-500">Browse recently ingested entries across all sources.</p>
|
|
6
|
+
</div>
|
|
7
|
+
<%= search_form_for @q, url: source_monitor.items_path, method: :get, html: { class: "w-full max-w-sm sm:w-auto", data: { turbo_frame: "source_monitor_items_table" } } do |form| %>
|
|
8
|
+
<%= form.label @search_field, "Search items", class: "sr-only" %>
|
|
9
|
+
<div class="flex rounded-md shadow-sm">
|
|
10
|
+
<%= form.search_field @search_field, placeholder: "Search title, summary, or source…", class: "w-full rounded-l-md border border-slate-200 bg-white px-3 py-2 text-sm text-slate-700 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500" %>
|
|
11
|
+
<%= form.submit "Search", class: "rounded-r-md bg-blue-600 px-4 py-2 text-sm font-semibold text-white hover:bg-blue-500" %>
|
|
12
|
+
</div>
|
|
13
|
+
<% end %>
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<div class="overflow-hidden rounded-lg border border-slate-200 bg-white shadow-sm">
|
|
17
|
+
<%= turbo_frame_tag "source_monitor_items_table" do %>
|
|
18
|
+
<% if @search_term.present? %>
|
|
19
|
+
<div class="rounded-t-lg border-b border-blue-100 bg-blue-50 px-4 py-2 text-xs text-blue-700">
|
|
20
|
+
Showing results for "<%= @search_term %>".
|
|
21
|
+
<%= link_to "Clear search", source_monitor.items_path, class: "ml-2 font-medium text-blue-600 hover:text-blue-500", data: { turbo_frame: "source_monitor_items_table" } %>
|
|
22
|
+
</div>
|
|
23
|
+
<% end %>
|
|
24
|
+
<table class="min-w-full divide-y divide-slate-200 text-left text-sm">
|
|
25
|
+
<thead class="bg-slate-50 text-xs font-semibold uppercase tracking-wide text-slate-500">
|
|
26
|
+
<tr>
|
|
27
|
+
<th scope="col"
|
|
28
|
+
class="px-6 py-3"
|
|
29
|
+
data-sort-column="title"
|
|
30
|
+
aria-sort="<%= table_sort_aria(@q, :title) %>">
|
|
31
|
+
<span class="inline-flex items-center gap-1">
|
|
32
|
+
<%= table_sort_link(
|
|
33
|
+
@q,
|
|
34
|
+
:title,
|
|
35
|
+
"Title",
|
|
36
|
+
frame: "source_monitor_items_table",
|
|
37
|
+
default_order: :asc,
|
|
38
|
+
secondary: ["published_at desc", "created_at desc"],
|
|
39
|
+
html_options: {
|
|
40
|
+
class: "inline-flex items-center gap-1 text-xs font-semibold uppercase tracking-wide text-slate-600 hover:text-slate-900 focus:outline-none"
|
|
41
|
+
}
|
|
42
|
+
) %>
|
|
43
|
+
<span class="text-[11px] text-slate-400" aria-hidden="true"><%= table_sort_arrow(@q, :title) %></span>
|
|
44
|
+
</span>
|
|
45
|
+
</th>
|
|
46
|
+
<th scope="col" class="px-6 py-3">Source</th>
|
|
47
|
+
<th scope="col"
|
|
48
|
+
class="px-6 py-3"
|
|
49
|
+
data-sort-column="published_at"
|
|
50
|
+
aria-sort="<%= table_sort_aria(@q, :published_at) %>">
|
|
51
|
+
<span class="inline-flex items-center gap-1">
|
|
52
|
+
<%= table_sort_link(
|
|
53
|
+
@q,
|
|
54
|
+
:published_at,
|
|
55
|
+
"Published",
|
|
56
|
+
frame: "source_monitor_items_table",
|
|
57
|
+
default_order: :desc,
|
|
58
|
+
secondary: ["created_at desc"],
|
|
59
|
+
html_options: {
|
|
60
|
+
class: "inline-flex items-center gap-1 text-xs font-semibold uppercase tracking-wide text-slate-600 hover:text-slate-900 focus:outline-none"
|
|
61
|
+
}
|
|
62
|
+
) %>
|
|
63
|
+
<span class="text-[11px] text-slate-400" aria-hidden="true"><%= table_sort_arrow(@q, :published_at) %></span>
|
|
64
|
+
</span>
|
|
65
|
+
</th>
|
|
66
|
+
<th scope="col" class="px-6 py-3">Scrape Status</th>
|
|
67
|
+
</tr>
|
|
68
|
+
</thead>
|
|
69
|
+
<tbody class="divide-y divide-slate-100 text-slate-700">
|
|
70
|
+
<% @items.each do |item| %>
|
|
71
|
+
<tr class="hover:bg-slate-50">
|
|
72
|
+
<td class="px-6 py-4">
|
|
73
|
+
<div class="font-medium text-slate-900">
|
|
74
|
+
<%= link_to item.title.presence || "(untitled)",
|
|
75
|
+
source_monitor.item_path(item),
|
|
76
|
+
class: "hover:text-blue-600",
|
|
77
|
+
data: { turbo_frame: "_top" } %>
|
|
78
|
+
</div>
|
|
79
|
+
<% if item.summary.present? %>
|
|
80
|
+
<div class="mt-1 text-xs text-slate-500"><%= item.summary %></div>
|
|
81
|
+
<% end %>
|
|
82
|
+
</td>
|
|
83
|
+
<td class="px-6 py-4 text-sm">
|
|
84
|
+
<% if item.source %>
|
|
85
|
+
<%= link_to item.source.name,
|
|
86
|
+
source_monitor.source_path(item.source),
|
|
87
|
+
class: "text-blue-600 hover:text-blue-500",
|
|
88
|
+
data: { turbo_frame: "_top" } %>
|
|
89
|
+
<% else %>
|
|
90
|
+
<span class="text-slate-400">—</span>
|
|
91
|
+
<% end %>
|
|
92
|
+
</td>
|
|
93
|
+
<td class="px-6 py-4 text-xs text-slate-500">
|
|
94
|
+
<%= item.published_at ? item.published_at.strftime("%b %d, %Y %H:%M") : "Unpublished" %>
|
|
95
|
+
</td>
|
|
96
|
+
<td class="px-6 py-4 text-xs">
|
|
97
|
+
<% status_label, status_classes =
|
|
98
|
+
case item.scrape_status
|
|
99
|
+
when "success"
|
|
100
|
+
["Scraped", "bg-green-100 text-green-700"]
|
|
101
|
+
when "failed"
|
|
102
|
+
["Failed", "bg-rose-100 text-rose-700"]
|
|
103
|
+
when "pending"
|
|
104
|
+
["Pending", "bg-amber-100 text-amber-700"]
|
|
105
|
+
else
|
|
106
|
+
["Not Scraped", "bg-slate-100 text-slate-600"]
|
|
107
|
+
end %>
|
|
108
|
+
<span class="inline-flex items-center rounded-full px-3 py-1 font-semibold <%= status_classes %>"><%= status_label %></span>
|
|
109
|
+
</td>
|
|
110
|
+
</tr>
|
|
111
|
+
<% end %>
|
|
112
|
+
|
|
113
|
+
<% if @items.blank? %>
|
|
114
|
+
<tr>
|
|
115
|
+
<td colspan="4" class="px-6 py-6 text-center text-sm text-slate-500">
|
|
116
|
+
No items found.
|
|
117
|
+
</td>
|
|
118
|
+
</tr>
|
|
119
|
+
<% end %>
|
|
120
|
+
</tbody>
|
|
121
|
+
</table>
|
|
122
|
+
<div class="flex flex-col items-center gap-3 border-t border-slate-200 px-6 py-4 sm:flex-row sm:justify-between">
|
|
123
|
+
<div class="text-xs text-slate-500">
|
|
124
|
+
Page <%= @page %>
|
|
125
|
+
</div>
|
|
126
|
+
<div class="flex gap-2">
|
|
127
|
+
<% prev_params = { page: @page - 1 } %>
|
|
128
|
+
<% prev_params[:q] = @search_params if @search_params.present? %>
|
|
129
|
+
<% next_params = { page: @page + 1 } %>
|
|
130
|
+
<% next_params[:q] = @search_params if @search_params.present? %>
|
|
131
|
+
|
|
132
|
+
<% if @has_previous_page %>
|
|
133
|
+
<%= link_to "Previous", source_monitor.items_path(prev_params), class: "inline-flex items-center rounded-md border border-slate-300 px-3 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50", data: { turbo_frame: "source_monitor_items_table" } %>
|
|
134
|
+
<% else %>
|
|
135
|
+
<span class="inline-flex items-center rounded-md border border-slate-200 px-3 py-2 text-sm font-medium text-slate-300">Previous</span>
|
|
136
|
+
<% end %>
|
|
137
|
+
|
|
138
|
+
<% if @has_next_page %>
|
|
139
|
+
<%= link_to "Next", source_monitor.items_path(next_params), class: "inline-flex items-center rounded-md border border-slate-300 px-3 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50", data: { turbo_frame: "source_monitor_items_table" } %>
|
|
140
|
+
<% else %>
|
|
141
|
+
<span class="inline-flex items-center rounded-md border border-slate-200 px-3 py-2 text-sm font-medium text-slate-300">Next</span>
|
|
142
|
+
<% end %>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
<% end %>
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
<div class="space-y-6">
|
|
2
|
+
<div class="flex flex-wrap items-center justify-between gap-3">
|
|
3
|
+
<div>
|
|
4
|
+
<h1 class="text-3xl font-semibold text-slate-900">Logs</h1>
|
|
5
|
+
<p class="mt-1 text-sm text-slate-500">Review recent fetch, scrape, and health check activity in a single view.</p>
|
|
6
|
+
</div>
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
<% base_params = @filter_params.except(:page) %>
|
|
10
|
+
|
|
11
|
+
<div class="flex flex-wrap items-center gap-3">
|
|
12
|
+
<div class="flex overflow-hidden rounded-md border border-slate-200">
|
|
13
|
+
<% status_options = [
|
|
14
|
+
["All", nil],
|
|
15
|
+
["Successes", "success"],
|
|
16
|
+
["Failures", "failed"]
|
|
17
|
+
] %>
|
|
18
|
+
<% status_options.each do |label, value| %>
|
|
19
|
+
<% params_for_status = base_params.merge(status: value, page: nil).compact %>
|
|
20
|
+
<% active = (@filter_set.status == value) || (@filter_set.status.nil? && value.nil?) %>
|
|
21
|
+
<%= link_to label,
|
|
22
|
+
source_monitor.logs_path(params_for_status),
|
|
23
|
+
class: [
|
|
24
|
+
"px-4 py-2 text-sm font-medium border-slate-200",
|
|
25
|
+
active ? "bg-slate-800 text-white" : "bg-white text-slate-600 hover:bg-slate-50",
|
|
26
|
+
value.present? ? "border-l" : ""
|
|
27
|
+
].join(" ") %>
|
|
28
|
+
<% end %>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<div class="flex overflow-hidden rounded-md border border-slate-200">
|
|
32
|
+
<% type_options = [
|
|
33
|
+
["All Logs", nil],
|
|
34
|
+
["Fetch Logs", "fetch"],
|
|
35
|
+
["Scrape Logs", "scrape"],
|
|
36
|
+
["Health Checks", "health_check"]
|
|
37
|
+
] %>
|
|
38
|
+
<% type_options.each do |label, value| %>
|
|
39
|
+
<% params_for_type = base_params.merge(log_type: value, page: nil).compact %>
|
|
40
|
+
<% active = (@filter_set.log_type == value) || (@filter_set.log_type.nil? && value.nil?) %>
|
|
41
|
+
<%= link_to label,
|
|
42
|
+
source_monitor.logs_path(params_for_type),
|
|
43
|
+
class: [
|
|
44
|
+
"px-4 py-2 text-sm font-medium border-slate-200",
|
|
45
|
+
active ? "bg-slate-800 text-white" : "bg-white text-slate-600 hover:bg-slate-50",
|
|
46
|
+
value.present? ? "border-l" : ""
|
|
47
|
+
].join(" ") %>
|
|
48
|
+
<% end %>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<%= form_with url: source_monitor.logs_path, method: :get, local: true, html: { class: "rounded-lg border border-slate-200 bg-white p-4 shadow-sm" } do |form| %>
|
|
53
|
+
<%= form.hidden_field :status, value: @filter_set.status %>
|
|
54
|
+
<%= form.hidden_field :log_type, value: @filter_set.log_type %>
|
|
55
|
+
|
|
56
|
+
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
|
57
|
+
<div class="flex flex-col">
|
|
58
|
+
<%= form.label :search, "Search logs", class: "text-xs font-semibold uppercase tracking-wide text-slate-500" %>
|
|
59
|
+
<%= form.text_field :search,
|
|
60
|
+
value: @filter_set.search,
|
|
61
|
+
placeholder: "Error message, source, adapter, HTTP status…",
|
|
62
|
+
class: "mt-1 rounded-md border border-slate-300 px-3 py-2 text-sm shadow-sm focus:border-slate-500 focus:outline-none focus:ring-2 focus:ring-slate-200" %>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<div class="flex flex-col">
|
|
66
|
+
<%= form.label :timeframe, "Timeframe", class: "text-xs font-semibold uppercase tracking-wide text-slate-500" %>
|
|
67
|
+
<%= form.select :timeframe,
|
|
68
|
+
options_for_select([["All time", nil], ["Last 24 hours", "24h"], ["Last 7 days", "7d"], ["Last 30 days", "30d"]], @filter_set.timeframe),
|
|
69
|
+
{}, class: "mt-1 rounded-md border border-slate-300 px-3 py-2 text-sm shadow-sm focus:border-slate-500 focus:outline-none focus:ring-2 focus:ring-slate-200" %>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
<div class="flex flex-col">
|
|
73
|
+
<%= form.label :started_after, "Started after", class: "text-xs font-semibold uppercase tracking-wide text-slate-500" %>
|
|
74
|
+
<%= form.datetime_field :started_after,
|
|
75
|
+
value: @filter_set.started_after&.strftime("%Y-%m-%dT%H:%M"),
|
|
76
|
+
class: "mt-1 rounded-md border border-slate-300 px-3 py-2 text-sm shadow-sm focus:border-slate-500 focus:outline-none focus:ring-2 focus:ring-slate-200" %>
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
<div class="flex flex-col">
|
|
80
|
+
<%= form.label :started_before, "Started before", class: "text-xs font-semibold uppercase tracking-wide text-slate-500" %>
|
|
81
|
+
<%= form.datetime_field :started_before,
|
|
82
|
+
value: @filter_set.started_before&.strftime("%Y-%m-%dT%H:%M"),
|
|
83
|
+
class: "mt-1 rounded-md border border-slate-300 px-3 py-2 text-sm shadow-sm focus:border-slate-500 focus:outline-none focus:ring-2 focus:ring-slate-200" %>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
<div class="flex flex-col">
|
|
87
|
+
<%= form.label :source_id, "Source ID", class: "text-xs font-semibold uppercase tracking-wide text-slate-500" %>
|
|
88
|
+
<%= form.number_field :source_id,
|
|
89
|
+
value: @filter_set.source_id,
|
|
90
|
+
class: "mt-1 rounded-md border border-slate-300 px-3 py-2 text-sm shadow-sm focus:border-slate-500 focus:outline-none focus:ring-2 focus:ring-slate-200" %>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
<div class="flex flex-col">
|
|
94
|
+
<%= form.label :item_id, "Item ID", class: "text-xs font-semibold uppercase tracking-wide text-slate-500" %>
|
|
95
|
+
<%= form.number_field :item_id,
|
|
96
|
+
value: @filter_set.item_id,
|
|
97
|
+
class: "mt-1 rounded-md border border-slate-300 px-3 py-2 text-sm shadow-sm focus:border-slate-500 focus:outline-none focus:ring-2 focus:ring-slate-200" %>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
<div class="mt-4 flex flex-wrap items-center gap-3">
|
|
102
|
+
<%= form.submit "Search", class: "inline-flex items-center rounded-md bg-slate-800 px-4 py-2 text-sm font-medium text-white hover:bg-slate-700" %>
|
|
103
|
+
<%= link_to "Clear", source_monitor.logs_path, class: "text-sm font-medium text-blue-600 hover:text-blue-500" %>
|
|
104
|
+
</div>
|
|
105
|
+
<% end %>
|
|
106
|
+
|
|
107
|
+
<div class="overflow-hidden rounded-lg border border-slate-200 bg-white shadow-sm" data-testid="logs-table">
|
|
108
|
+
<table class="min-w-full divide-y divide-slate-200 text-left text-sm">
|
|
109
|
+
<thead class="bg-slate-50 text-xs font-semibold uppercase tracking-wide text-slate-500">
|
|
110
|
+
<tr>
|
|
111
|
+
<th scope="col" class="px-6 py-3">Started</th>
|
|
112
|
+
<th scope="col" class="px-6 py-3">Type</th>
|
|
113
|
+
<th scope="col" class="px-6 py-3">Subject</th>
|
|
114
|
+
<th scope="col" class="px-6 py-3">Source</th>
|
|
115
|
+
<th scope="col" class="px-6 py-3">HTTP / Adapter</th>
|
|
116
|
+
<th scope="col" class="px-6 py-3">Result</th>
|
|
117
|
+
<th scope="col" class="px-6 py-3">Metrics</th>
|
|
118
|
+
<th scope="col" class="px-6 py-3 text-right"></th>
|
|
119
|
+
</tr>
|
|
120
|
+
</thead>
|
|
121
|
+
<tbody class="divide-y divide-slate-100 text-slate-700">
|
|
122
|
+
<% @rows.each do |row| %>
|
|
123
|
+
<tr class="hover:bg-slate-50" data-log-row="<%= row.dom_id %>">
|
|
124
|
+
<td class="px-6 py-4 text-xs text-slate-500">
|
|
125
|
+
<%= row.started_at&.strftime("%b %d, %Y %H:%M:%S") || "Unknown" %>
|
|
126
|
+
</td>
|
|
127
|
+
<td class="px-6 py-4 text-sm">
|
|
128
|
+
<% type_classes = row.fetch? ? "bg-slate-200 text-slate-800" : "bg-blue-100 text-blue-700" %>
|
|
129
|
+
<span class="inline-flex items-center rounded-full px-3 py-1 text-xs font-semibold <%= type_classes %>"><%= row.type_label %></span>
|
|
130
|
+
</td>
|
|
131
|
+
<td class="px-6 py-4 text-sm">
|
|
132
|
+
<% if row.primary_path %>
|
|
133
|
+
<%= link_to row.primary_label, row.primary_path, class: "text-blue-600 hover:text-blue-500" %>
|
|
134
|
+
<% else %>
|
|
135
|
+
<%= row.primary_label %>
|
|
136
|
+
<% end %>
|
|
137
|
+
</td>
|
|
138
|
+
<td class="px-6 py-4 text-sm">
|
|
139
|
+
<% if row.source_path %>
|
|
140
|
+
<%= link_to row.source_label, row.source_path, class: "text-blue-600 hover:text-blue-500" %>
|
|
141
|
+
<% else %>
|
|
142
|
+
<%= row.source_label || "—" %>
|
|
143
|
+
<% end %>
|
|
144
|
+
</td>
|
|
145
|
+
<td class="px-6 py-4 text-sm">
|
|
146
|
+
<span class="font-medium"><%= row.http_summary %></span>
|
|
147
|
+
</td>
|
|
148
|
+
<td class="px-6 py-4 text-sm">
|
|
149
|
+
<% status_classes = row.success? ? "bg-green-100 text-green-700" : "bg-rose-100 text-rose-700" %>
|
|
150
|
+
<span class="inline-flex items-center rounded-full px-3 py-1 text-xs font-semibold <%= status_classes %>"><%= row.status_label %></span>
|
|
151
|
+
<% if row.error_message.present? %>
|
|
152
|
+
<p class="mt-2 text-xs text-slate-500"><%= row.error_message %></p>
|
|
153
|
+
<% end %>
|
|
154
|
+
</td>
|
|
155
|
+
<td class="px-6 py-4 text-xs text-slate-500">
|
|
156
|
+
<%= row.metrics_summary %>
|
|
157
|
+
</td>
|
|
158
|
+
<td class="px-6 py-4 text-right text-sm">
|
|
159
|
+
<% if row.detail_path %>
|
|
160
|
+
<%= link_to "View Details", row.detail_path, class: "text-blue-600 hover:text-blue-500" %>
|
|
161
|
+
<% else %>
|
|
162
|
+
—
|
|
163
|
+
<% end %>
|
|
164
|
+
</td>
|
|
165
|
+
</tr>
|
|
166
|
+
<% end %>
|
|
167
|
+
|
|
168
|
+
<% if @rows.blank? %>
|
|
169
|
+
<tr>
|
|
170
|
+
<td colspan="8" class="px-6 py-6 text-center text-sm text-slate-500">
|
|
171
|
+
No logs recorded yet.
|
|
172
|
+
</td>
|
|
173
|
+
</tr>
|
|
174
|
+
<% end %>
|
|
175
|
+
</tbody>
|
|
176
|
+
</table>
|
|
177
|
+
</div>
|
|
178
|
+
|
|
179
|
+
<div class="flex flex-wrap items-center justify-between gap-3">
|
|
180
|
+
<div class="text-xs text-slate-500" data-page-indicator="<%= @query_result.page %>">
|
|
181
|
+
Page <%= @query_result.page %>
|
|
182
|
+
</div>
|
|
183
|
+
<div class="flex gap-2">
|
|
184
|
+
<% prev_page = @query_result.has_previous_page ? @query_result.page - 1 : @query_result.page %>
|
|
185
|
+
<% next_page = @query_result.has_next_page ? @query_result.page + 1 : @query_result.page %>
|
|
186
|
+
<% prev_params = @filter_params.merge(page: prev_page).compact %>
|
|
187
|
+
<% next_params = @filter_params.merge(page: next_page).compact %>
|
|
188
|
+
<%= link_to "Previous",
|
|
189
|
+
source_monitor.logs_path(prev_params),
|
|
190
|
+
class: [
|
|
191
|
+
"inline-flex items-center rounded-md border px-3 py-1 text-sm font-medium",
|
|
192
|
+
@query_result.has_previous_page ? "border-slate-300 text-slate-700 hover:bg-slate-50" : "border-slate-200 text-slate-300 cursor-not-allowed"
|
|
193
|
+
].join(" "),
|
|
194
|
+
aria: { disabled: !@query_result.has_previous_page } %>
|
|
195
|
+
<%= link_to "Next",
|
|
196
|
+
source_monitor.logs_path(next_params),
|
|
197
|
+
class: [
|
|
198
|
+
"inline-flex items-center rounded-md border px-3 py-1 text-sm font-medium",
|
|
199
|
+
@query_result.has_next_page ? "border-slate-300 text-slate-700 hover:bg-slate-50" : "border-slate-200 text-slate-300 cursor-not-allowed"
|
|
200
|
+
].join(" "),
|
|
201
|
+
aria: { disabled: !@query_result.has_next_page } %>
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
|
|
205
|
+
<p class="text-xs text-slate-500">
|
|
206
|
+
Showing up to <%= @query_result.per_page %> logs per page.
|
|
207
|
+
</p>
|
|
208
|
+
</div>
|