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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +242 -0
- data/Rakefile +6 -0
- data/app/assets/images/code_sunset/.keep +0 -0
- data/app/assets/images/code_sunset/logo-dark.png +0 -0
- data/app/assets/images/code_sunset/logo-light.png +0 -0
- data/app/assets/images/code_sunset/topography.svg +1 -0
- data/app/assets/javascripts/code_sunset/application.js +179 -0
- data/app/assets/javascripts/code_sunset/vendor/chart.umd.min.js +14 -0
- data/app/assets/stylesheets/code_sunset/application.css +1075 -0
- data/app/controllers/code_sunset/application_controller.rb +50 -0
- data/app/controllers/code_sunset/dashboard_controller.rb +12 -0
- data/app/controllers/code_sunset/features_controller.rb +62 -0
- data/app/controllers/code_sunset/removal_candidates_controller.rb +9 -0
- data/app/controllers/concerns/.keep +0 -0
- data/app/helpers/code_sunset/application_helper.rb +244 -0
- data/app/jobs/code_sunset/aggregate_daily_rollups_job.rb +48 -0
- data/app/jobs/code_sunset/application_job.rb +4 -0
- data/app/jobs/code_sunset/cleanup_events_job.rb +10 -0
- data/app/jobs/code_sunset/evaluate_alerts_job.rb +15 -0
- data/app/jobs/code_sunset/flush_buffered_events_job.rb +9 -0
- data/app/jobs/code_sunset/persist_event_job.rb +9 -0
- data/app/mailers/code_sunset/application_mailer.rb +6 -0
- data/app/models/code_sunset/alert_delivery.rb +14 -0
- data/app/models/code_sunset/application_record.rb +5 -0
- data/app/models/code_sunset/daily_rollup.rb +31 -0
- data/app/models/code_sunset/event.rb +190 -0
- data/app/models/code_sunset/feature.rb +33 -0
- data/app/models/concerns/.keep +0 -0
- data/app/views/code_sunset/dashboard/index.html.erb +123 -0
- data/app/views/code_sunset/features/index.html.erb +88 -0
- data/app/views/code_sunset/features/show.html.erb +266 -0
- data/app/views/code_sunset/removal_candidates/index.html.erb +118 -0
- data/app/views/code_sunset/shared/_filter_bar.html.erb +81 -0
- data/app/views/layouts/code_sunset/application.html.erb +57 -0
- data/bin/rails +26 -0
- data/code_sunset.gemspec +35 -0
- data/config/routes.rb +5 -0
- data/db/migrate/20260330000000_create_code_sunset_tables.rb +62 -0
- data/db/migrate/20260331000000_harden_code_sunset_reliability.rb +39 -0
- data/lib/code_sunset/alert_dispatcher.rb +103 -0
- data/lib/code_sunset/analytics_filters.rb +59 -0
- data/lib/code_sunset/configuration.rb +104 -0
- data/lib/code_sunset/context.rb +49 -0
- data/lib/code_sunset/controller_context.rb +23 -0
- data/lib/code_sunset/engine.rb +24 -0
- data/lib/code_sunset/event_buffer.rb +28 -0
- data/lib/code_sunset/event_ingestor.rb +57 -0
- data/lib/code_sunset/event_payload.rb +79 -0
- data/lib/code_sunset/feature_query.rb +69 -0
- data/lib/code_sunset/feature_snapshot.rb +14 -0
- data/lib/code_sunset/feature_usage_series.rb +59 -0
- data/lib/code_sunset/identity_hash.rb +9 -0
- data/lib/code_sunset/instrumentation.rb +17 -0
- data/lib/code_sunset/job_context.rb +17 -0
- data/lib/code_sunset/removal_candidate_query.rb +17 -0
- data/lib/code_sunset/removal_prompt_builder.rb +331 -0
- data/lib/code_sunset/score_calculator.rb +83 -0
- data/lib/code_sunset/status_engine.rb +38 -0
- data/lib/code_sunset/subscriber.rb +24 -0
- data/lib/code_sunset/version.rb +3 -0
- data/lib/code_sunset.rb +109 -0
- data/lib/generators/code_sunset/eject_ui/eject_ui_generator.rb +33 -0
- data/lib/generators/code_sunset/initializer/initializer_generator.rb +13 -0
- data/lib/generators/code_sunset/initializer/templates/code_sunset.rb +34 -0
- data/lib/generators/code_sunset/install/install_generator.rb +23 -0
- data/lib/generators/code_sunset/migration/migration_generator.rb +21 -0
- data/lib/generators/code_sunset/migration/templates/create_code_sunset_tables.rb +75 -0
- data/lib/tasks/code_sunset_tasks.rake +21 -0
- 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"
|
data/code_sunset.gemspec
ADDED
|
@@ -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,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
|