source_monitor 0.11.1 → 0.12.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.
Files changed (115) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/commands/rails-audit.md +77 -0
  3. data/CHANGELOG.md +50 -0
  4. data/CLAUDE.md +2 -2
  5. data/Gemfile.lock +7 -20
  6. data/RAILS_AUDIT.md +424 -0
  7. data/VERSION +1 -1
  8. data/app/assets/builds/source_monitor/application.css +4 -24
  9. data/app/assets/builds/source_monitor/application.js +57 -89
  10. data/app/assets/builds/source_monitor/application.js.map +4 -4
  11. data/app/assets/javascripts/source_monitor/application.js +3 -6
  12. data/app/assets/javascripts/source_monitor/controllers/dropdown_controller.js +6 -86
  13. data/app/assets/javascripts/source_monitor/controllers/filter_submit_controller.js +13 -0
  14. data/app/assets/javascripts/source_monitor/controllers/modal_controller.js +56 -0
  15. data/app/assets/javascripts/source_monitor/controllers/notification_controller.js +3 -13
  16. data/app/components/source_monitor/application_component.rb +10 -0
  17. data/app/components/source_monitor/filter_dropdown_component.rb +62 -0
  18. data/app/components/source_monitor/icon_component.rb +140 -0
  19. data/app/components/source_monitor/status_badge_component.html.erb +8 -0
  20. data/app/components/source_monitor/status_badge_component.rb +96 -0
  21. data/app/controllers/concerns/source_monitor/sanitizes_search_params.rb +4 -0
  22. data/app/controllers/concerns/source_monitor/set_source.rb +13 -0
  23. data/app/controllers/source_monitor/application_controller.rb +17 -0
  24. data/app/controllers/source_monitor/bulk_scrape_enablements_controller.rb +6 -10
  25. data/app/controllers/source_monitor/dashboard_controller.rb +5 -1
  26. data/app/controllers/source_monitor/import_history_dismissals_controller.rb +1 -1
  27. data/app/controllers/source_monitor/import_sessions_controller.rb +30 -9
  28. data/app/controllers/source_monitor/item_scrapes_controller.rb +70 -0
  29. data/app/controllers/source_monitor/items_controller.rb +2 -69
  30. data/app/controllers/source_monitor/source_bulk_scrapes_controller.rb +1 -4
  31. data/app/controllers/source_monitor/source_favicon_fetches_controller.rb +2 -12
  32. data/app/controllers/source_monitor/source_fetches_controller.rb +1 -6
  33. data/app/controllers/source_monitor/source_health_checks_controller.rb +9 -16
  34. data/app/controllers/source_monitor/source_health_resets_controller.rb +1 -6
  35. data/app/controllers/source_monitor/source_retries_controller.rb +1 -6
  36. data/app/controllers/source_monitor/source_scrape_tests_controller.rb +2 -4
  37. data/app/controllers/source_monitor/source_turbo_responses.rb +1 -3
  38. data/app/controllers/source_monitor/sources_controller.rb +15 -20
  39. data/app/helpers/source_monitor/application_helper.rb +15 -31
  40. data/app/helpers/source_monitor/health_badge_helper.rb +8 -0
  41. data/app/jobs/source_monitor/download_content_images_job.rb +1 -59
  42. data/app/jobs/source_monitor/favicon_fetch_job.rb +1 -58
  43. data/app/jobs/source_monitor/fetch_feed_job.rb +2 -52
  44. data/app/jobs/source_monitor/import_opml_job.rb +6 -145
  45. data/app/jobs/source_monitor/import_session_health_check_job.rb +15 -76
  46. data/app/jobs/source_monitor/item_cleanup_job.rb +5 -0
  47. data/app/jobs/source_monitor/log_cleanup_job.rb +13 -2
  48. data/app/jobs/source_monitor/schedule_fetches_job.rb +8 -0
  49. data/app/jobs/source_monitor/scrape_item_job.rb +6 -52
  50. data/app/jobs/source_monitor/source_health_check_job.rb +1 -72
  51. data/app/models/concerns/source_monitor/loggable.rb +12 -0
  52. data/app/models/source_monitor/fetch_log.rb +0 -8
  53. data/app/models/source_monitor/health_check_log.rb +0 -8
  54. data/app/models/source_monitor/import_history.rb +14 -0
  55. data/app/models/source_monitor/import_session.rb +2 -0
  56. data/app/models/source_monitor/item.rb +15 -0
  57. data/app/models/source_monitor/item_content.rb +4 -3
  58. data/app/models/source_monitor/scrape_log.rb +4 -6
  59. data/app/models/source_monitor/source.rb +28 -19
  60. data/app/presenters/source_monitor/base_presenter.rb +19 -0
  61. data/app/presenters/source_monitor/source_details_presenter.rb +61 -0
  62. data/app/presenters/source_monitor/sources_filter_presenter.rb +61 -0
  63. data/app/views/source_monitor/dashboard/_recent_activity.html.erb +3 -3
  64. data/app/views/source_monitor/dashboard/_stat_card.html.erb +2 -1
  65. data/app/views/source_monitor/dashboard/_stats.html.erb +5 -7
  66. data/app/views/source_monitor/items/_details.html.erb +11 -14
  67. data/app/views/source_monitor/items/index.html.erb +10 -35
  68. data/app/views/source_monitor/logs/index.html.erb +20 -41
  69. data/app/views/source_monitor/shared/_form_errors.html.erb +14 -0
  70. data/app/views/source_monitor/source_scrape_tests/_result.html.erb +1 -29
  71. data/app/views/source_monitor/source_scrape_tests/_result_content.html.erb +33 -0
  72. data/app/views/source_monitor/source_scrape_tests/show.html.erb +1 -29
  73. data/app/views/source_monitor/sources/_bulk_scrape_enable_modal.html.erb +2 -2
  74. data/app/views/source_monitor/sources/_bulk_scrape_modal.html.erb +7 -5
  75. data/app/views/source_monitor/sources/_details.html.erb +24 -52
  76. data/app/views/source_monitor/sources/_health_status_badge.html.erb +4 -6
  77. data/app/views/source_monitor/sources/_row.html.erb +7 -18
  78. data/app/views/source_monitor/sources/edit.html.erb +1 -10
  79. data/app/views/source_monitor/sources/index.html.erb +26 -46
  80. data/app/views/source_monitor/sources/new.html.erb +1 -10
  81. data/config/routes.rb +1 -1
  82. data/db/migrate/20260313120000_add_composite_indexes_to_log_tables.rb +14 -0
  83. data/db/migrate/20260314120000_align_health_status_default.rb +11 -0
  84. data/lib/source_monitor/analytics/sources_index_metrics.rb +15 -0
  85. data/lib/source_monitor/dashboard/queries/recent_activity_query.rb +10 -4
  86. data/lib/source_monitor/dashboard/turbo_broadcaster.rb +21 -5
  87. data/lib/source_monitor/favicons/fetcher.rb +86 -0
  88. data/lib/source_monitor/fetching/cloudflare_bypass.rb +14 -5
  89. data/lib/source_monitor/fetching/completion/event_publisher.rb +12 -0
  90. data/lib/source_monitor/fetching/completion/follow_up_handler.rb +15 -2
  91. data/lib/source_monitor/fetching/completion/retention_handler.rb +11 -3
  92. data/lib/source_monitor/fetching/feed_fetcher.rb +2 -21
  93. data/lib/source_monitor/fetching/fetch_runner.rb +12 -3
  94. data/lib/source_monitor/fetching/retry_orchestrator.rb +102 -0
  95. data/lib/source_monitor/fetching/stalled_fetch_reconciler.rb +9 -0
  96. data/lib/source_monitor/health/source_health_check_orchestrator.rb +95 -0
  97. data/lib/source_monitor/health.rb +1 -0
  98. data/lib/source_monitor/images/downloader.rb +6 -7
  99. data/lib/source_monitor/images/processor.rb +98 -0
  100. data/lib/source_monitor/import_sessions/health_check_updater.rb +95 -0
  101. data/lib/source_monitor/import_sessions/opml_importer.rb +163 -0
  102. data/lib/source_monitor/items/item_creator.rb +0 -21
  103. data/lib/source_monitor/logs/query.rb +20 -0
  104. data/lib/source_monitor/queries/scrape_candidates_query.rb +30 -0
  105. data/lib/source_monitor/queries.rb +7 -0
  106. data/lib/source_monitor/scheduler.rb +5 -0
  107. data/lib/source_monitor/scraping/bulk_result_presenter.rb +11 -8
  108. data/lib/source_monitor/scraping/runner.rb +52 -0
  109. data/lib/source_monitor/scraping/scheduler.rb +5 -0
  110. data/lib/source_monitor/scraping/state.rb +4 -2
  111. data/lib/source_monitor/security/parameter_sanitizer.rb +7 -0
  112. data/lib/source_monitor/version.rb +1 -1
  113. data/lib/source_monitor.rb +7 -0
  114. data/source_monitor.gemspec +1 -0
  115. metadata +47 -1
