code_sunset 0.1.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 (71) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +242 -0
  4. data/Rakefile +6 -0
  5. data/app/assets/images/code_sunset/.keep +0 -0
  6. data/app/assets/images/code_sunset/logo-dark.png +0 -0
  7. data/app/assets/images/code_sunset/logo-light.png +0 -0
  8. data/app/assets/images/code_sunset/topography.svg +1 -0
  9. data/app/assets/javascripts/code_sunset/application.js +179 -0
  10. data/app/assets/javascripts/code_sunset/vendor/chart.umd.min.js +14 -0
  11. data/app/assets/stylesheets/code_sunset/application.css +1075 -0
  12. data/app/controllers/code_sunset/application_controller.rb +50 -0
  13. data/app/controllers/code_sunset/dashboard_controller.rb +12 -0
  14. data/app/controllers/code_sunset/features_controller.rb +62 -0
  15. data/app/controllers/code_sunset/removal_candidates_controller.rb +9 -0
  16. data/app/controllers/concerns/.keep +0 -0
  17. data/app/helpers/code_sunset/application_helper.rb +244 -0
  18. data/app/jobs/code_sunset/aggregate_daily_rollups_job.rb +48 -0
  19. data/app/jobs/code_sunset/application_job.rb +4 -0
  20. data/app/jobs/code_sunset/cleanup_events_job.rb +10 -0
  21. data/app/jobs/code_sunset/evaluate_alerts_job.rb +15 -0
  22. data/app/jobs/code_sunset/flush_buffered_events_job.rb +9 -0
  23. data/app/jobs/code_sunset/persist_event_job.rb +9 -0
  24. data/app/mailers/code_sunset/application_mailer.rb +6 -0
  25. data/app/models/code_sunset/alert_delivery.rb +14 -0
  26. data/app/models/code_sunset/application_record.rb +5 -0
  27. data/app/models/code_sunset/daily_rollup.rb +31 -0
  28. data/app/models/code_sunset/event.rb +190 -0
  29. data/app/models/code_sunset/feature.rb +33 -0
  30. data/app/models/concerns/.keep +0 -0
  31. data/app/views/code_sunset/dashboard/index.html.erb +123 -0
  32. data/app/views/code_sunset/features/index.html.erb +88 -0
  33. data/app/views/code_sunset/features/show.html.erb +266 -0
  34. data/app/views/code_sunset/removal_candidates/index.html.erb +118 -0
  35. data/app/views/code_sunset/shared/_filter_bar.html.erb +81 -0
  36. data/app/views/layouts/code_sunset/application.html.erb +57 -0
  37. data/bin/rails +26 -0
  38. data/code_sunset.gemspec +35 -0
  39. data/config/routes.rb +5 -0
  40. data/db/migrate/20260330000000_create_code_sunset_tables.rb +62 -0
  41. data/db/migrate/20260331000000_harden_code_sunset_reliability.rb +39 -0
  42. data/lib/code_sunset/alert_dispatcher.rb +103 -0
  43. data/lib/code_sunset/analytics_filters.rb +59 -0
  44. data/lib/code_sunset/configuration.rb +104 -0
  45. data/lib/code_sunset/context.rb +49 -0
  46. data/lib/code_sunset/controller_context.rb +23 -0
  47. data/lib/code_sunset/engine.rb +24 -0
  48. data/lib/code_sunset/event_buffer.rb +28 -0
  49. data/lib/code_sunset/event_ingestor.rb +57 -0
  50. data/lib/code_sunset/event_payload.rb +79 -0
  51. data/lib/code_sunset/feature_query.rb +69 -0
  52. data/lib/code_sunset/feature_snapshot.rb +14 -0
  53. data/lib/code_sunset/feature_usage_series.rb +59 -0
  54. data/lib/code_sunset/identity_hash.rb +9 -0
  55. data/lib/code_sunset/instrumentation.rb +17 -0
  56. data/lib/code_sunset/job_context.rb +17 -0
  57. data/lib/code_sunset/removal_candidate_query.rb +17 -0
  58. data/lib/code_sunset/removal_prompt_builder.rb +331 -0
  59. data/lib/code_sunset/score_calculator.rb +83 -0
  60. data/lib/code_sunset/status_engine.rb +38 -0
  61. data/lib/code_sunset/subscriber.rb +24 -0
  62. data/lib/code_sunset/version.rb +3 -0
  63. data/lib/code_sunset.rb +109 -0
  64. data/lib/generators/code_sunset/eject_ui/eject_ui_generator.rb +33 -0
  65. data/lib/generators/code_sunset/initializer/initializer_generator.rb +13 -0
  66. data/lib/generators/code_sunset/initializer/templates/code_sunset.rb +34 -0
  67. data/lib/generators/code_sunset/install/install_generator.rb +23 -0
  68. data/lib/generators/code_sunset/migration/migration_generator.rb +21 -0
  69. data/lib/generators/code_sunset/migration/templates/create_code_sunset_tables.rb +75 -0
  70. data/lib/tasks/code_sunset_tasks.rake +21 -0
  71. metadata +185 -0
