rails_nexus 2.0.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 (114) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +430 -0
  4. data/Rakefile +6 -0
  5. data/app/assets/config/rails_nexus_manifest.js +2 -0
  6. data/app/assets/javascripts/rails_nexus/controllers.js +603 -0
  7. data/app/assets/stylesheets/rails_nexus/application.css +1964 -0
  8. data/app/controllers/rails_nexus/analytics_controller.rb +361 -0
  9. data/app/controllers/rails_nexus/application_controller.rb +15 -0
  10. data/app/controllers/rails_nexus/backup_controller.rb +91 -0
  11. data/app/controllers/rails_nexus/cron_jobs_controller.rb +64 -0
  12. data/app/controllers/rails_nexus/database_health_controller.rb +128 -0
  13. data/app/controllers/rails_nexus/logged_exceptions_controller.rb +227 -0
  14. data/app/controllers/rails_nexus/n1_patterns_controller.rb +10 -0
  15. data/app/controllers/rails_nexus/settings_controller.rb +100 -0
  16. data/app/controllers/rails_nexus/source_code_controller.rb +39 -0
  17. data/app/controllers/rails_nexus/stats_controller.rb +494 -0
  18. data/app/controllers/rails_nexus/workflow_controller.rb +96 -0
  19. data/app/helpers/rails_nexus/application_helper.rb +5 -0
  20. data/app/helpers/rails_nexus/logged_exceptions_helper.rb +254 -0
  21. data/app/helpers/rails_nexus/source_code_helper.rb +107 -0
  22. data/app/javascript/controllers/rails_nexus_controller.js +380 -0
  23. data/app/javascript/controllers/rails_nexus_detail_controller.js +165 -0
  24. data/app/javascript/controllers/rails_nexus_sidebar_controller.js +15 -0
  25. data/app/javascript/controllers/rails_nexus_theme_controller.js +31 -0
  26. data/app/javascript/rails_nexus/application.js +17 -0
  27. data/app/jobs/rails_nexus/application_job.rb +4 -0
  28. data/app/mailers/rails_nexus/application_mailer.rb +6 -0
  29. data/app/models/rails_nexus/application_record.rb +5 -0
  30. data/app/models/rails_nexus/backup.rb +107 -0
  31. data/app/models/rails_nexus/base_record.rb +26 -0
  32. data/app/models/rails_nexus/comment.rb +13 -0
  33. data/app/models/rails_nexus/cron_job.rb +72 -0
  34. data/app/models/rails_nexus/database_stat.rb +128 -0
  35. data/app/models/rails_nexus/event.rb +101 -0
  36. data/app/models/rails_nexus/logged_exception.rb +864 -0
  37. data/app/models/rails_nexus/logged_exception_n1.rb +87 -0
  38. data/app/models/rails_nexus/metric.rb +107 -0
  39. data/app/models/rails_nexus/nginx_metric.rb +112 -0
  40. data/app/models/rails_nexus/server_metric.rb +168 -0
  41. data/app/models/rails_nexus/webhook_delivery.rb +64 -0
  42. data/app/services/rails_nexus/backup_service.rb +275 -0
  43. data/app/views/layouts/rails_nexus/application.html.erb +185 -0
  44. data/app/views/rails_nexus/analytics/index.html.erb +363 -0
  45. data/app/views/rails_nexus/backup/files.html.erb +53 -0
  46. data/app/views/rails_nexus/backup/index.html.erb +211 -0
  47. data/app/views/rails_nexus/backup/settings.html.erb +158 -0
  48. data/app/views/rails_nexus/cron_jobs/index.html.erb +141 -0
  49. data/app/views/rails_nexus/database_health/index.html.erb +139 -0
  50. data/app/views/rails_nexus/logged_exceptions/_comments_list.html.erb +50 -0
  51. data/app/views/rails_nexus/logged_exceptions/_exceptions.html.erb +190 -0
  52. data/app/views/rails_nexus/logged_exceptions/_feed.html.erb +3 -0
  53. data/app/views/rails_nexus/logged_exceptions/_filter_group.html.erb +10 -0
  54. data/app/views/rails_nexus/logged_exceptions/_show.html.erb +554 -0
  55. data/app/views/rails_nexus/logged_exceptions/_workflow_panel.html.erb +118 -0
  56. data/app/views/rails_nexus/logged_exceptions/clear.turbo_stream.erb +6 -0
  57. data/app/views/rails_nexus/logged_exceptions/destroy.turbo_stream.erb +2 -0
  58. data/app/views/rails_nexus/logged_exceptions/destroy_all.turbo_stream.erb +6 -0
  59. data/app/views/rails_nexus/logged_exceptions/feed.rss.builder +20 -0
  60. data/app/views/rails_nexus/logged_exceptions/index.html.erb +323 -0
  61. data/app/views/rails_nexus/logged_exceptions/query.html.erb +3 -0
  62. data/app/views/rails_nexus/logged_exceptions/query.turbo_stream.erb +6 -0
  63. data/app/views/rails_nexus/logged_exceptions/show.html.erb +3 -0
  64. data/app/views/rails_nexus/logged_exceptions/show.turbo_stream.erb +5 -0
  65. data/app/views/rails_nexus/n1_patterns/index.html.erb +153 -0
  66. data/app/views/rails_nexus/settings/index.html.erb +140 -0
  67. data/app/views/rails_nexus/stats/index.html.erb +569 -0
  68. data/config/importmap.rb +15 -0
  69. data/config/initializers/date_formats.rb +5 -0
  70. data/config/initializers/rails_nexus.rb +18 -0
  71. data/config/locales/en.yml +47 -0
  72. data/config/routes.rb +58 -0
  73. data/db/migrate/20240330122311_create_rails_nexus_logged_exceptions.rb +22 -0
  74. data/lib/generators/rails_nexus/backup_generator.rb +70 -0
  75. data/lib/generators/rails_nexus/customize_generator.rb +159 -0
  76. data/lib/generators/rails_nexus/install_generator.rb +61 -0
  77. data/lib/generators/rails_nexus/templates/INSTALL.md +9 -0
  78. data/lib/generators/rails_nexus/templates/backup/backup_helper.sh +152 -0
  79. data/lib/generators/rails_nexus/templates/backup/config.rb +27 -0
  80. data/lib/generators/rails_nexus/templates/backup/models/daily_backup.rb +24 -0
  81. data/lib/generators/rails_nexus/templates/backup/models/full_backup.rb +35 -0
  82. data/lib/generators/rails_nexus/templates/backup/models/sync_backup.rb +43 -0
  83. data/lib/generators/rails_nexus/templates/backup/mysql-config/db_config.cnf +15 -0
  84. data/lib/generators/rails_nexus/templates/backup/schedule.rb +28 -0
  85. data/lib/generators/rails_nexus/templates/javascript/controllers/rails_nexus_controller.js +310 -0
  86. data/lib/generators/rails_nexus/templates/javascript/controllers/rails_nexus_detail_controller.js +98 -0
  87. data/lib/generators/rails_nexus/templates/javascript/controllers/rails_nexus_sidebar_controller.js +15 -0
  88. data/lib/generators/rails_nexus/templates/javascript/controllers/rails_nexus_theme_controller.js +30 -0
  89. data/lib/generators/rails_nexus/templates/migration.rb +24 -0
  90. data/lib/generators/rails_nexus/templates/migration_advanced_features.rb +30 -0
  91. data/lib/generators/rails_nexus/templates/migration_cron_jobs.rb +22 -0
  92. data/lib/generators/rails_nexus/templates/migration_new_schema.rb +120 -0
  93. data/lib/generators/rails_nexus/templates/migration_platform_detection.rb +11 -0
  94. data/lib/generators/rails_nexus/templates/migration_webhook_deliveries.rb +21 -0
  95. data/lib/generators/rails_nexus/templates/migration_workflow.rb +29 -0
  96. data/lib/generators/rails_nexus/templates/rails_nexus.rb +96 -0
  97. data/lib/generators/rails_nexus/templates/views/layouts/rails_nexus/application.html.erb +129 -0
  98. data/lib/generators/rails_nexus/templates/views/rails_nexus/logged_exceptions/_exceptions.html.erb +123 -0
  99. data/lib/generators/rails_nexus/templates/views/rails_nexus/logged_exceptions/_show.html.erb +297 -0
  100. data/lib/generators/rails_nexus/templates/views/rails_nexus/logged_exceptions/index.html.erb +113 -0
  101. data/lib/rails_nexus/breadcrumbs.rb +161 -0
  102. data/lib/rails_nexus/cleanup.rb +32 -0
  103. data/lib/rails_nexus/configuration.rb +319 -0
  104. data/lib/rails_nexus/engine.rb +135 -0
  105. data/lib/rails_nexus/logger.rb +188 -0
  106. data/lib/rails_nexus/notifications.rb +105 -0
  107. data/lib/rails_nexus/ransack_config.rb +4 -0
  108. data/lib/rails_nexus/storm_protection.rb +124 -0
  109. data/lib/rails_nexus/version.rb +3 -0
  110. data/lib/rails_nexus.rb +133 -0
  111. data/lib/tasks/rails_nexus.rake +230 -0
  112. data/lib/tasks/rails_nexus_tasks.rake +37 -0
  113. data/rails_nexus.gemspec +36 -0
  114. metadata +252 -0