@@ -626,6 +626,10 @@ video {
626
626
  pointer-events: auto;
627
627
  }
628
628
 
629
+ .fm-admin .visible {
630
+ visibility: visible;
631
+ }
632
+
629
633
  .fm-admin .static {
630
634
  position: static;
631
635
  }
@@ -705,10 +709,6 @@ video {
705
709
  margin-bottom: 0.25rem;
706
710
  }
707
711
 
708
- .fm-admin .mb-1 {
709
- margin-bottom: 0.25rem;
710
- }
711
-
712
712
  .fm-admin .ml-0\.5 {
713
713
  margin-left: 0.125rem;
714
714
  }
@@ -717,14 +717,6 @@ video {
717
717
  margin-left: 0.5rem;
718
718
  }
719
719
 
720
- .fm-admin .ml-3 {
721
- margin-left: 0.75rem;
722
- }
723
-
724
- .fm-admin .ml-4 {
725
- margin-left: 1rem;
726
- }
727
-
728
720
  .fm-admin .mr-1 {
729
721
  margin-right: 0.25rem;
730
722
  }
@@ -809,10 +801,6 @@ video {
809
801
  height: 1rem;
810
802
  }
811
803
 
812
- .fm-admin .h-5 {
813
- height: 1.25rem;
814
- }
815
-
816
804
  .fm-admin .h-8 {
817
805
  height: 2rem;
818
806
  }