@@ -0,0 +1,266 @@
1
+ <section class="cs-page-header cs-page-header--feature">
2
+ <div>
3
+ <h2 class="cs-page-title cs-code"><%= @feature.key %></h2>
4
+ <p class="cs-page-copy">Start with the trend and latest events, then use the supporting context to decide whether this code path can be retired.</p>
5
+ </div>
6
+ <div class="cs-page-actions cs-page-actions--feature">
7
+ <%= status_badge(@summary.status) if @summary %>
8
+ <%= link_to removal_prompt_path_for(@feature), class: "cs-button cs-button--ai" do %>
9
+ <span class="cs-button__icon" aria-hidden="true">
10
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
11
+ <path d="M12 3.75l1.5 4.5 4.5 1.5-4.5 1.5-1.5 4.5-1.5-4.5-4.5-1.5 4.5-1.5L12 3.75z"></path>
12
+ <path d="M18.75 14.25l.75 2.25 2.25.75-2.25.75-.75 2.25-.75-2.25-2.25-.75 2.25-.75.75-2.25z"></path>
13
+ </svg>
14
+ </span>
15
+ <span><%= @removal_prompt_artifact ? "Regenerate Removal Prompt" : "Generate Removal Prompt" %></span>
16
+ <% end %>
17
+ <%= link_to "Back to Features", filter_path_for(features_path), class: "cs-inline-link" %>
18
+ </div>
19
+ </section>
20
+
21
+ <section class="cs-grid cs-grid--feature-summary">
22
+ <article class="cs-stat">
23
+ <p class="cs-stat__label">Owner</p>
24
+ <p class="cs-stat__value"><%= @feature.owner.presence || "Unassigned" %></p>
25
+ </article>
26
+ <article class="cs-stat">
27
+ <p class="cs-stat__label">Status</p>
28
+ <p class="cs-stat__value"><%= status_badge(@summary.status) if @summary %></p>
29
+ </article>
30
+ <article class="cs-stat">
31
+ <p class="cs-stat__label">Last Seen</p>
32
+ <p class="cs-stat__value"><%= format_seen_at(@summary&.last_seen_at) %></p>
33
+ </article>
34
+ <article class="cs-stat">
35
+ <p class="cs-stat__label">Removal Score</p>
36
+ <p class="cs-stat__value"><%= removal_score_badge(@feature, @summary, label: (@summary&.score&.round(1) || 0).to_s) %></p>
37
+ </article>
38
+ </section>
39
+
40
+ <%= render_filter_bar(
41
+ form_url: feature_path(@feature.key),
42
+ reset_url: feature_path(@feature.key),
43
+ title: "Filter Feature Usage",
44
+ copy: "Narrow the trend, supporting signals, and recent event stream."
45
+ ) %>
46
+
47
+ <% if @removal_prompt_artifact %>
48
+ <section class="cs-panel cs-panel--removal-prompt">
49
+ <div class="cs-panel__heading cs-panel__heading--prompt">
50
+ <div>
51
+ <h2>Removal Prompt</h2>
52
+ <p class="cs-panel__copy"><%= @removal_prompt_artifact.summary %></p>
53
+ </div>
54
+ <div class="cs-page-actions cs-page-actions--compact">
55
+ <button
56
+ type="button"
57
+ class="cs-button cs-button--ai"
58
+ data-copy-target="cs-removal-prompt"
59
+ data-copy-default-label="Copy Prompt"
60
+ data-copy-success-label="Copied"
61
+ >
62
+ <span class="cs-button__icon" aria-hidden="true">
63
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
64
+ <path d="M12 3.75l1.5 4.5 4.5 1.5-4.5 1.5-1.5 4.5-1.5-4.5-4.5-1.5 4.5-1.5L12 3.75z"></path>
65
+ </svg>
66
+ </span>
67
+ <span>Copy Prompt</span>
68
+ </button>
69
+ <%= link_to "Hide", removal_prompt_reset_path_for(@feature), class: "cs-inline-link" %>
70
+ </div>
71
+ </div>
72
+
73
+ <% if @removal_prompt_artifact.warnings.any? %>
74
+ <div class="cs-removal-prompt__warnings">
75
+ <% @removal_prompt_artifact.warnings.each do |warning| %>
76
+ <p class="cs-removal-prompt__warning"><strong>Warning:</strong> <%= warning %></p>
77
+ <% end %>
78
+ </div>
79
+ <% end %>
80
+
81
+ <div class="cs-removal-prompt">
82
+ <section class="cs-removal-prompt__section">
83
+ <div class="cs-panel__heading">
84
+ <div>
85
+ <h2>Prompt</h2>
86
+ </div>
87
+ </div>
88
+ <pre id="cs-removal-prompt" class="cs-markdown-block"><code><%= @removal_prompt_artifact.prompt_markdown %></code></pre>
89
+ </section>
90
+
91
+ <section class="cs-removal-prompt__section">
92
+ <div class="cs-panel__heading">
93
+ <div>
94
+ <h2>Evidence Pack</h2>
95
+ </div>
96
+ </div>
97
+ <pre class="cs-markdown-block"><code><%= @removal_prompt_artifact.evidence_markdown %></code></pre>
98
+ </section>
99
+ </div>
100
+ </section>
101
+ <% end %>
102
+
103
+ <section class="cs-detail-layout">
104
+ <div class="cs-stack">
105
+ <section class="cs-panel">
106
+ <div class="cs-panel__heading">
107
+ <div>
108
+ <h2>Usage Trend</h2>
109
+ </div>
110
+ </div>
111
+ <%= usage_chart(@usage_series) %>
112
+ </section>
113
+
114
+ <section class="cs-panel">
115
+ <div class="cs-panel__heading">
116
+ <div>
117
+ <h2>Recent Events</h2>
118
+ </div>
119
+ <% if @recent_events_total_count.positive? %>
120
+ <% start_event = ((@recent_events_page - 1) * @recent_events_per_page) + 1 %>
121
+ <% end_event = [@recent_events_page * @recent_events_per_page, @recent_events_total_count].min %>
122
+ <span class="cs-panel__count"><%= "#{start_event}-#{end_event} of #{human_count(@recent_events_total_count)}" %></span>
123
+ <% end %>
124
+ </div>
125
+ <div class="cs-table-wrap">
126
+ <table class="cs-table cs-table--compact">
127
+ <thead>
128
+ <tr>
129
+ <th>Occurred At</th>
130
+ <th>Actor</th>
131
+ <th>Context</th>
132
+ <th>Runtime</th>
133
+ </tr>
134
+ </thead>
135
+ <tbody>
136
+ <% @recent_events.each do |event| %>
137
+ <tr>
138
+ <td><%= format_seen_at(event.occurred_at) %></td>
139
+ <td>
140
+ <div class="cs-stacked-meta">
141
+ <span class="cs-stacked-meta__primary cs-code"><%= event.display_org_identifier || "-" %></span>
142
+ <span class="cs-stacked-meta__secondary cs-code">User <%= event.display_user_identifier || "-" %></span>
143
+ </div>
144
+ </td>
145
+ <td><span class="cs-code cs-code--wrap"><%= event.request_path.presence || event.job_class.presence || event.source %></span></td>
146
+ <td>
147
+ <div class="cs-stacked-meta">
148
+ <span class="cs-stacked-meta__primary"><%= event.app_env.presence || "-" %></span>
149
+ <span class="cs-stacked-meta__secondary cs-code"><%= event.app_version.presence || "-" %></span>
150
+ </div>
151
+ </td>
152
+ </tr>
153
+ <% end %>
154
+ <% if @recent_events.empty? %>
155
+ <tr>
156
+ <td colspan="4" class="cs-subtle">No events matched the selected filters.</td>
157
+ </tr>
158
+ <% end %>
159
+ </tbody>
160
+ </table>
161
+ </div>
162
+ <% if @recent_events_total_pages > 1 %>
163
+ <nav class="cs-pagination" aria-label="Recent events pagination">
164
+ <% if @recent_events_page > 1 %>
165
+ <%= pagination_link_for(@feature, "Previous", page: @recent_events_page - 1, current: false, filters: filters.except(:page), rel: "prev") %>
166
+ <% else %>
167
+ <span class="cs-pagination__link is-disabled">Previous</span>
168
+ <% end %>
169
+
170
+ <span class="cs-pagination__summary">Page <%= @recent_events_page %> of <%= @recent_events_total_pages %></span>
171
+
172
+ <% if @recent_events_page < @recent_events_total_pages %>
173
+ <%= pagination_link_for(@feature, "Next", page: @recent_events_page + 1, current: false, filters: filters.except(:page), rel: "next") %>
174
+ <% else %>
175
+ <span class="cs-pagination__link is-disabled">Next</span>
176
+ <% end %>
177
+ </nav>
178
+ <% end %>
179
+ </section>
180
+ </div>
181
+
182
+ <div class="cs-stack">
183
+ <section class="cs-panel">
184
+ <div class="cs-panel__heading">
185
+ <div>
186
+ <h2>Feature Overview</h2>
187
+ </div>
188
+ </div>
189
+ <div class="cs-meta-grid">
190
+ <div class="cs-meta-row">
191
+ <span class="cs-meta-row__label">Description</span>
192
+ <span><%= @feature.description.presence || "No description provided." %></span>
193
+ </div>
194
+ <div class="cs-meta-row">
195
+ <span class="cs-meta-row__label">Owner</span>
196
+ <span><%= @feature.owner.presence || "Unassigned" %></span>
197
+ </div>
198
+ <div class="cs-meta-row">
199
+ <span class="cs-meta-row__label">Sunset after days</span>
200
+ <span><%= @feature.sunset_after_days %></span>
201
+ </div>
202
+ <div class="cs-meta-row">
203
+ <span class="cs-meta-row__label">Remove after days unused</span>
204
+ <span><%= @feature.remove_after_days_unused %></span>
205
+ </div>
206
+ </div>
207
+ </section>
208
+
209
+ <section class="cs-panel">
210
+ <div class="cs-panel__heading">
211
+ <div>
212
+ <h2>Supporting Signals</h2>
213
+ </div>
214
+ </div>
215
+ <div class="cs-breakdown-group">
216
+ <section class="cs-breakdown-block">
217
+ <h3 class="cs-breakdown-block__title">Top Orgs</h3>
218
+ <ol class="cs-list">
219
+ <% @top_orgs.each do |identifier, hits| %>
220
+ <li><span class="cs-code"><%= identifier %></span> • <%= human_count(hits) %> hits</li>
221
+ <% end %>
222
+ <% if @top_orgs.empty? %>
223
+ <li class="cs-subtle">No org usage in the current window.</li>
224
+ <% end %>
225
+ </ol>
226
+ </section>
227
+
228
+ <section class="cs-breakdown-block">
229
+ <h3 class="cs-breakdown-block__title">Top Users</h3>
230
+ <ol class="cs-list">
231
+ <% @top_users.each do |identifier, hits| %>
232
+ <li><span class="cs-code"><%= identifier %></span> • <%= human_count(hits) %> hits</li>
233
+ <% end %>
234
+ <% if @top_users.empty? %>
235
+ <li class="cs-subtle">No user usage in the current window.</li>
236
+ <% end %>
237
+ </ol>
238
+ </section>
239
+
240
+ <section class="cs-breakdown-block">
241
+ <h3 class="cs-breakdown-block__title">Request Paths</h3>
242
+ <ol class="cs-list">
243
+ <% @request_paths.each do |path, hits| %>
244
+ <li><span class="cs-code"><%= path %></span> • <%= human_count(hits) %> hits</li>
245
+ <% end %>
246
+ <% if @request_paths.empty? %>
247
+ <li class="cs-subtle">No request traffic captured in this window.</li>
248
+ <% end %>
249
+ </ol>
250
+ </section>
251
+
252
+ <section class="cs-breakdown-block">
253
+ <h3 class="cs-breakdown-block__title">Job Usage</h3>
254
+ <ol class="cs-list">
255
+ <% @job_usage.each do |job_class, hits| %>
256
+ <li><span class="cs-code"><%= job_class %></span> • <%= human_count(hits) %> hits</li>
257
+ <% end %>
258
+ <% if @job_usage.empty? %>
259
+ <li class="cs-subtle">No background job usage captured in this window.</li>
260
+ <% end %>
261
+ </ol>
262
+ </section>
263
+ </div>
264
+ </section>
265
+ </div>
266
+ </section>
@@ -0,0 +1,118 @@
1
+ <section class="cs-page-header">
2
+ <div>
3
+ <h2 class="cs-page-title">Removal Queue</h2>
4
+ <p class="cs-page-copy">Review the safest candidates first, then work through the features that still need a final migration pass.</p>
5
+ </div>
6
+ <div class="cs-page-actions cs-page-actions--compact">
7
+ <%= link_to "Features", filter_path_for(features_path), class: "cs-inline-link" %>
8
+ </div>
9
+ </section>
10
+
11
+ <section class="cs-grid">
12
+ <article class="cs-stat">
13
+ <p class="cs-stat__label">Safe To Remove</p>
14
+ <p class="cs-stat__value"><%= human_count(@safe.count) %></p>
15
+ <p class="cs-stat__hint">Highest-confidence candidates.</p>
16
+ </article>
17
+ <article class="cs-stat">
18
+ <p class="cs-stat__label">Needs Review</p>
19
+ <p class="cs-stat__value"><%= human_count(@candidates.count) %></p>
20
+ <p class="cs-stat__hint">Still in the cooling-down window.</p>
21
+ </article>
22
+ <article class="cs-stat">
23
+ <p class="cs-stat__label">Total In Queue</p>
24
+ <p class="cs-stat__value"><%= human_count(@summaries.count) %></p>
25
+ <p class="cs-stat__hint">Visible in the current view.</p>
26
+ </article>
27
+ </section>
28
+
29
+ <%= render_filter_bar(
30
+ form_url: removal_candidates_path,
31
+ reset_url: removal_candidates_path,
32
+ title: "Filter Removal Queue",
33
+ copy: "Use the shared filters to narrow removal candidates before reviewing safe and cooling-down features."
34
+ ) %>
35
+
36
+ <section class="cs-candidate-grid">
37
+ <section class="cs-panel">
38
+ <div class="cs-panel__heading">
39
+ <div>
40
+ <h2>Safe To Remove</h2>
41
+ </div>
42
+ <span class="cs-panel__count"><%= human_count(@safe.count) %> items</span>
43
+ </div>
44
+ <div class="cs-table-wrap">
45
+ <table class="cs-table cs-table--compact">
46
+ <thead>
47
+ <tr>
48
+ <th>Feature</th>
49
+ <th>Last Seen</th>
50
+ <th>Signals</th>
51
+ <th>Score</th>
52
+ </tr>
53
+ </thead>
54
+ <tbody>
55
+ <% @safe.each do |summary| %>
56
+ <tr>
57
+ <td><%= link_to summary.feature.key, filter_path_for(feature_path(summary.feature.key)), class: "cs-code" %></td>
58
+ <td><%= format_seen_at(summary.last_seen_at) %></td>
59
+ <td>
60
+ <div class="cs-stacked-meta">
61
+ <span class="cs-stacked-meta__primary"><%= human_count(summary.hits_30d) %> hits in 30d</span>
62
+ <span class="cs-stacked-meta__secondary"><%= human_count(summary.unique_orgs_count) %> orgs</span>
63
+ </div>
64
+ </td>
65
+ <td><%= removal_score_badge(summary.feature, summary, label: summary.score.round(1).to_s) %></td>
66
+ </tr>
67
+ <% end %>
68
+ <% if @safe.empty? %>
69
+ <tr>
70
+ <td colspan="4" class="cs-subtle">No features are safe to remove yet.</td>
71
+ </tr>
72
+ <% end %>
73
+ </tbody>
74
+ </table>
75
+ </div>
76
+ </section>
77
+
78
+ <section class="cs-panel">
79
+ <div class="cs-panel__heading">
80
+ <div>
81
+ <h2>Candidate For Removal</h2>
82
+ </div>
83
+ <span class="cs-panel__count"><%= human_count(@candidates.count) %> items</span>
84
+ </div>
85
+ <div class="cs-table-wrap">
86
+ <table class="cs-table cs-table--compact">
87
+ <thead>
88
+ <tr>
89
+ <th>Feature</th>
90
+ <th>Last Seen</th>
91
+ <th>Signals</th>
92
+ <th>Score</th>
93
+ </tr>
94
+ </thead>
95
+ <tbody>
96
+ <% @candidates.each do |summary| %>
97
+ <tr>
98
+ <td><%= link_to summary.feature.key, filter_path_for(feature_path(summary.feature.key)), class: "cs-code" %></td>
99
+ <td><%= format_seen_at(summary.last_seen_at) %></td>
100
+ <td>
101
+ <div class="cs-stacked-meta">
102
+ <span class="cs-stacked-meta__primary"><%= human_count(summary.hits_30d) %> hits in 30d</span>
103
+ <span class="cs-stacked-meta__secondary"><%= human_count(summary.unique_orgs_count) %> orgs</span>
104
+ </div>
105
+ </td>
106
+ <td><%= removal_score_badge(summary.feature, summary, label: summary.score.round(1).to_s) %></td>
107
+ </tr>
108
+ <% end %>
109
+ <% if @candidates.empty? %>
110
+ <tr>
111
+ <td colspan="4" class="cs-subtle">No features are in the cooling-off window for removal.</td>
112
+ </tr>
113
+ <% end %>
114
+ </tbody>
115
+ </table>
116
+ </div>
117
+ </section>
118
+ </section>
@@ -0,0 +1,81 @@
1
+ <section class="cs-panel">
2
+ <div class="cs-panel__heading">
3
+ <div>
4
+ <h2><%= title %></h2>
5
+ </div>
6
+ <% if filters_applied %>
7
+ <%= link_to "Clear", reset_url, class: "cs-inline-link" %>
8
+ <% end %>
9
+ </div>
10
+ <% if copy.present? %>
11
+ <p class="cs-panel__copy"><%= copy %></p>
12
+ <% end %>
13
+ <%= form_with url: form_url, method: :get, local: true do %>
14
+ <div class="cs-filter-shell">
15
+ <div class="cs-filters cs-filters--primary">
16
+ <div class="cs-field">
17
+ <label for="app_env">Environment</label>
18
+ <%= text_field_tag :app_env, current_filter_value(:app_env), placeholder: "production" %>
19
+ </div>
20
+ <div class="cs-field">
21
+ <label for="from">From</label>
22
+ <%= date_field_tag :from, current_filter_value(:from) %>
23
+ </div>
24
+ <div class="cs-field">
25
+ <label for="to">To</label>
26
+ <%= date_field_tag :to, current_filter_value(:to) %>
27
+ </div>
28
+ <div class="cs-filter-actions">
29
+ <%= submit_tag "Apply", class: "cs-button" %>
30
+ <% if filters_applied %>
31
+ <%= link_to "Reset", reset_url, class: "cs-inline-link" %>
32
+ <% end %>
33
+ </div>
34
+ </div>
35
+
36
+ <details class="cs-disclosure" <%= "open" if advanced_filters_open? %>>
37
+ <summary class="cs-disclosure__summary">More Filters</summary>
38
+ <div class="cs-filters cs-filters--secondary">
39
+ <div class="cs-field">
40
+ <label for="plan">Plan</label>
41
+ <%= text_field_tag :plan, current_filter_value(:plan), placeholder: "pro" %>
42
+ </div>
43
+ <div class="cs-field">
44
+ <label for="org_id">Org ID</label>
45
+ <%= text_field_tag :org_id, current_filter_value(:org_id) %>
46
+ </div>
47
+ <div class="cs-field">
48
+ <label for="user_id">User ID</label>
49
+ <%= text_field_tag :user_id, current_filter_value(:user_id) %>
50
+ </div>
51
+ <div class="cs-field">
52
+ <label for="paid_only">Paid Orgs Only</label>
53
+ <%= select_tag :paid_only, options_for_select([["Any", nil], ["Yes", true]], current_filter_value(:paid_only)), include_blank: false %>
54
+ </div>
55
+ <div class="cs-field">
56
+ <label for="exclude_internal">Exclude Internal</label>
57
+ <%= select_tag :exclude_internal, options_for_select([["No", nil], ["Yes", true]], current_filter_value(:exclude_internal)), include_blank: false %>
58
+ </div>
59
+ <% configured_custom_filters.each do |key, definition| %>
60
+ <div class="cs-field">
61
+ <label for="<%= key %>"><%= definition[:label] %></label>
62
+ <%= custom_filter_input_tag(key, definition) %>
63
+ </div>
64
+ <% end %>
65
+ </div>
66
+ </details>
67
+
68
+ <% if active_chips.any? %>
69
+ <div class="cs-chip-row" aria-label="Active filters">
70
+ <% active_chips.each do |chip| %>
71
+ <span class="cs-chip">
72
+ <span class="cs-chip__label"><%= chip[:label] %>:</span>
73
+ <span class="cs-chip__value"><%= chip[:value] %></span>
74
+ <%= link_to "Remove", chip[:clear_path], class: "cs-chip__remove", "aria-label": "Remove #{chip[:label]} filter" %>
75
+ </span>
76
+ <% end %>
77
+ </div>
78
+ <% end %>
79
+ </div>
80
+ <% end %>
81
+ </section>
@@ -0,0 +1,57 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Code Sunset</title>
5
+ <%= csrf_meta_tags %>
6
+ <%= csp_meta_tag %>
7
+
8
+ <%= yield :head %>
9
+
10
+ <%= stylesheet_link_tag "code_sunset/application", media: "all" %>
11
+ <%= javascript_include_tag "code_sunset/vendor/chart.umd.min", defer: true %>
12
+ <%= javascript_include_tag "code_sunset/application", defer: true %>
13
+ </head>
14
+ <body>
15
+ <div
16
+ class="cs-background"
17
+ aria-hidden="true"
18
+ style="background-image: url('<%= asset_path("code_sunset/topography.svg") %>');"
19
+ ></div>
20
+ <div class="cs-app">
21
+ <aside class="cs-sidebar">
22
+ <div class="cs-sidebar__brand">
23
+ <%= link_to root_path, class: "cs-brand-lockup", "aria-label": "Code Sunset dashboard" do %>
24
+ <%= image_tag "code_sunset/logo-light.png", alt: "Code Sunset", class: "cs-brand-mark" %>
25
+ <% end %>
26
+ <p class="cs-tagline">Clear runtime signals for the code you want to retire.</p>
27
+ </div>
28
+
29
+ <div class="cs-sidebar__section">
30
+ <p class="cs-sidebar__label">Navigate</p>
31
+ <nav class="cs-nav">
32
+ <%= sidebar_nav_link "Dashboard", root_path, icon: :dashboard %>
33
+ <%= sidebar_nav_link "Features", filter_path_for(features_path), icon: :features %>
34
+ <%= sidebar_nav_link "Removal Queue", filter_path_for(removal_candidates_path), icon: :removal_queue %>
35
+ </nav>
36
+ </div>
37
+
38
+ <footer class="cs-footer">
39
+ <p class="cs-footer__meta">Mounted at <span class="cs-code">/code_sunset</span></p>
40
+ Gem developed by <a href="https://sdglhm.com" target="_blank" rel="noopener noreferrer">sdglhm.com</a>
41
+ </footer>
42
+ </aside>
43
+
44
+ <div class="cs-shell">
45
+ <main class="cs-main">
46
+ <% if analytics_notice.present? %>
47
+ <section class="cs-panel cs-panel--notice">
48
+ <p><strong>Raw Event Window:</strong> <%= analytics_notice %></p>
49
+ </section>
50
+ <% end %>
51
+
52
+ <%= yield %>
53
+ </main>
54
+ </div>
55
+ </div>
56
+ </body>
57
+ </html>
data/bin/rails ADDED
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env ruby
2
+ # This command will automatically be run when you run "rails" with Rails gems
3
+ # installed from the root of your application.
4
+
5
+ ENGINE_ROOT = File.expand_path("..", __dir__)
6
+ ENGINE_PATH = File.expand_path("../lib/code_sunset/engine", __dir__)
7
+ APP_PATH = File.expand_path("../spec/dummy/config/application", __dir__)
8
+
9
+ # Set up gems listed in the Gemfile.
10
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
11
+ require "bundler/setup" if File.exist?(ENV["BUNDLE_GEMFILE"])
12
+
13
+ require "rails"
14
+ # Pick the frameworks you want:
15
+ require "active_model/railtie"
16
+ require "active_job/railtie"
17
+ require "active_record/railtie"
18
+ # require "active_storage/engine"
19
+ require "action_controller/railtie"
20
+ require "action_mailer/railtie"
21
+ # require "action_mailbox/engine"
22
+ # require "action_text/engine"
23
+ require "action_view/railtie"
24
+ # require "action_cable/engine"
25
+ require "rails/test_unit/railtie"
26
+ require "rails/engine/commands"
@@ -0,0 +1,35 @@
1
+ require_relative "lib/code_sunset/version"
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "code_sunset"
5
+ spec.version = CodeSunset::VERSION
6
+ spec.authors = [ "sdglhm" ]
7
+ spec.email = [ "hello@sdglhm.com" ]
8
+ spec.homepage = "https://sdglhm.com"
9
+ spec.summary = "Runtime-aware deprecation intelligence for Rails."
10
+ spec.description = "Track legacy code usage, aggregate removal signals, and review everything in a mounted Rails engine dashboard."
11
+ spec.license = "MIT"
12
+
13
+ spec.metadata["homepage_uri"] = spec.homepage
14
+ spec.metadata["source_code_uri"] = "https://github.com/sdglhm/code_sunset"
15
+ spec.metadata["changelog_uri"] = "https://github.com/sdglhm/code_sunset/blob/main/README.md"
16
+ spec.metadata["bug_tracker_uri"] = "https://github.com/sdglhm/code_sunset/issues"
17
+
18
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
19
+ tracked = `git ls-files -z 2>/dev/null`.split("\x0")
20
+ tracked = Dir["app/**/*", "config/**/*", "db/**/*", "lib/**/*", "bin/*", "MIT-LICENSE", "Rakefile", "README.md", "*.gemspec"] if tracked.empty?
21
+ tracked.select do |path|
22
+ path.match?(%r{\A(app|config|db|lib)/}) ||
23
+ path.match?(%r{\Abin/}) ||
24
+ %w[MIT-LICENSE Rakefile README.md code_sunset.gemspec].include?(path)
25
+ end
26
+ end
27
+
28
+ spec.require_paths = ["lib"]
29
+
30
+ spec.add_dependency "rails", ">= 7.1", "< 9.0"
31
+
32
+ spec.add_development_dependency "pg", "~> 1.6"
33
+ spec.add_development_dependency "puma", "~> 7.0"
34
+ spec.add_development_dependency "propshaft", ">= 1.0", "< 2.0"
35
+ end
data/config/routes.rb ADDED
@@ -0,0 +1,5 @@
1
+ CodeSunset::Engine.routes.draw do
2
+ root to: "dashboard#index"
3
+ resources :features, only: [:index, :show], constraints: { id: /[^\/]+/ }
4
+ resources :removal_candidates, only: [:index]
5
+ end
@@ -0,0 +1,62 @@
1
+ class CreateCodeSunsetTables < ActiveRecord::Migration[7.1]
2
+ def change
3
+ create_table :code_sunset_features do |t|
4
+ t.string :key, null: false
5
+ t.string :owner
6
+ t.text :description
7
+ t.string :status
8
+ t.integer :sunset_after_days, null: false, default: 90
9
+ t.integer :remove_after_days_unused, null: false, default: 60
10
+ t.timestamps
11
+ end
12
+
13
+ add_index :code_sunset_features, :key, unique: true
14
+ add_index :code_sunset_features, :status
15
+
16
+ create_table :code_sunset_events do |t|
17
+ t.string :feature_key, null: false
18
+ t.datetime :occurred_at, null: false
19
+ t.bigint :user_id
20
+ t.bigint :org_id
21
+ t.string :hashed_user_id
22
+ t.string :hashed_org_id
23
+ t.string :request_id
24
+ t.string :source
25
+ t.string :request_path
26
+ t.string :controller
27
+ t.string :action
28
+ t.string :job_class
29
+ t.string :app_env
30
+ t.string :app_version
31
+ t.string :plan
32
+ t.boolean :internal_org
33
+ t.boolean :paid_org
34
+ t.jsonb :metadata, null: false, default: {}
35
+ t.timestamps
36
+ end
37
+
38
+ add_index :code_sunset_events, :feature_key
39
+ add_index :code_sunset_events, :occurred_at
40
+ add_index :code_sunset_events, [:feature_key, :occurred_at]
41
+ add_index :code_sunset_events, :user_id
42
+ add_index :code_sunset_events, :org_id
43
+ add_index :code_sunset_events, :hashed_user_id
44
+ add_index :code_sunset_events, :hashed_org_id
45
+ add_index :code_sunset_events, :app_env
46
+ add_index :code_sunset_events, :plan
47
+ add_index :code_sunset_events, :metadata, using: :gin
48
+
49
+ create_table :code_sunset_daily_rollups do |t|
50
+ t.string :feature_key, null: false
51
+ t.date :day, null: false
52
+ t.integer :hits_count, null: false, default: 0
53
+ t.integer :unique_users_count, null: false, default: 0
54
+ t.integer :unique_orgs_count, null: false, default: 0
55
+ t.datetime :last_seen_at
56
+ t.timestamps
57
+ end
58
+
59
+ add_index :code_sunset_daily_rollups, [:feature_key, :day], unique: true
60
+ add_index :code_sunset_daily_rollups, :day
61
+ end
62
+ end