@@ -0,0 +1,153 @@
1
+ <% page_title t(".title", default: "N+1 Query Patterns") %>
2
+
3
+ <div class="space-y-6">
4
+ <%# ─── Header ──────────────────────────────────────────────── %>
5
+ <nav class="rn-breadcrumbs" aria-label="Breadcrumb">
6
+ <a href="<%= root_path %>" class="rn-breadcrumb">Dashboard</a>
7
+ <span class="rn-breadcrumb-sep">/</span>
8
+ <span class="rn-breadcrumb-current">N+1 Query Patterns</span>
9
+ </nav>
10
+
11
+ <div class="flex items-center gap-3">
12
+ <svg class="w-6 h-6" style="color: var(--rn-primary)" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
13
+ <path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" />
14
+ </svg>
15
+ <h1 class="text-2xl font-bold" style="color: var(--rn-text)">N+1 Query Patterns</h1>
16
+ </div>
17
+ <p class="text-sm" style="color: var(--rn-text-muted)">
18
+ Detects N+1 query patterns from exception breadcrumbs (SQL queries) and backtrace analysis.
19
+ Breadcrumbs are now enabled — capture exceptions in your app to populate this page.
20
+ </p>
21
+
22
+ <%# ─── Summary Stats ───────────────────────────────────────── %>
23
+ <% if @summary %>
24
+ <div class="rn-stats">
25
+ <div class="rn-stat">
26
+ <span class="rn-stat-label">Unique Patterns</span>
27
+ <span class="rn-stat-value"><%= @summary[:unique_patterns] %></span>
28
+ <span class="rn-stat-change">Distinct query groups</span>
29
+ </div>
30
+ <div class="rn-stat">
31
+ <span class="rn-stat-label">Total Repetitions</span>
32
+ <span class="rn-stat-value danger"><%= number_with_delimiter(@summary[:total_patterns]) %></span>
33
+ <span class="rn-stat-change">Similar queries detected</span>
34
+ </div>
35
+ <% if @summary[:worst_pattern] %>
36
+ <div class="rn-stat">
37
+ <span class="rn-stat-label">Worst Offender</span>
38
+ <span class="rn-stat-value" style="font-size: 0.875rem"><%= @summary[:worst_pattern][:table] %></span>
39
+ <span class="rn-stat-change"><%= @summary[:worst_pattern][:count] %> repetitions</span>
40
+ </div>
41
+ <% end %>
42
+ <div class="rn-stat">
43
+ <span class="rn-stat-label">Top Tables</span>
44
+ <span class="rn-stat-value"><%= @summary[:top_tables]&.size || 0 %></span>
45
+ <span class="rn-stat-change">Affected tables</span>
46
+ </div>
47
+ </div>
48
+ <% end %>
49
+
50
+ <%# ─── Top Tables Breakdown ───────────────────────────────── %>
51
+ <% if @summary && @summary[:top_tables]&.any? %>
52
+ <div class="rn-card">
53
+ <div class="p-4" style="border-bottom: 1px solid var(--rn-border-light)">
54
+ <h2 class="text-sm font-semibold" style="color: var(--rn-text)">Top Affected Tables</h2>
55
+ <p class="text-xs mt-1" style="color: var(--rn-text-muted)">Tables with the most N+1 query repetitions</p>
56
+ </div>
57
+ <div class="p-4">
58
+ <div class="space-y-3">
59
+ <% max_count = @summary[:top_tables].map { |t| t[:count] }.max.to_f %>
60
+ <% @summary[:top_tables].each do |table_data| %>
61
+ <div>
62
+ <div class="flex items-center justify-between mb-1">
63
+ <span class="text-sm font-medium" style="color: var(--rn-text)"><code><%= table_data[:table] %></code></span>
64
+ <span class="text-xs font-semibold" style="color: var(--rn-danger)"><%= table_data[:count] %> queries</span>
65
+ </div>
66
+ <div class="rn-progress" style="height: 8px">
67
+ <div class="rn-progress-bar danger" style="width: <%= (table_data[:count] / max_count * 100).round %>%"></div>
68
+ </div>
69
+ </div>
70
+ <% end %>
71
+ </div>
72
+ </div>
73
+ </div>
74
+ <% end %>
75
+
76
+ <%# ─── Patterns Table ─────────────────────────────────────── %>
77
+ <div class="rn-card">
78
+ <div class="p-4" style="border-bottom: 1px solid var(--rn-border-light)">
79
+ <div class="flex items-center gap-2">
80
+ <svg class="w-4 h-4" style="color: var(--rn-danger)" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
81
+ <path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z" />
82
+ </svg>
83
+ <h2 class="text-sm font-semibold" style="color: var(--rn-text)">Detected Patterns</h2>
84
+ </div>
85
+ <p class="text-xs mt-1" style="color: var(--rn-text-muted)">
86
+ Queries with 3+ similar occurrences in a single exception's breadcrumbs
87
+ </p>
88
+ </div>
89
+
90
+ <% if @patterns.any? %>
91
+ <div class="rn-table-wrap">
92
+ <table class="rn-table">
93
+ <thead>
94
+ <tr>
95
+ <th>Table</th>
96
+ <th>Count</th>
97
+ <th>Avg Duration</th>
98
+ <th>Sample</th>
99
+ <th>Source</th>
100
+ <th>Exception IDs</th>
101
+ </tr>
102
+ </thead>
103
+ <tbody>
104
+ <% @patterns.each do |pattern| %>
105
+ <tr>
106
+ <td>
107
+ <span class="rn-badge"><%= pattern[:table] %></span>
108
+ </td>
109
+ <td>
110
+ <span class="font-bold" style="color: var(--rn-danger)"><%= pattern[:count] %></span>
111
+ </td>
112
+ <td>
113
+ <span style="color: var(--rn-text-secondary)"><%= pattern[:avg_duration] %>ms</span>
114
+ </td>
115
+ <td>
116
+ <code class="text-xs" style="color: var(--rn-text-muted); word-break: break-all; max-width: 400px; display: inline-block">
117
+ <%= truncate(pattern[:sql_sample], length: 120) %>
118
+ </code>
119
+ </td>
120
+ <td>
121
+ <span class="rn-badge rn-badge-<%= pattern[:source] == :breadcrumbs ? 'info' : 'warning' %>">
122
+ <%= pattern[:source] == :breadcrumbs ? 'SQL' : 'Backtrace' %>
123
+ </span>
124
+ </td>
125
+ <td>
126
+ <% pattern[:exception_ids]&.each do |eid| %>
127
+ <%= link_to "##{eid}", logged_exception_path(eid), class: "text-xs", style: "color: var(--rn-primary)", data: { turbo_frame: "exception-details" } %>
128
+ <% end %>
129
+ </td>
130
+ </tr>
131
+ <% end %>
132
+ </tbody>
133
+ </table>
134
+ </div>
135
+ <% else %>
136
+ <div class="p-8 text-center">
137
+ <svg class="w-10 h-10 mx-auto" style="color: var(--rn-text-muted)" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
138
+ <path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
139
+ </svg>
140
+ <p class="mt-3 text-sm font-medium" style="color: var(--rn-text-muted)">No N+1 patterns detected</p>
141
+ <p class="mt-1 text-xs" style="color: var(--rn-text-muted)">
142
+ Capture some exceptions in your application to populate this page.
143
+ N+1 patterns are detected from both SQL breadcrumbs and backtrace analysis.
144
+ </p>
145
+ <% unless RailsNexus.configuration.breadcrumbs_enabled %>
146
+ <p class="mt-2 text-xs font-medium" style="color: var(--rn-warning)">
147
+ ⚠ Breadcrumbs are disabled. Enable <code>config.breadcrumbs_enabled = true</code> for better N+1 detection.
148
+ </p>
149
+ <% end %>
150
+ </div>
151
+ <% end %>
152
+ </div>
153
+ </div>
@@ -0,0 +1,140 @@
1
+ <% page_title t(".title", default: "Settings") %>
2
+
3
+ <div class="space-y-6">
4
+ <%# ─── Header ──────────────────────────────────────────────── %>
5
+ <nav class="rn-breadcrumbs" aria-label="Breadcrumb">
6
+ <a href="<%= root_path %>" class="rn-breadcrumb">Dashboard</a>
7
+ <span class="rn-breadcrumb-sep">/</span>
8
+ <span class="rn-breadcrumb-current">Settings</span>
9
+ </nav>
10
+
11
+ <h1 class="text-2xl font-bold" style="color: var(--rn-text)">Settings</h1>
12
+
13
+ <%# ─── Webhook Configuration ──────────────────────────────── %>
14
+ <div class="rn-card">
15
+ <div class="p-4" style="border-bottom: 1px solid var(--rn-border-light)">
16
+ <h2 class="text-sm font-semibold" style="color: var(--rn-text)">Webhook Configuration</h2>
17
+ <p class="text-xs mt-1" style="color: var(--rn-text-muted)">
18
+ Configure webhook URLs to receive notifications when exceptions occur.
19
+ </p>
20
+ </div>
21
+ <div class="p-4">
22
+ <% if @webhooks.any? %>
23
+ <div class="space-y-3 mb-6">
24
+ <% @webhooks.each_with_index do |url, index| %>
25
+ <div class="flex items-center gap-3 p-3 rounded-lg" style="background: var(--rn-bg-secondary)">
26
+ <span class="w-2 h-2 rounded-full" style="background: var(--rn-success)"></span>
27
+ <span class="text-sm font-mono flex-1" style="color: var(--rn-text)"><%= url %></span>
28
+ <span class="text-xs" style="color: var(--rn-text-muted)">Active</span>
29
+ </div>
30
+ <% end %>
31
+ </div>
32
+ <% else %>
33
+ <div class="p-4 rounded-lg mb-6" style="background: var(--rn-bg-secondary)">
34
+ <p class="text-sm" style="color: var(--rn-text-muted)">No webhooks configured. Add webhook URLs in your initializer:</p>
35
+ <pre class="mt-2 text-xs rn-mono p-3 rounded" style="background: var(--rn-bg-primary); color: var(--rn-text-secondary)"><code>RailsNexus.configure do |config|
36
+ config.webhooks = ["https://hooks.slack.com/services/xxx"]
37
+ config.webhook_headers = { "Authorization" => "Bearer token" }
38
+ config.webhook_timeout = 5
39
+ end</code></pre>
40
+ </div>
41
+ <% end %>
42
+
43
+ <%# Test Webhook Form %>
44
+ <div class="border-t pt-4" style="border-color: var(--rn-border-light)">
45
+ <h3 class="text-sm font-semibold mb-3" style="color: var(--rn-text)">Test Webhook</h3>
46
+ <%= form_tag test_webhook_path, method: :post, class: "flex items-center gap-3" do %>
47
+ <input type="url" name="url"
48
+ placeholder="https://hooks.slack.com/services/xxx"
49
+ class="rn-input flex-1"
50
+ required>
51
+ <%= submit_tag "Send Test", class: "rn-btn rn-btn-primary" %>
52
+ <% end %>
53
+ </div>
54
+ </div>
55
+ </div>
56
+
57
+ <%# ─── Webhook Headers ─────────────────────────────────────── %>
58
+ <% if @webhook_headers.any? %>
59
+ <div class="rn-card">
60
+ <div class="p-4" style="border-bottom: 1px solid var(--rn-border-light)">
61
+ <h2 class="text-sm font-semibold" style="color: var(--rn-text)">Webhook Headers</h2>
62
+ </div>
63
+ <div class="p-4">
64
+ <div class="space-y-2">
65
+ <% @webhook_headers.each do |key, value| %>
66
+ <div class="flex items-center gap-3 text-sm">
67
+ <span class="rn-mono font-medium" style="color: var(--rn-text-secondary)"><%= key %>:</span>
68
+ <span class="rn-mono" style="color: var(--rn-text-muted)"><%= value %></span>
69
+ </div>
70
+ <% end %>
71
+ </div>
72
+ </div>
73
+ </div>
74
+ <% end %>
75
+
76
+ <%# ─── Delivery History ────────────────────────────────────── %>
77
+ <div class="rn-card">
78
+ <div class="p-4" style="border-bottom: 1px solid var(--rn-border-light)">
79
+ <div class="flex items-center justify-between">
80
+ <h2 class="text-sm font-semibold" style="color: var(--rn-text)">Recent Deliveries</h2>
81
+ <%= link_to "View all", webhook_deliveries_path, class: "text-xs font-medium", style: "color: var(--rn-primary)" %>
82
+ </div>
83
+ </div>
84
+
85
+ <% if @recent_deliveries.any? %>
86
+ <div class="rn-table-wrap">
87
+ <table class="rn-table">
88
+ <thead>
89
+ <tr>
90
+ <th>Status</th>
91
+ <th>URL</th>
92
+ <th>Event</th>
93
+ <th>Response</th>
94
+ <th>Duration</th>
95
+ <th>Time</th>
96
+ </tr>
97
+ </thead>
98
+ <tbody>
99
+ <% @recent_deliveries.each do |delivery| %>
100
+ <tr>
101
+ <td>
102
+ <span class="rn-badge rn-badge-<%= delivery.success? ? 'success' : 'danger' %>">
103
+ <%= delivery.status.capitalize %>
104
+ </span>
105
+ </td>
106
+ <td>
107
+ <span class="text-xs font-mono truncate" style="color: var(--rn-text-secondary); max-width: 200px; display: inline-block">
108
+ <%= delivery.url %>
109
+ </span>
110
+ </td>
111
+ <td>
112
+ <span class="text-xs" style="color: var(--rn-text-muted)"><%= delivery.event_type || "—" %></span>
113
+ </td>
114
+ <td>
115
+ <span class="rn-mono text-xs" style="color: <%= delivery.success? ? 'var(--rn-success)' : 'var(--rn-danger)' %>">
116
+ <%= delivery.response_code || "—" %>
117
+ </span>
118
+ </td>
119
+ <td>
120
+ <span class="text-xs" style="color: var(--rn-text-muted)">
121
+ <%= delivery.duration ? "#{(delivery.duration * 1000).round}ms" : "—" %>
122
+ </span>
123
+ </td>
124
+ <td>
125
+ <span class="text-xs" style="color: var(--rn-text-muted)">
126
+ <%= delivery.created_at.strftime("%b %d %I:%M %p") %>
127
+ </span>
128
+ </td>
129
+ </tr>
130
+ <% end %>
131
+ </tbody>
132
+ </table>
133
+ </div>
134
+ <% else %>
135
+ <div class="py-8 text-center">
136
+ <p class="text-sm" style="color: var(--rn-text-muted)">No deliveries recorded yet</p>
137
+ </div>
138
+ <% end %>
139
+ </div>
140
+ </div>