@@ -861,10 +849,6 @@ video {
861
849
  width: 10rem;
862
850
  }
863
851
 
864
- .fm-admin .w-5 {
865
- width: 1.25rem;
866
- }
867
-
868
852
  .fm-admin .w-8 {
869
853
  width: 2rem;
870
854
  }
@@ -1885,10 +1869,6 @@ video {
1885
1869
  opacity: 0;
1886
1870
  }
1887
1871
 
1888
- .fm-admin .opacity-25 {
1889
- opacity: 0.25;
1890
- }
1891
-
1892
1872
  .fm-admin .opacity-60 {
1893
1873
  opacity: 0.6;
1894
1874
  }
@@ -2483,11 +2483,7 @@ var notification_controller_default = class extends Controller {
2483
2483
  delay: { default: 5e3, type: Number }
2484
2484
  };
2485
2485
  connect() {
2486
- if (!window.SourceMonitorControllers) {
2487
- window.SourceMonitorControllers = {};
2488
- }
2489
2486
  this.clearTimeout();
2490
- this.registerController();
2491
2487
  this.applyLevelDelay();
2492
2488
  this.startTimer();
2493
2489
  }
@@ -2499,18 +2495,11 @@ var notification_controller_default = class extends Controller {
2499
2495
  this.clearTimeout();
2500
2496
  this.dismiss();
2501
2497
  }
2502
- registerController() {
2503
- window.SourceMonitorControllers.notification = this;
2504
- }
2505
2498
  startTimer() {
2506
2499
  if (this.delayValue <= 0) return;
2507
2500
  this.timeoutId = window.setTimeout(() => this.dismiss(), this.delayValue);
2508
2501
  }
2509
2502
  applyLevelDelay() {
2510
- const level = this.element.dataset.level;
2511
- if (level === "error" && this.delayValue === 5e3) {
2512
- this.delayValue = 1e4;
2513
- }
2514
2503
  }
