llm_cost_tracker 0.2.0.alpha2 → 0.3.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 +4 -4
- data/CHANGELOG.md +48 -1
- data/README.md +114 -70
- data/Rakefile +2 -0
- data/app/assets/llm_cost_tracker/application.css +760 -0
- data/app/controllers/llm_cost_tracker/application_controller.rb +1 -7
- data/app/controllers/llm_cost_tracker/assets_controller.rb +12 -0
- data/app/controllers/llm_cost_tracker/calls_controller.rb +29 -12
- data/app/controllers/llm_cost_tracker/dashboard_controller.rb +5 -1
- data/app/helpers/llm_cost_tracker/application_helper.rb +46 -5
- data/app/helpers/llm_cost_tracker/chart_helper.rb +133 -0
- data/app/helpers/llm_cost_tracker/dashboard_filter_helper.rb +47 -0
- data/app/helpers/llm_cost_tracker/dashboard_filter_options_helper.rb +34 -0
- data/app/helpers/llm_cost_tracker/dashboard_query_helper.rb +58 -0
- data/app/helpers/llm_cost_tracker/pagination_helper.rb +18 -0
- data/app/services/llm_cost_tracker/dashboard/data_quality.rb +16 -1
- data/app/services/llm_cost_tracker/dashboard/filter.rb +22 -3
- data/app/services/llm_cost_tracker/dashboard/overview_stats.rb +16 -1
- data/app/services/llm_cost_tracker/dashboard/spend_anomaly.rb +79 -0
- data/app/services/llm_cost_tracker/dashboard/tag_key_explorer.rb +19 -46
- data/app/services/llm_cost_tracker/dashboard/top_models.rb +17 -8
- data/app/services/llm_cost_tracker/pagination.rb +6 -0
- data/app/views/layouts/llm_cost_tracker/application.html.erb +35 -333
- data/app/views/llm_cost_tracker/calls/index.html.erb +116 -74
- data/app/views/llm_cost_tracker/calls/show.html.erb +58 -1
- data/app/views/llm_cost_tracker/dashboard/index.html.erb +211 -111
- data/app/views/llm_cost_tracker/data_quality/index.html.erb +224 -78
- data/app/views/llm_cost_tracker/errors/database.html.erb +3 -3
- data/app/views/llm_cost_tracker/errors/invalid_filter.html.erb +3 -3
- data/app/views/llm_cost_tracker/errors/not_found.html.erb +3 -3
- data/app/views/llm_cost_tracker/models/index.html.erb +66 -58
- data/app/views/llm_cost_tracker/shared/_active_filters.html.erb +16 -0
- data/app/views/llm_cost_tracker/shared/_metric_stack.html.erb +23 -0
- data/app/views/llm_cost_tracker/shared/_spend_chart.html.erb +18 -0
- data/app/views/llm_cost_tracker/shared/_tag_chips.html.erb +15 -0
- data/app/views/llm_cost_tracker/shared/setup_required.html.erb +3 -2
- data/app/views/llm_cost_tracker/tags/index.html.erb +55 -12
- data/app/views/llm_cost_tracker/tags/show.html.erb +88 -39
- data/config/routes.rb +3 -0
- data/lib/llm_cost_tracker/assets.rb +19 -0
- data/lib/llm_cost_tracker/configuration.rb +78 -42
- data/lib/llm_cost_tracker/engine.rb +2 -0
- data/lib/llm_cost_tracker/event.rb +2 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/add_streaming_generator.rb +29 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_streaming_to_llm_api_calls.rb.erb +25 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_api_calls.rb.erb +4 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/llm_cost_tracker_prices.yml.erb +8 -1
- data/lib/llm_cost_tracker/llm_api_call.rb +9 -1
- data/lib/llm_cost_tracker/middleware/faraday.rb +57 -9
- data/lib/llm_cost_tracker/parsed_usage.rb +7 -3
- data/lib/llm_cost_tracker/parsers/anthropic.rb +79 -1
- data/lib/llm_cost_tracker/parsers/base.rb +17 -5
- data/lib/llm_cost_tracker/parsers/gemini.rb +59 -6
- data/lib/llm_cost_tracker/parsers/openai.rb +8 -0
- data/lib/llm_cost_tracker/parsers/openai_compatible.rb +8 -0
- data/lib/llm_cost_tracker/parsers/openai_usage.rb +55 -1
- data/lib/llm_cost_tracker/parsers/registry.rb +15 -3
- data/lib/llm_cost_tracker/parsers/sse.rb +81 -0
- data/lib/llm_cost_tracker/price_registry.rb +18 -7
- data/lib/llm_cost_tracker/price_sync/fetcher.rb +72 -0
- data/lib/llm_cost_tracker/price_sync/merger.rb +72 -0
- data/lib/llm_cost_tracker/price_sync/model_catalog.rb +77 -0
- data/lib/llm_cost_tracker/price_sync/raw_price.rb +35 -0
- data/lib/llm_cost_tracker/price_sync/source.rb +29 -0
- data/lib/llm_cost_tracker/price_sync/source_result.rb +7 -0
- data/lib/llm_cost_tracker/price_sync/sources/litellm.rb +91 -0
- data/lib/llm_cost_tracker/price_sync/sources/open_router.rb +94 -0
- data/lib/llm_cost_tracker/price_sync/validator.rb +66 -0
- data/lib/llm_cost_tracker/price_sync.rb +310 -0
- data/lib/llm_cost_tracker/pricing.rb +19 -6
- data/lib/llm_cost_tracker/retention.rb +34 -0
- data/lib/llm_cost_tracker/storage/active_record_store.rb +3 -1
- data/lib/llm_cost_tracker/stream_collector.rb +158 -0
- data/lib/llm_cost_tracker/tag_query.rb +7 -2
- data/lib/llm_cost_tracker/tags_column.rb +21 -1
- data/lib/llm_cost_tracker/tracker.rb +15 -12
- data/lib/llm_cost_tracker/value_helpers.rb +40 -0
- data/lib/llm_cost_tracker/version.rb +1 -1
- data/lib/llm_cost_tracker.rb +51 -29
- data/lib/tasks/llm_cost_tracker.rake +124 -0
- data/llm_cost_tracker.gemspec +9 -8
- metadata +40 -12
- data/PLAN_0.2.md +0 -488
data/PLAN_0.2.md
DELETED
|
@@ -1,488 +0,0 @@
|
|
|
1
|
-
# llm_cost_tracker v0.2.0 Plan
|
|
2
|
-
|
|
3
|
-
## Goal
|
|
4
|
-
|
|
5
|
-
Ship an opt-in Rails dashboard engine:
|
|
6
|
-
|
|
7
|
-
```ruby
|
|
8
|
-
require "llm_cost_tracker/engine"
|
|
9
|
-
|
|
10
|
-
mount LlmCostTracker::Engine => "/llm-costs"
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
The dashboard is read-only and shows where a Rails app spends money on LLM APIs:
|
|
14
|
-
|
|
15
|
-
- spend over time
|
|
16
|
-
- top models
|
|
17
|
-
- recent calls
|
|
18
|
-
- call details
|
|
19
|
-
- tag breakdowns such as `feature`
|
|
20
|
-
- optional monthly budget status
|
|
21
|
-
|
|
22
|
-
The first milestone is a useful Overview page that can be used as the README screenshot.
|
|
23
|
-
|
|
24
|
-
## Non-Goals
|
|
25
|
-
|
|
26
|
-
These are intentionally out of scope for v0.2.0:
|
|
27
|
-
|
|
28
|
-
- built-in authorization
|
|
29
|
-
- CSV or JSON export
|
|
30
|
-
- alerts, webhooks, or Slack notifications
|
|
31
|
-
- JavaScript charts or live updates
|
|
32
|
-
- editing settings from the dashboard
|
|
33
|
-
- persistent budget models
|
|
34
|
-
- `/budgets`
|
|
35
|
-
- `/tags` index or automatic tag-key discovery
|
|
36
|
-
- timezone-aware SQL bucketing
|
|
37
|
-
- hourly period grouping
|
|
38
|
-
- SaaS or remote sync
|
|
39
|
-
|
|
40
|
-
## Compatibility Decisions
|
|
41
|
-
|
|
42
|
-
### Core Gem
|
|
43
|
-
|
|
44
|
-
The core gem remains lightweight and usable outside Rails:
|
|
45
|
-
|
|
46
|
-
- keep `activesupport >= 7.0, < 9.0`
|
|
47
|
-
- do not add `railties` as a runtime dependency
|
|
48
|
-
- keep middleware-only usage working for plain Faraday, Sinatra, Hanami, and service objects
|
|
49
|
-
|
|
50
|
-
### Dashboard Engine
|
|
51
|
-
|
|
52
|
-
The Engine is Rails-only and opt-in:
|
|
53
|
-
|
|
54
|
-
- users must `require "llm_cost_tracker/engine"`
|
|
55
|
-
- Engine requires Rails 7.1+
|
|
56
|
-
- the Rails version guard must run at top level in `engine.rb`, before defining `class Engine < ::Rails::Engine`
|
|
57
|
-
- `railties` is a development/test dependency only
|
|
58
|
-
|
|
59
|
-
Example guard:
|
|
60
|
-
|
|
61
|
-
```ruby
|
|
62
|
-
unless Gem::Version.new(Rails.version) >= Gem::Version.new("7.1.0")
|
|
63
|
-
raise LlmCostTracker::Error, "LlmCostTracker::Engine requires Rails 7.1+"
|
|
64
|
-
end
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
## Phase 1: Public Query Primitive
|
|
68
|
-
|
|
69
|
-
Add SQL-side period grouping to `LlmCostTracker::LlmApiCall`.
|
|
70
|
-
|
|
71
|
-
### API
|
|
72
|
-
|
|
73
|
-
```ruby
|
|
74
|
-
LlmCostTracker::LlmApiCall.group_by_period(:day).sum(:total_cost)
|
|
75
|
-
LlmCostTracker::LlmApiCall.group_by_period(:month).count
|
|
76
|
-
LlmCostTracker::LlmApiCall.group_by_period(:day, column: :created_at)
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
Default column: `:tracked_at`.
|
|
80
|
-
|
|
81
|
-
### Supported Periods
|
|
82
|
-
|
|
83
|
-
Only:
|
|
84
|
-
|
|
85
|
-
- `:day`
|
|
86
|
-
- `:month`
|
|
87
|
-
|
|
88
|
-
Do not add `:hour` or `:week` in v0.2.0.
|
|
89
|
-
|
|
90
|
-
### Return Keys
|
|
91
|
-
|
|
92
|
-
Return string keys across all adapters:
|
|
93
|
-
|
|
94
|
-
```ruby
|
|
95
|
-
:day # "2026-04-18"
|
|
96
|
-
:month # "2026-04"
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
### SQL Strategy
|
|
100
|
-
|
|
101
|
-
- PostgreSQL: `TO_CHAR(DATE_TRUNC(...), ...)`
|
|
102
|
-
- MySQL: `DATE_FORMAT(...)`
|
|
103
|
-
- SQLite: `strftime(...)`
|
|
104
|
-
|
|
105
|
-
### Validation
|
|
106
|
-
|
|
107
|
-
- period must be whitelisted
|
|
108
|
-
- column must exist in `column_names`
|
|
109
|
-
- invalid period or column raises `ArgumentError`
|
|
110
|
-
- no `time_zone:` parameter in v0.2.0
|
|
111
|
-
|
|
112
|
-
### Tests
|
|
113
|
-
|
|
114
|
-
- real SQLite grouping specs
|
|
115
|
-
- generated SQL expression specs for PostgreSQL/MySQL if those adapters are not in CI
|
|
116
|
-
- injection tests for period and column
|
|
117
|
-
- composition test:
|
|
118
|
-
|
|
119
|
-
```ruby
|
|
120
|
-
LlmCostTracker::LlmApiCall
|
|
121
|
-
.this_month
|
|
122
|
-
.where(provider: "openai")
|
|
123
|
-
.group_by_period(:day)
|
|
124
|
-
.sum(:total_cost)
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
## Phase 2: Engine Skeleton
|
|
128
|
-
|
|
129
|
-
Add the minimal Rails Engine structure:
|
|
130
|
-
|
|
131
|
-
```text
|
|
132
|
-
lib/llm_cost_tracker/engine.rb
|
|
133
|
-
app/controllers/llm_cost_tracker/application_controller.rb
|
|
134
|
-
app/controllers/llm_cost_tracker/dashboard_controller.rb
|
|
135
|
-
app/views/layouts/llm_cost_tracker/application.html.erb
|
|
136
|
-
config/routes.rb
|
|
137
|
-
spec/dummy/
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
### Requirements
|
|
141
|
-
|
|
142
|
-
- isolated namespace: `isolate_namespace LlmCostTracker`
|
|
143
|
-
- no automatic loading for non-Rails users
|
|
144
|
-
- no asset pipeline dependency
|
|
145
|
-
- no JavaScript
|
|
146
|
-
- inline CSS in the Engine layout
|
|
147
|
-
- all CSS classes prefixed with `.lct-`
|
|
148
|
-
- minimal `spec/dummy` Rails app for Engine request specs
|
|
149
|
-
|
|
150
|
-
### Acceptance
|
|
151
|
-
|
|
152
|
-
- dummy app mounts the Engine at `/llm-costs`
|
|
153
|
-
- `GET /llm-costs` returns 200
|
|
154
|
-
- empty database shows an intentional empty state
|
|
155
|
-
- missing `llm_api_calls` table shows a friendly setup error
|
|
156
|
-
|
|
157
|
-
## Phase 3: Dashboard Data Layer
|
|
158
|
-
|
|
159
|
-
Use plain Ruby objects under:
|
|
160
|
-
|
|
161
|
-
```text
|
|
162
|
-
app/services/llm_cost_tracker/dashboard/
|
|
163
|
-
```
|
|
164
|
-
|
|
165
|
-
### Filter
|
|
166
|
-
|
|
167
|
-
Parses dashboard params and returns an ActiveRecord relation.
|
|
168
|
-
|
|
169
|
-
Supported params:
|
|
170
|
-
|
|
171
|
-
- `from`
|
|
172
|
-
- `to`
|
|
173
|
-
- `provider`
|
|
174
|
-
- `model`
|
|
175
|
-
- `tag[key]=value`
|
|
176
|
-
|
|
177
|
-
Rules:
|
|
178
|
-
|
|
179
|
-
- parse `from` and `to` with `Date.iso8601`
|
|
180
|
-
- ignore invalid dates
|
|
181
|
-
- use AR placeholders for provider/model
|
|
182
|
-
- parse all `params[:tag]` keys as a hash
|
|
183
|
-
- pass multi-key tag filters to `by_tags`
|
|
184
|
-
- validate tag keys with the same whitelist as `group_by_tag`
|
|
185
|
-
|
|
186
|
-
Example:
|
|
187
|
-
|
|
188
|
-
```text
|
|
189
|
-
?tag[feature]=chat&tag[user_id]=42
|
|
190
|
-
```
|
|
191
|
-
|
|
192
|
-
becomes:
|
|
193
|
-
|
|
194
|
-
```ruby
|
|
195
|
-
scope.by_tags("feature" => "chat", "user_id" => "42")
|
|
196
|
-
```
|
|
197
|
-
|
|
198
|
-
### Page
|
|
199
|
-
|
|
200
|
-
Use a small immutable object, not a large service:
|
|
201
|
-
|
|
202
|
-
```ruby
|
|
203
|
-
Dashboard::Page = Data.define(:page, :per)
|
|
204
|
-
```
|
|
205
|
-
|
|
206
|
-
Rules:
|
|
207
|
-
|
|
208
|
-
- `page` minimum: 1
|
|
209
|
-
- `per` default: 50
|
|
210
|
-
- `per` maximum: 200
|
|
211
|
-
- expose `limit`, `offset`, `prev_page?`, and `next_page?`
|
|
212
|
-
|
|
213
|
-
### OverviewStats
|
|
214
|
-
|
|
215
|
-
Compute:
|
|
216
|
-
|
|
217
|
-
- total spend
|
|
218
|
-
- total calls
|
|
219
|
-
- average cost per call
|
|
220
|
-
- average latency only if `latency_ms` exists
|
|
221
|
-
- monthly budget status only if `LlmCostTracker.configuration.monthly_budget` is set
|
|
222
|
-
|
|
223
|
-
Do not compute `known_pricing_rate` in v0.2.0.
|
|
224
|
-
|
|
225
|
-
### TimeSeries
|
|
226
|
-
|
|
227
|
-
Uses `group_by_period(:day).sum(:total_cost)`.
|
|
228
|
-
|
|
229
|
-
Rules:
|
|
230
|
-
|
|
231
|
-
- default range: last 30 days
|
|
232
|
-
- fill missing days with zero
|
|
233
|
-
- output array:
|
|
234
|
-
|
|
235
|
-
```ruby
|
|
236
|
-
[{ label: "2026-04-01", cost: 0.0 }]
|
|
237
|
-
```
|
|
238
|
-
|
|
239
|
-
### TopModels
|
|
240
|
-
|
|
241
|
-
Compute:
|
|
242
|
-
|
|
243
|
-
- provider
|
|
244
|
-
- model
|
|
245
|
-
- calls
|
|
246
|
-
- total cost
|
|
247
|
-
- average cost per call
|
|
248
|
-
- input tokens
|
|
249
|
-
- output tokens
|
|
250
|
-
- average latency if available
|
|
251
|
-
|
|
252
|
-
### TopTags
|
|
253
|
-
|
|
254
|
-
Only for configured keys.
|
|
255
|
-
|
|
256
|
-
Default keys:
|
|
257
|
-
|
|
258
|
-
```ruby
|
|
259
|
-
["feature"]
|
|
260
|
-
```
|
|
261
|
-
|
|
262
|
-
No automatic tag-key discovery in v0.2.0.
|
|
263
|
-
|
|
264
|
-
## Phase 4: Dashboard Pages
|
|
265
|
-
|
|
266
|
-
### Overview: `GET /`
|
|
267
|
-
|
|
268
|
-
The main screenshot-worthy page.
|
|
269
|
-
|
|
270
|
-
Show:
|
|
271
|
-
|
|
272
|
-
- total spend
|
|
273
|
-
- total calls
|
|
274
|
-
- average cost per call
|
|
275
|
-
- average latency if available
|
|
276
|
-
- monthly budget status if configured
|
|
277
|
-
- daily spend table with CSS bars
|
|
278
|
-
- top 5 models
|
|
279
|
-
- cost by `feature` tag if data exists
|
|
280
|
-
|
|
281
|
-
Budget wording:
|
|
282
|
-
|
|
283
|
-
> Soft monthly limit. Blocking is not atomic under concurrency.
|
|
284
|
-
|
|
285
|
-
### Calls Index: `GET /calls`
|
|
286
|
-
|
|
287
|
-
Filters:
|
|
288
|
-
|
|
289
|
-
- `from`
|
|
290
|
-
- `to`
|
|
291
|
-
- `provider`
|
|
292
|
-
- `model`
|
|
293
|
-
- `tag[key]=value`
|
|
294
|
-
- `page`
|
|
295
|
-
- `per`
|
|
296
|
-
|
|
297
|
-
Columns:
|
|
298
|
-
|
|
299
|
-
- created_at
|
|
300
|
-
- provider
|
|
301
|
-
- model
|
|
302
|
-
- input tokens
|
|
303
|
-
- output tokens
|
|
304
|
-
- cache tokens if present
|
|
305
|
-
- total cost
|
|
306
|
-
- latency if available
|
|
307
|
-
- tags summary
|
|
308
|
-
- details link
|
|
309
|
-
|
|
310
|
-
Rules:
|
|
311
|
-
|
|
312
|
-
- newest first
|
|
313
|
-
- max `per=200`
|
|
314
|
-
- one count query and one paginated query
|
|
315
|
-
- no N+1 behavior
|
|
316
|
-
|
|
317
|
-
### Call Details: `GET /calls/:id`
|
|
318
|
-
|
|
319
|
-
Show:
|
|
320
|
-
|
|
321
|
-
- all stored columns
|
|
322
|
-
- token breakdown
|
|
323
|
-
- cost breakdown
|
|
324
|
-
- latency if available
|
|
325
|
-
- tags as pretty JSON
|
|
326
|
-
- metadata if present
|
|
327
|
-
|
|
328
|
-
Missing records render a friendly 404 inside the Engine layout.
|
|
329
|
-
|
|
330
|
-
### Models: `GET /models`
|
|
331
|
-
|
|
332
|
-
Show rows grouped by provider/model:
|
|
333
|
-
|
|
334
|
-
- provider
|
|
335
|
-
- model
|
|
336
|
-
- calls
|
|
337
|
-
- total cost
|
|
338
|
-
- average cost per call
|
|
339
|
-
- input tokens
|
|
340
|
-
- output tokens
|
|
341
|
-
- average latency if available
|
|
342
|
-
|
|
343
|
-
Default sort: total cost descending.
|
|
344
|
-
|
|
345
|
-
### Tag Breakdown: `GET /tags/:key`
|
|
346
|
-
|
|
347
|
-
Validate `key` using the same tag-key whitelist as `group_by_tag`.
|
|
348
|
-
|
|
349
|
-
Show:
|
|
350
|
-
|
|
351
|
-
- tag value
|
|
352
|
-
- calls
|
|
353
|
-
- total cost
|
|
354
|
-
- average cost per call
|
|
355
|
-
|
|
356
|
-
Invalid keys render a friendly 400.
|
|
357
|
-
|
|
358
|
-
Do not add `/tags` index in v0.2.0.
|
|
359
|
-
|
|
360
|
-
## Graceful Degradation
|
|
361
|
-
|
|
362
|
-
The dashboard must not explode when optional pieces are missing.
|
|
363
|
-
|
|
364
|
-
Handle:
|
|
365
|
-
|
|
366
|
-
- empty database
|
|
367
|
-
- missing `llm_api_calls` table
|
|
368
|
-
- missing `latency_ms`
|
|
369
|
-
- text `tags` fallback instead of JSONB
|
|
370
|
-
- malformed legacy tag JSON
|
|
371
|
-
- unknown-pricing rows with `total_cost = nil`
|
|
372
|
-
- no configured monthly budget
|
|
373
|
-
|
|
374
|
-
## Security
|
|
375
|
-
|
|
376
|
-
- dashboard is read-only
|
|
377
|
-
- routes are GET-only
|
|
378
|
-
- keep `protect_from_forgery with: :exception`
|
|
379
|
-
- no built-in auth
|
|
380
|
-
- README must show Basic Auth and Devise examples
|
|
381
|
-
- validate every param through `Dashboard::Filter`
|
|
382
|
-
- do not log full tags or request details from the Engine
|
|
383
|
-
|
|
384
|
-
README warning:
|
|
385
|
-
|
|
386
|
-
> Do not expose this dashboard publicly. Tags may contain internal user, tenant, or feature identifiers.
|
|
387
|
-
|
|
388
|
-
## Styling
|
|
389
|
-
|
|
390
|
-
- ERB templates
|
|
391
|
-
- no JS
|
|
392
|
-
- no external fonts
|
|
393
|
-
- no external CSS
|
|
394
|
-
- no Chart.js
|
|
395
|
-
- no Tailwind dependency
|
|
396
|
-
- inline `<style>` in layout
|
|
397
|
-
- `.lct-` class prefix
|
|
398
|
-
- CSS bar charts through inline custom properties
|
|
399
|
-
|
|
400
|
-
Example:
|
|
401
|
-
|
|
402
|
-
```html
|
|
403
|
-
<div class="lct-bar" style="--lct-width: 73%"></div>
|
|
404
|
-
```
|
|
405
|
-
|
|
406
|
-
## Testing Plan
|
|
407
|
-
|
|
408
|
-
### Core Specs
|
|
409
|
-
|
|
410
|
-
- `group_by_period(:day)`
|
|
411
|
-
- `group_by_period(:month)`
|
|
412
|
-
- invalid period
|
|
413
|
-
- invalid column
|
|
414
|
-
- composition with existing scopes
|
|
415
|
-
|
|
416
|
-
### Engine Request Specs
|
|
417
|
-
|
|
418
|
-
- `/` empty DB
|
|
419
|
-
- `/` seeded DB
|
|
420
|
-
- `/calls`
|
|
421
|
-
- `/calls` filters narrow results
|
|
422
|
-
- `/calls/:id`
|
|
423
|
-
- `/calls/:missing`
|
|
424
|
-
- `/models`
|
|
425
|
-
- `/tags/feature`
|
|
426
|
-
- invalid tag key returns 400
|
|
427
|
-
- missing table renders setup error
|
|
428
|
-
|
|
429
|
-
### Service Specs
|
|
430
|
-
|
|
431
|
-
- `Dashboard::Filter`
|
|
432
|
-
- `Dashboard::Page`
|
|
433
|
-
- `Dashboard::TimeSeries`
|
|
434
|
-
- `Dashboard::OverviewStats`
|
|
435
|
-
- `Dashboard::TopModels`
|
|
436
|
-
- `Dashboard::TopTags`
|
|
437
|
-
|
|
438
|
-
### CI
|
|
439
|
-
|
|
440
|
-
- Ruby 3.3, 3.4
|
|
441
|
-
- Rails Engine specs on Rails 7.1, 7.2, 8.0
|
|
442
|
-
- SQLite default
|
|
443
|
-
- PostgreSQL job for period grouping and tag grouping
|
|
444
|
-
- MySQL best-effort only; not required for v0.2.0 CI
|
|
445
|
-
|
|
446
|
-
## Documentation
|
|
447
|
-
|
|
448
|
-
README additions:
|
|
449
|
-
|
|
450
|
-
- Dashboard quick start
|
|
451
|
-
- mount snippet
|
|
452
|
-
- Basic Auth snippet
|
|
453
|
-
- Devise snippet
|
|
454
|
-
- warning not to expose dashboard publicly
|
|
455
|
-
- note that Engine requires Rails 7.1+
|
|
456
|
-
- note that core middleware remains usable without Rails
|
|
457
|
-
|
|
458
|
-
## Release Strategy
|
|
459
|
-
|
|
460
|
-
1. Work on `codex/engine-dashboard`.
|
|
461
|
-
2. Implement `group_by_period`.
|
|
462
|
-
3. Add Engine skeleton.
|
|
463
|
-
4. Build Overview first.
|
|
464
|
-
5. Build Calls index.
|
|
465
|
-
6. Build Call details.
|
|
466
|
-
7. Build Models.
|
|
467
|
-
8. Build `/tags/:key`.
|
|
468
|
-
9. Dogfood in a real Rails app.
|
|
469
|
-
10. Release `0.2.0.alpha1`.
|
|
470
|
-
11. Fix issues from real usage.
|
|
471
|
-
12. Release `0.2.0.rc1`.
|
|
472
|
-
13. Release final `0.2.0`.
|
|
473
|
-
|
|
474
|
-
## v0.2.0 Acceptance Criteria
|
|
475
|
-
|
|
476
|
-
The release is ready when:
|
|
477
|
-
|
|
478
|
-
- `group_by_period(:day/:month)` is tested and documented
|
|
479
|
-
- Engine is opt-in and requires Rails 7.1+
|
|
480
|
-
- non-Rails core usage does not gain Rails runtime dependencies
|
|
481
|
-
- `/llm-costs` renders without JS or asset pipeline dependencies
|
|
482
|
-
- empty DB and missing table states are friendly
|
|
483
|
-
- seeded dashboard shows useful spend data
|
|
484
|
-
- filters cannot SQL-inject
|
|
485
|
-
- Overview is good enough for README screenshot
|
|
486
|
-
- README includes auth snippets
|
|
487
|
-
- full test suite and RuboCop pass
|
|
488
|
-
- alpha has been tested in one real Rails app
|