2515
2504
  dismiss() {
2516
2505
  if (!this.element) return;
@@ -2651,92 +2640,25 @@ var notification_container_controller_default = class extends Controller {
2651
2640
  var dropdown_controller_default = class extends Controller {
2652
2641
  static targets = ["menu"];
2653
2642
  static values = {
2654
- transitionModule: { type: String, default: "stimulus-use" },
2655
2643
  hiddenClass: { type: String, default: "hidden" }
2656
2644
  };
2657
2645
  connect() {
2658
- this.element.dataset.dropdownState = "initializing";
2659
- this.transitionEnabled = false;
2660
- if (typeof this.toggleTransition !== "function") {
2661
- this.toggleTransition = this.toggleVisibility.bind(this);
2662
- }
2663
- if (typeof this.leave !== "function") {
2664
- this.leave = this.hideMenu.bind(this);
2665
- }
2666
- this.loadTransitions().catch(() => null).finally(() => {
2667
- this.element.dataset.dropdownState = "ready";
2668
- });
2646
+ this._hideOnClickOutside = this.hide.bind(this);
2647
+ document.addEventListener("click", this._hideOnClickOutside);
2669
2648
  }
2670
2649
  disconnect() {
2671
- delete this.element.dataset.dropdownState;
2672
- }
2673
- // Dynamic import provides progressive enhancement: smooth transitions when stimulus-use
2674
- // is available, graceful fallback to CSS class toggling when not. This complexity is
2675
- // justified as it allows the engine to work without requiring stimulus-use as a dependency.
2676
- // Evaluated for simplification in Phase 20.05.07 - Decision: Keep current implementation.
2677
- async loadTransitions() {
2678
- if (!this.hasMenuTarget || this.transitionModuleValue === "") {
2679
- this.logFallback();
2680
- return;
2681
- }
2682
- try {
2683
- const module = await import(this.transitionModuleValue);
2684
- const useTransition = module?.useTransition || module?.default?.useTransition;
2685
- if (typeof useTransition === "function") {
2686
- useTransition(this, {
2687
- element: this.menuTarget,
2688
- hiddenClass: this.hiddenClassValue
2689
- });
2690
- this.transitionEnabled = true;
2691
- } else {
2692
- this.logFallback();
2693
- }
2694
- } catch (error2) {
2695
- this.logFallback(error2);
2696
- }
2650
+ document.removeEventListener("click", this._hideOnClickOutside);
2697
2651
  }
2698
2652
  toggle(event) {
2699
- if (this.transitionEnabled && typeof this.toggleTransition === "function") {
2700
- this.toggleTransition();
2701
- } else {
2702
- this.toggleVisibility();
2703
- }
2653
+ if (event) event.stopPropagation();
2654
+ if (!this.hasMenuTarget) return;
2655
+ this.menuTarget.classList.toggle(this.hiddenClassValue);
2704
2656
  }
2705
2657
  hide(event) {
2706
2658
  if (!this.hasMenuTarget) return;
2707
2659
  if (event && this.element.contains(event.target)) return;
2708
- if (this.transitionEnabled && typeof this.leave === "function") {
2709
- this.leave();
2710
- } else {
2711
- this.hideMenu();
2712
- }
2713
- }
2714
- toggleVisibility() {
2715
- this.isOpen() ? this.hideMenu() : this.showMenu();
2716
- }
2717
- showMenu() {
2718
- if (!this.hasMenuTarget) return;
2719
- this.menuTarget.classList.remove(this.hiddenClassValue);
2720
- }
2721
- hideMenu() {
2722
- if (!this.hasMenuTarget) return;
2723
2660
  this.menuTarget.classList.add(this.hiddenClassValue);
2724
2661
  }
2725
- isOpen() {
2726
- return this.hasMenuTarget && !this.menuTarget.classList.contains(this.hiddenClassValue);
2727
- }
2728
- logFallback(error2 = null) {
2729
- this.transitionEnabled = false;
2730
- if (!this._fallbackLogged && typeof window !== "undefined" && window.console) {
2731
- window.console.warn(
2732
- "SourceMonitor dropdown transitions unavailable; using CSS class toggling instead."
2733
- );
2734
- if (error2 && typeof window.console.debug === "function") {
2735
- window.console.debug(error2);
2736
- }
2737
- }
2738
- this._fallbackLogged = true;
2739
- }
2740
2662
  };
2741
2663
 
2742
2664
  // app/assets/javascripts/source_monitor/controllers/modal_controller.js
@@ -2746,6 +2668,7 @@ var modal_controller_default = class extends Controller {
2746
2668
  static values = { autoOpen: Boolean, removeOnClose: Boolean };
2747
2669
  connect() {
2748
2670
  this.handleEscape = this.handleEscape.bind(this);
2671
+ this._inertElements = [];
2749
2672
  if (this.autoOpenValue) {
2750
2673
  this.open();
2751
2674
  }
@@ -2762,6 +2685,8 @@ var modal_controller_default = class extends Controller {
2762
2685
  }
2763
2686
  document.body.classList.add("overflow-hidden");
2764
2687
  document.addEventListener("keydown", this.handleEscape);
2688
+ this._setInert(true);
2689
+ this._focusFirstElement();
2765
2690
  }
2766
2691
  close(event) {
2767
2692
  if (event) event.preventDefault();
@@ -2770,6 +2695,7 @@ var modal_controller_default = class extends Controller {
2770
2695
  if (this.hasOpenClass) {
2771
2696
  this.panelTarget.classList.remove(this.openClass);
2772
2697
  }
2698
+ this._setInert(false);
2773
2699
  this.teardown();
2774
2700
  if (this.removeOnCloseValue) {
2775
2701
  this.element.remove();
@@ -2789,6 +2715,43 @@ var modal_controller_default = class extends Controller {
2789
2715
  document.body.classList.remove("overflow-hidden");
2790
2716
  document.removeEventListener("keydown", this.handleEscape);
2791
2717
  }
2718
+ // -- Private helpers --
2719
+ _setInert(inert) {
2720
+ if (inert) {
2721
+ const modalRoot = this._findModalRoot();
2722
+ if (!modalRoot) return;
2723
+ this._inertElements = [];
2724
+ for (const sibling of document.body.children) {
2725
+ if (sibling === modalRoot || sibling === this.element) continue;
2726
+ if (sibling.nodeType !== Node.ELEMENT_NODE) continue;
2727
+ if (!sibling.hasAttribute("inert")) {
2728
+ sibling.setAttribute("inert", "");
2729
+ this._inertElements.push(sibling);
2730
+ }
2731
+ }
2732
+ } else {
2733
+ for (const el of this._inertElements) {
2734
+ el.removeAttribute("inert");
2735
+ }
2736
+ this._inertElements = [];
2737
+ }
2738
+ }
2739
+ _findModalRoot() {
2740
+ let el = this.hasPanelTarget ? this.panelTarget : this.element;
2741
+ while (el && el.parentElement !== document.body) {
2742
+ el = el.parentElement;
2743
+ }
2744
+ return el;
2745
+ }
2746
+ _focusFirstElement() {
2747
+ requestAnimationFrame(() => {
2748
+ if (!this.hasPanelTarget) return;
2749
+ const focusable = this.panelTarget.querySelector(
2750
+ 'button, [href], input:not([type="hidden"]), select, textarea, [tabindex]:not([tabindex="-1"])'
2751
+ );
2752
+ if (focusable) focusable.focus();
2753
+ });
2754
+ }
2792
2755
  };
2793
2756
 
2794
2757
  // app/assets/javascripts/source_monitor/controllers/confirm_navigation_controller.js
@@ -2878,6 +2841,14 @@ var select_all_controller_default = class extends Controller {
2878
2841
  }
2879
2842
  };
2880
2843
 
2844
+ // app/assets/javascripts/source_monitor/controllers/filter_submit_controller.js
2845
+ var filter_submit_controller_default = class extends Controller {
2846
+ submit(event) {
2847
+ const form = event.target.closest("form");
2848
+ if (form) form.requestSubmit();
2849
+ }
2850
+ };
2851
+
2881
2852
  // app/assets/javascripts/source_monitor/turbo_actions.js
2882
2853
  if (window.Turbo && window.Turbo.StreamActions) {
2883
2854
  window.Turbo.StreamActions.redirect = function() {
@@ -2890,11 +2861,7 @@ if (window.Turbo && window.Turbo.StreamActions) {
2890
2861
  }
2891
2862
 
2892
2863
  // app/assets/javascripts/source_monitor/application.js
2893
- var existingApplication = window.SourceMonitorStimulus;
2894
- var application = existingApplication || Application.start();
2895
- if (!existingApplication) {
2896
- window.SourceMonitorStimulus = application;
2897
- }
2864
+ var application = Application.start();
2898
2865
  application.register("notification", notification_controller_default);
2899
2866
  application.register("notification-container", notification_container_controller_default);
2900
2867
  application.register("async-submit", async_submit_controller_default);
@@ -2902,6 +2869,7 @@ application.register("dropdown", dropdown_controller_default);
2902
2869
  application.register("modal", modal_controller_default);
2903
2870
  application.register("confirm-navigation", confirm_navigation_controller_default);
2904
2871
  application.register("select-all", select_all_controller_default);
2872
+ application.register("filter-submit", filter_submit_controller_default);
2905
2873
  var application_default = application;
2906
2874
  export {
2907
2875
  application_default as default