nilometer 0.0.0-stage → 0.2.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.
- package/CHANGELOG.md +68 -0
- package/LICENSE +21 -0
- package/README.md +464 -2
- package/core/pricing/prices.json +348 -0
- package/core/schema/001_ingestion.sql +57 -0
- package/core/schema/002_line_count.sql +7 -0
- package/core/schema/003_derived.sql +70 -0
- package/core/schema/004_requests_dedup.sql +50 -0
- package/core/schema/005_repositories.sql +25 -0
- package/core/schema/006_status_readings.sql +33 -0
- package/core/schema/007_pricing.sql +95 -0
- package/core/schema/008_interruptions.sql +263 -0
- package/core/schema/009_headroom.sql +84 -0
- package/core/schema/010_attribution.sql +106 -0
- package/core/schema/011_projections.sql +83 -0
- package/core/schema/012_unsplit_cache_writes.sql +49 -0
- package/core/schema/013_repository_revalidation.sql +104 -0
- package/core/schema/014_observed_readings.sql +204 -0
- package/core/schema/015_response_observations.sql +127 -0
- package/core/schema/016_windows_repository_paths.sql +41 -0
- package/core/schema/017_unattributed_usage_once.sql +47 -0
- package/core/schema/018_request_time_index.sql +21 -0
- package/core/schema/019_dedup_earliest_of_equals.sql +61 -0
- package/core/schema/020_limit_quota_fields.sql +131 -0
- package/core/schema/021_plan_periods.sql +121 -0
- package/core/schema/022_attachment_walk.sql +223 -0
- package/core/schema/023_auto_continuations.sql +129 -0
- package/core/schema/024_counted_limit_groups.sql +29 -0
- package/core/schema/025_model_fallback.sql +77 -0
- package/core/schema/026_changed_readings.sql +106 -0
- package/core/schema/027_weekly_crossings.sql +32 -0
- package/core/schema/028_stale_readings.sql +196 -0
- package/core/schema/029_lockout_sessions.sql +68 -0
- package/core/schema/030_repository_aliases.sql +81 -0
- package/core/schema/031_cte_cursor_budget.sql +153 -0
- package/core/schema/032_stored_views.sql +233 -0
- package/dist/cli/main.cli.js +39 -0
- package/dist/cli/program.js +881 -0
- package/dist/core/db/database.js +182 -0
- package/dist/core/db/fingerprint.js +71 -0
- package/dist/core/ingest/attribution.js +131 -0
- package/dist/core/ingest/classify.js +382 -0
- package/dist/core/ingest/command.js +95 -0
- package/dist/core/ingest/derive.js +219 -0
- package/dist/core/ingest/discover.js +126 -0
- package/dist/core/ingest/ingest.js +160 -0
- package/dist/core/ingest/reader.js +89 -0
- package/dist/core/ingest/reset-time.js +97 -0
- package/dist/core/ingest/results.js +201 -0
- package/dist/core/ingest/spool.js +165 -0
- package/dist/core/ingest/store.js +99 -0
- package/dist/core/ingest/stored-views.js +65 -0
- package/dist/core/install/install.js +185 -0
- package/dist/core/install/locations.js +89 -0
- package/dist/core/install/private-files.js +259 -0
- package/dist/core/install/record.js +103 -0
- package/dist/core/plans/plan-prices.js +128 -0
- package/dist/core/plans/repository-aliases.js +136 -0
- package/dist/core/pricing/prices.js +147 -0
- package/dist/core/settings/settings-file.js +215 -0
- package/dist/core/settings/statusline.js +240 -0
- package/dist/core/util/commands.js +91 -0
- package/dist/core/util/node-version.js +58 -0
- package/dist/core/util/open-page.js +69 -0
- package/dist/core/util/paths.js +36 -0
- package/dist/core/verify/compare.js +77 -0
- package/dist/core/verify/verify.js +220 -0
- package/dist/core/view/server.js +142 -0
- package/dist/viewer/explain.js +574 -0
- package/dist/viewer/format.js +150 -0
- package/dist/viewer/html-charts.js +1292 -0
- package/dist/viewer/html-contract.js +2 -0
- package/dist/viewer/html-data.js +718 -0
- package/dist/viewer/html.js +1717 -0
- package/dist/viewer/queries.js +128 -0
- package/dist/viewer/render.js +602 -0
- package/dist/viewer/report.js +120 -0
- package/dist/viewer/save.js +110 -0
- package/docs/requirements.md +113 -0
- package/hooks/statusline.sh +104 -0
- package/package.json +61 -4
|
@@ -0,0 +1,718 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The HTML report's data layer (step G1.1, D-070): everything the page draws, read once
|
|
3
|
+
* from the same views the text report reads, with the raw line IDs behind every drawn element.
|
|
4
|
+
*
|
|
5
|
+
* Implements README, section MVP scope, as a picture, under the Measurement Principles: observed rows and
|
|
6
|
+
* projected rows are loaded by separate functions from separate views (principle 2), every element
|
|
7
|
+
* carries the raw line IDs of its events (principle 4), and every chart's span comes from the
|
|
8
|
+
* views' own coverage columns (principle 5). All selection, grouping, and arithmetic stays in SQL;
|
|
9
|
+
* this module only maps rows onto the contract's types and collects the events they reference.
|
|
10
|
+
* Numbers come back unrounded, so rounding happens only at display.
|
|
11
|
+
*
|
|
12
|
+
* Which events stand behind each element follows `viewer/explain.ts`, so the page and `nilometer
|
|
13
|
+
* explain` list the same events for the same number. Each view is read in one pass, with each
|
|
14
|
+
* element's events gathered by an ordered `json_group_array`, rather than one query per element:
|
|
15
|
+
* the views behind limit hits and readings are costly to evaluate, and a month of real data has
|
|
16
|
+
* hundreds of window instances. See D-023, D-024, D-045, D-067, D-068, D-070.
|
|
17
|
+
*
|
|
18
|
+
* Limit hits sit in gauge columns by time, not by reset (step G1.7): a hit belongs to the column of
|
|
19
|
+
* its own window kind whose span holds it, the rule D-023 merges hits by, so a hit whose logged
|
|
20
|
+
* reset is unknown or a second off the status line's still lands in its window. Every hit no
|
|
21
|
+
* column holds is counted beside the lockouts, so the columns and that count add up to the text
|
|
22
|
+
* report's total.
|
|
23
|
+
*/
|
|
24
|
+
import { listAliases } from "../core/plans/repository-aliases.js";
|
|
25
|
+
import { lastIngestAt, } from "./queries.js";
|
|
26
|
+
/**
|
|
27
|
+
* The canonical raw line order (file, then first run, then line) from `001_ingestion.sql`, for a
|
|
28
|
+
* `raw_lines` row aliased `l`. Every {@link EventRef} is in this order, as the contract promises.
|
|
29
|
+
*/
|
|
30
|
+
const CANONICAL = "l.source_file_id, l.first_run_id, l.line_number";
|
|
31
|
+
/**
|
|
32
|
+
* Two CTEs, for the head of a `WITH`: `columns`, the gauge's columns, and `placed`, every row of
|
|
33
|
+
* `obs_limit_hits_events` with the reset of the column that holds it (`column_reset_at_utc`, null
|
|
34
|
+
* when none does). Shared text, so the columns' hits and the count of hits in no column come from
|
|
35
|
+
* one rule and always add up to `obs_limit_hits.hits`.
|
|
36
|
+
*
|
|
37
|
+
* The rule is the contract's (GaugeWindow.limitHits): same window kind, and hit time in
|
|
38
|
+
* (reset minus the window's length, reset], compared in epoch seconds as `obs_limit_hits_events`
|
|
39
|
+
* compares them when it merges hits (D-023). The hit's own `reset_at_utc` plays no part.
|
|
40
|
+
*/
|
|
41
|
+
const PLACED_HITS = `columns AS MATERIALIZED (
|
|
42
|
+
-- The gauge's columns: every window instance with a counted reading (D-024), the rows
|
|
43
|
+
-- obs_window_headroom has with readings > 0. Read from window_readings, whose counted rows
|
|
44
|
+
-- window_instances groups into those instances, because the headroom view's other columns
|
|
45
|
+
-- cost seconds on a month of real data and nothing here needs them.
|
|
46
|
+
SELECT r.window, r.reset_at_utc, r.resets_at AS reset_s,
|
|
47
|
+
CASE r.window WHEN 'five_hour' THEN 5 * 3600 ELSE 7 * 86400 END AS window_seconds
|
|
48
|
+
FROM window_readings r
|
|
49
|
+
WHERE r.after_reset = 0
|
|
50
|
+
GROUP BY r.window, r.resets_at
|
|
51
|
+
),
|
|
52
|
+
placed AS MATERIALIZED (
|
|
53
|
+
-- Materialized because the events view is costly and several aggregates read this.
|
|
54
|
+
SELECT e.raw_line_id, e.window, e.source, e.hit_at_utc, e.reset_at_utc,
|
|
55
|
+
-- An unknown window or hit time compares as NULL, so such a hit is in no column. Where
|
|
56
|
+
-- jittered resets make two same-kind spans overlap, the earliest reset takes the hit, as
|
|
57
|
+
-- obs_limit_hits_events picks the earliest status line group, so no hit counts twice.
|
|
58
|
+
(SELECT c.reset_at_utc FROM columns c
|
|
59
|
+
WHERE c.window = e.window
|
|
60
|
+
AND unixepoch(e.hit_at_utc, 'subsec') > c.reset_s - c.window_seconds
|
|
61
|
+
AND unixepoch(e.hit_at_utc, 'subsec') <= c.reset_s
|
|
62
|
+
ORDER BY c.reset_s
|
|
63
|
+
LIMIT 1) AS column_reset_at_utc
|
|
64
|
+
FROM obs_limit_hits_events e
|
|
65
|
+
)`;
|
|
66
|
+
/**
|
|
67
|
+
* Parses a JSON array of raw line IDs built by `json_group_array`.
|
|
68
|
+
* @param json - The array's text; `[]` when the group is empty.
|
|
69
|
+
* @returns The IDs, in the array's order.
|
|
70
|
+
*/
|
|
71
|
+
function parseIds(json) {
|
|
72
|
+
// Integers below 2^53 round-trip through JSON exactly, so no ID changes on the way.
|
|
73
|
+
return JSON.parse(json);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Wraps a single optional raw line ID as an {@link EventRef}.
|
|
77
|
+
* @param id - A raw line ID, or null when the value has no event behind it.
|
|
78
|
+
* @returns A one-element list, or an empty one for null.
|
|
79
|
+
*/
|
|
80
|
+
function single(id) {
|
|
81
|
+
return id === null ? [] : [id];
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Loads how far back each source reaches.
|
|
85
|
+
* @param db - Open, migrated database.
|
|
86
|
+
* @returns One row per source, `session_logs` then `status_line`; spans are null for a source with no data.
|
|
87
|
+
*/
|
|
88
|
+
function loadCoverage(db) {
|
|
89
|
+
return db
|
|
90
|
+
.prepare("SELECT source, covers_from, covers_to FROM source_coverage ORDER BY source")
|
|
91
|
+
.all().map((row) => ({ source: row.source, span: { from: row.covers_from, to: row.covers_to } }));
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Loads every window instance with at least one counted reading, with its readings and limit hits.
|
|
95
|
+
* @param db - Open, migrated database.
|
|
96
|
+
* @param at - The page's time in Unix seconds: a window is open only until its reset, as of it (D-083).
|
|
97
|
+
* @returns One row per window instance, oldest reset first; empty with no counted reading.
|
|
98
|
+
* @see D-024 for what a window instance is and which readings count, D-045 for observed times,
|
|
99
|
+
* {@link PLACED_HITS} for which hits a window holds.
|
|
100
|
+
*/
|
|
101
|
+
function loadWindows(db, at) {
|
|
102
|
+
return db
|
|
103
|
+
.prepare(`WITH ${PLACED_HITS},
|
|
104
|
+
hits AS (
|
|
105
|
+
-- Each column's hits by the shared rule. A placed hit always has a time, so every hit in
|
|
106
|
+
-- the gauge also sits on its window's line over time.
|
|
107
|
+
SELECT x.window, x.column_reset_at_utc AS reset_at_utc,
|
|
108
|
+
COUNT(*) AS limit_hits,
|
|
109
|
+
json_group_array(x.raw_line_id ORDER BY ${CANONICAL}) AS hit_ids,
|
|
110
|
+
json_group_array(json_array(x.hit_at_utc, x.raw_line_id) ORDER BY x.hit_at_utc, x.raw_line_id)
|
|
111
|
+
AS series_hits
|
|
112
|
+
FROM placed x
|
|
113
|
+
JOIN raw_lines l ON l.id = x.raw_line_id
|
|
114
|
+
WHERE x.column_reset_at_utc IS NOT NULL
|
|
115
|
+
GROUP BY x.window, x.column_reset_at_utc
|
|
116
|
+
),
|
|
117
|
+
points AS (
|
|
118
|
+
-- The readings window_instances counts (after_reset = 0), in the order the reading-pair
|
|
119
|
+
-- view walks them, so a series has exactly the window's readings count of points (D-024).
|
|
120
|
+
SELECT r.window, r.reset_at_utc,
|
|
121
|
+
json_group_array(json_array(r.observed_at_utc, r.used_percentage, r.raw_line_id, r.stale)
|
|
122
|
+
ORDER BY r.observed_at_s, r.raw_line_id) AS points
|
|
123
|
+
FROM window_readings r
|
|
124
|
+
WHERE r.after_reset = 0
|
|
125
|
+
GROUP BY r.window, r.resets_at
|
|
126
|
+
)
|
|
127
|
+
-- Open only until its reset, as of the page's time (D-083), as the text report reads it.
|
|
128
|
+
SELECT h.window, h.reset_at_utc,
|
|
129
|
+
(h.window_open = 1 AND unixepoch(h.reset_at_utc, 'subsec') > :at) AS window_open,
|
|
130
|
+
p.peak_used_percentage, p.peak_reading_at_utc, p.peak_reading_raw_line_id,
|
|
131
|
+
h.last_used_percentage, h.last_reading_at_utc, h.last_reading_raw_line_id,
|
|
132
|
+
h.readings,
|
|
133
|
+
COALESCE(x.limit_hits, 0) AS limit_hits,
|
|
134
|
+
COALESCE(x.hit_ids, '[]') AS hit_ids,
|
|
135
|
+
COALESCE(x.series_hits, '[]') AS series_hits,
|
|
136
|
+
COALESCE(r.points, '[]') AS points
|
|
137
|
+
FROM obs_window_headroom h
|
|
138
|
+
-- The same join loadObserved uses, so every number here equals the text report's.
|
|
139
|
+
JOIN obs_window_peak p ON p.window = h.window AND p.reset_at_utc = h.reset_at_utc
|
|
140
|
+
LEFT JOIN hits x ON x.window = h.window AND x.reset_at_utc = h.reset_at_utc
|
|
141
|
+
LEFT JOIN points r ON r.window = h.window AND r.reset_at_utc = h.reset_at_utc
|
|
142
|
+
-- A window whose every reading came after its reset has nothing to draw (D-024).
|
|
143
|
+
WHERE h.readings > 0
|
|
144
|
+
ORDER BY h.reset_at_utc, h.window`)
|
|
145
|
+
.all({ at });
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Maps a window instance onto its gauge column.
|
|
149
|
+
* @param row - From {@link loadWindows}.
|
|
150
|
+
* @returns The column; `peak` may exceed 100 (D-068).
|
|
151
|
+
*/
|
|
152
|
+
function toGauge(row) {
|
|
153
|
+
return {
|
|
154
|
+
window: row.window,
|
|
155
|
+
resetAtUtc: row.reset_at_utc,
|
|
156
|
+
// NULL can't occur with a counted reading, but it would mean "not known open", so it reads false.
|
|
157
|
+
open: row.window_open === 1,
|
|
158
|
+
peak: row.peak_used_percentage,
|
|
159
|
+
peakAtUtc: row.peak_reading_at_utc,
|
|
160
|
+
last: row.last_used_percentage,
|
|
161
|
+
lastAtUtc: row.last_reading_at_utc,
|
|
162
|
+
readings: row.readings,
|
|
163
|
+
limitHits: row.limit_hits,
|
|
164
|
+
peakEvents: single(row.peak_reading_raw_line_id),
|
|
165
|
+
lastEvents: single(row.last_reading_raw_line_id),
|
|
166
|
+
hitEvents: parseIds(row.hit_ids),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Maps a window instance onto its usage-over-time series.
|
|
171
|
+
* @param row - From {@link loadWindows}.
|
|
172
|
+
* @returns The series: one point per counted reading, dated when observed (D-045), and its hits.
|
|
173
|
+
*/
|
|
174
|
+
function toSeries(row) {
|
|
175
|
+
// Real values round-trip through SQLite's JSON exactly (17 significant digits), so each point
|
|
176
|
+
// keeps the recorded used_percentage unrounded, including values above 100 (D-068).
|
|
177
|
+
const points = JSON.parse(row.points).map(([atUtc, used, event, stale]) => ({ atUtc, used, event, stale: stale === 1 }));
|
|
178
|
+
const hits = JSON.parse(row.series_hits).map(([atUtc, event]) => ({ atUtc, event }));
|
|
179
|
+
return { window: row.window, resetAtUtc: row.reset_at_utc, points, hits };
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Maps a plan period's columns onto the contract's reference, with its last month as the view
|
|
183
|
+
* computes it for the per-plan table (the month before `until_month`).
|
|
184
|
+
* @param row - The period's columns.
|
|
185
|
+
* @returns The reference.
|
|
186
|
+
*/
|
|
187
|
+
function periodRef(row) {
|
|
188
|
+
return {
|
|
189
|
+
kind: row.period_kind,
|
|
190
|
+
fromMonth: row.from_month,
|
|
191
|
+
toMonth: row.to_month ?? null,
|
|
192
|
+
untilMonth: row.until_month,
|
|
193
|
+
planName: row.plan_name,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Loads the lockout intervals, each with the hits merged into it.
|
|
198
|
+
* @param db - Open, migrated database.
|
|
199
|
+
* @param withPeriods - Whether to tag each interval with its plan period (D-075): only once a plan
|
|
200
|
+
* has been entered, as the text report only then prints the split.
|
|
201
|
+
* @returns Intervals in time order; empty with no hit whose reset is known.
|
|
202
|
+
* @see D-023 for the interval (hit to reset, overlaps merged) and reset to next request.
|
|
203
|
+
*/
|
|
204
|
+
function loadLockouts(db, withPeriods = false) {
|
|
205
|
+
const rows = db
|
|
206
|
+
.prepare(`WITH members AS (
|
|
207
|
+
SELECT h.interval_number, json_group_array(h.raw_line_id ORDER BY ${CANONICAL}) AS hit_ids
|
|
208
|
+
FROM obs_lockout_interval_hits h
|
|
209
|
+
JOIN raw_lines l ON l.id = h.raw_line_id
|
|
210
|
+
GROUP BY h.interval_number
|
|
211
|
+
)
|
|
212
|
+
SELECT i.interval_number, i.locked_from_utc, i.locked_until_utc, i.hits, i.refused_requests,
|
|
213
|
+
i.sessions, i.lockout_seconds,
|
|
214
|
+
i.auto_continuations, i.first_auto_continuation_seconds,
|
|
215
|
+
-- The markers obs_lockout_intervals counts, so the list re-adds to auto_continuations.
|
|
216
|
+
COALESCE((SELECT json_group_array(a.raw_line_id ORDER BY ${CANONICAL})
|
|
217
|
+
FROM obs_auto_continuation_events a
|
|
218
|
+
JOIN raw_lines l ON l.id = a.raw_line_id
|
|
219
|
+
WHERE a.interval_number = i.interval_number), '[]') AS auto_continuation_ids,
|
|
220
|
+
i.reset_to_next_request_seconds, i.reset_after_coverage,
|
|
221
|
+
COALESCE(m.hit_ids, '[]') AS hit_ids,
|
|
222
|
+
-- The request behind the view's next_request_at_utc: matching that value, rather than
|
|
223
|
+
-- restating its rule, keeps the event tied to the number shown. Requests sharing that
|
|
224
|
+
-- instant are equally behind it, so the first in canonical order stands for them.
|
|
225
|
+
(SELECT d.raw_line_id FROM requests_dedup d
|
|
226
|
+
JOIN raw_lines l ON l.id = d.raw_line_id
|
|
227
|
+
WHERE d.timestamp_utc = i.next_request_at_utc
|
|
228
|
+
ORDER BY ${CANONICAL}
|
|
229
|
+
LIMIT 1) AS next_request_raw_line_id
|
|
230
|
+
FROM obs_lockout_intervals i
|
|
231
|
+
LEFT JOIN members m ON m.interval_number = i.interval_number
|
|
232
|
+
-- Intervals are numbered in start order, as loadObserved reads them.
|
|
233
|
+
ORDER BY i.interval_number`)
|
|
234
|
+
.all();
|
|
235
|
+
const periods = new Map((withPeriods
|
|
236
|
+
? db
|
|
237
|
+
.prepare(
|
|
238
|
+
// to_month as obs_interruptions_by_plan computes it, so a lane names the same months.
|
|
239
|
+
`SELECT interval_number, period_kind, from_month, until_month, plan_name,
|
|
240
|
+
CASE WHEN until_month IS NOT NULL
|
|
241
|
+
THEN strftime('%Y-%m', until_month || '-01', '-1 month') END AS to_month
|
|
242
|
+
FROM obs_lockout_by_plan_events`)
|
|
243
|
+
.all()
|
|
244
|
+
: []).map((row) => [row.interval_number, periodRef(row)]));
|
|
245
|
+
return rows.map((row) => ({
|
|
246
|
+
...(periods.has(row.interval_number)
|
|
247
|
+
? { period: periods.get(row.interval_number) }
|
|
248
|
+
: {}),
|
|
249
|
+
lockedFromUtc: row.locked_from_utc,
|
|
250
|
+
lockedUntilUtc: row.locked_until_utc,
|
|
251
|
+
seconds: row.lockout_seconds,
|
|
252
|
+
hits: row.hits,
|
|
253
|
+
refusedRequests: row.refused_requests,
|
|
254
|
+
sessions: row.sessions,
|
|
255
|
+
resetToNextRequestSeconds: row.reset_to_next_request_seconds,
|
|
256
|
+
// NULL means there are no session logs to compare with; nothing says the reset came after
|
|
257
|
+
// them, so it reads false and the null reset-to-next-request stands on its own.
|
|
258
|
+
resetAfterCoverage: row.reset_after_coverage === 1,
|
|
259
|
+
events: parseIds(row.hit_ids),
|
|
260
|
+
nextRequestEvents: single(row.next_request_raw_line_id),
|
|
261
|
+
autoContinuations: row.auto_continuations,
|
|
262
|
+
firstAutoContinuationSeconds: row.first_auto_continuation_seconds,
|
|
263
|
+
autoContinuationEvents: parseIds(row.auto_continuation_ids),
|
|
264
|
+
}));
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Loads the interruption counts the text report prints beside the lockouts, with their hits.
|
|
268
|
+
* @param db - Open, migrated database.
|
|
269
|
+
* @param totals - The text report's `obs_limit_hits` and `obs_lockout_time` rows, when the caller
|
|
270
|
+
* already read them in the same transaction; read here otherwise. Both views are among the
|
|
271
|
+
* costliest on real data, so reading them once matters.
|
|
272
|
+
* @returns The totals, and the hits behind each count, including those no gauge column holds.
|
|
273
|
+
* @see D-023 for merging and unknown resets, {@link PLACED_HITS} for which hits a column holds.
|
|
274
|
+
*/
|
|
275
|
+
function loadInterruptions(db, totals) {
|
|
276
|
+
const { limitHits, lockout, interruptionsByPlan } = totals ?? {
|
|
277
|
+
limitHits: db.prepare("SELECT * FROM obs_limit_hits").get(),
|
|
278
|
+
lockout: db.prepare("SELECT * FROM obs_lockout_time").get(),
|
|
279
|
+
interruptionsByPlan: db
|
|
280
|
+
.prepare(`SELECT * FROM obs_interruptions_by_plan
|
|
281
|
+
ORDER BY CASE period_kind WHEN 'none' THEN 0 WHEN 'plan' THEN 1 ELSE 2 END, from_month`)
|
|
282
|
+
.all(),
|
|
283
|
+
};
|
|
284
|
+
const row = db
|
|
285
|
+
.prepare(`WITH ${PLACED_HITS},
|
|
286
|
+
lists AS (
|
|
287
|
+
-- placed is obs_limit_hits_events row for row, so these lists are the view's hits.
|
|
288
|
+
SELECT
|
|
289
|
+
json_group_array(p.raw_line_id ORDER BY ${CANONICAL}) AS hit_ids,
|
|
290
|
+
-- The same test obs_lockout_time counts hits_with_unknown_reset by.
|
|
291
|
+
json_group_array(p.raw_line_id ORDER BY ${CANONICAL})
|
|
292
|
+
FILTER (WHERE p.reset_at_utc IS NULL OR p.hit_at_utc IS NULL) AS unknown_reset_ids,
|
|
293
|
+
COUNT(*) FILTER (WHERE p.column_reset_at_utc IS NULL) AS hits_in_no_column,
|
|
294
|
+
json_group_array(p.raw_line_id ORDER BY ${CANONICAL})
|
|
295
|
+
FILTER (WHERE p.column_reset_at_utc IS NULL) AS no_column_ids,
|
|
296
|
+
-- The same FILTERs obs_limit_hits counts its splits by, so each split number and its
|
|
297
|
+
-- list come from one rule (principle 4).
|
|
298
|
+
json_group_array(p.raw_line_id ORDER BY ${CANONICAL})
|
|
299
|
+
FILTER (WHERE p.window = 'five_hour') AS five_hour_ids,
|
|
300
|
+
json_group_array(p.raw_line_id ORDER BY ${CANONICAL})
|
|
301
|
+
FILTER (WHERE p.window = 'seven_day') AS seven_day_ids,
|
|
302
|
+
json_group_array(p.raw_line_id ORDER BY ${CANONICAL})
|
|
303
|
+
FILTER (WHERE p.window IS NULL) AS unknown_window_ids,
|
|
304
|
+
json_group_array(p.raw_line_id ORDER BY ${CANONICAL})
|
|
305
|
+
FILTER (WHERE p.source = 'session_log') AS logged_ids,
|
|
306
|
+
json_group_array(p.raw_line_id ORDER BY ${CANONICAL})
|
|
307
|
+
FILTER (WHERE p.source = 'status_line') AS status_line_only_ids
|
|
308
|
+
FROM placed p
|
|
309
|
+
JOIN raw_lines l ON l.id = p.raw_line_id
|
|
310
|
+
)
|
|
311
|
+
-- Ungrouped, lists always has one row, and json_group_array gives [] over no rows.
|
|
312
|
+
SELECT * FROM lists`)
|
|
313
|
+
.get();
|
|
314
|
+
return {
|
|
315
|
+
hits: limitHits.hits,
|
|
316
|
+
fiveHourHits: limitHits.five_hour_hits,
|
|
317
|
+
sevenDayHits: limitHits.seven_day_hits,
|
|
318
|
+
unknownWindowHits: limitHits.unknown_window_hits,
|
|
319
|
+
loggedHits: limitHits.logged_hits,
|
|
320
|
+
statusLineOnlyHits: limitHits.status_line_only_hits,
|
|
321
|
+
lockoutSeconds: lockout.lockout_seconds,
|
|
322
|
+
lockoutIntervals: lockout.intervals,
|
|
323
|
+
hitsWithUnknownReset: lockout.hits_with_unknown_reset,
|
|
324
|
+
unknownResetEvents: parseIds(row.unknown_reset_ids),
|
|
325
|
+
hitsInNoColumn: row.hits_in_no_column,
|
|
326
|
+
hitsInNoColumnEvents: parseIds(row.no_column_ids),
|
|
327
|
+
events: parseIds(row.hit_ids),
|
|
328
|
+
fiveHourEvents: parseIds(row.five_hour_ids),
|
|
329
|
+
sevenDayEvents: parseIds(row.seven_day_ids),
|
|
330
|
+
unknownWindowEvents: parseIds(row.unknown_window_ids),
|
|
331
|
+
loggedEvents: parseIds(row.logged_ids),
|
|
332
|
+
statusLineOnlyEvents: parseIds(row.status_line_only_ids),
|
|
333
|
+
// obs_limit_hits carries the session logs' span from source_coverage, the span the text
|
|
334
|
+
// report prints beside these counts (principle 5).
|
|
335
|
+
span: { from: limitHits.covers_from, to: limitHits.covers_to },
|
|
336
|
+
byPlan: loadByPlan(db, interruptionsByPlan),
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Loads the per-plan interruptions (D-075), with the events behind each count, only once a plan
|
|
341
|
+
* has been entered: the text report prints the split only then.
|
|
342
|
+
* @param db - Open, migrated database.
|
|
343
|
+
* @param rows - `obs_interruptions_by_plan`, as the text report read it.
|
|
344
|
+
* @returns One entry per row, in the rows' order; empty with no plan entered.
|
|
345
|
+
*/
|
|
346
|
+
function loadByPlan(db, rows) {
|
|
347
|
+
if (!rows.some((row) => row.period_kind === "plan")) {
|
|
348
|
+
return [];
|
|
349
|
+
}
|
|
350
|
+
// Grouped by the same two columns obs_interruptions_by_plan counts by, so each list re-adds.
|
|
351
|
+
const hitLists = db
|
|
352
|
+
.prepare(`SELECT b.period_kind, b.from_month, json_group_array(b.raw_line_id ORDER BY ${CANONICAL}) AS ids
|
|
353
|
+
FROM obs_limit_hits_by_plan_events b
|
|
354
|
+
JOIN raw_lines l ON l.id = b.raw_line_id
|
|
355
|
+
GROUP BY b.period_kind, b.from_month`)
|
|
356
|
+
.all();
|
|
357
|
+
const lockoutLists = db
|
|
358
|
+
.prepare(`SELECT i.period_kind, i.from_month, json_group_array(h.raw_line_id ORDER BY ${CANONICAL}) AS ids
|
|
359
|
+
FROM obs_lockout_by_plan_events i
|
|
360
|
+
JOIN obs_lockout_interval_hits h ON h.interval_number = i.interval_number
|
|
361
|
+
JOIN raw_lines l ON l.id = h.raw_line_id
|
|
362
|
+
GROUP BY i.period_kind, i.from_month`)
|
|
363
|
+
.all();
|
|
364
|
+
/**
|
|
365
|
+
* Finds a period's list.
|
|
366
|
+
* @param lists - Lists keyed by kind and first month.
|
|
367
|
+
* @param row - The period.
|
|
368
|
+
* @returns Its IDs; empty when it has none.
|
|
369
|
+
*/
|
|
370
|
+
const listOf = (lists, row) => parseIds(lists.find((x) => x.period_kind === row.period_kind && x.from_month === row.from_month)
|
|
371
|
+
?.ids ?? "[]");
|
|
372
|
+
return rows.map((row) => ({
|
|
373
|
+
period: periodRef(row),
|
|
374
|
+
hits: row.hits,
|
|
375
|
+
lockouts: row.lockouts,
|
|
376
|
+
lockoutSeconds: row.lockout_seconds,
|
|
377
|
+
span: { from: row.covers_from, to: row.covers_to },
|
|
378
|
+
hitEvents: listOf(hitLists, row),
|
|
379
|
+
lockoutEvents: listOf(lockoutLists, row),
|
|
380
|
+
}));
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Loads the weekly windows' first readings at or above 75% and 90% (D-087), reusing the text
|
|
384
|
+
* report's rows when it has read them from the same snapshot.
|
|
385
|
+
* @param db - Open, migrated database.
|
|
386
|
+
* @param observed - What the text report read, if it did.
|
|
387
|
+
* @returns One entry per window and level, newest window first.
|
|
388
|
+
*/
|
|
389
|
+
function loadCrossings(db, observed) {
|
|
390
|
+
// The same view and order as loadObserved, so the page and the text list the same rows.
|
|
391
|
+
const rows = observed?.weeklyCrossings ??
|
|
392
|
+
db
|
|
393
|
+
.prepare("SELECT * FROM obs_weekly_crossings ORDER BY reset_at_utc DESC, threshold")
|
|
394
|
+
.all();
|
|
395
|
+
return rows.map((r) => ({
|
|
396
|
+
resetAtUtc: r.reset_at_utc,
|
|
397
|
+
threshold: r.threshold,
|
|
398
|
+
firstAtUtc: r.first_at_utc,
|
|
399
|
+
usedPercentage: r.used_percentage,
|
|
400
|
+
secondsBeforeReset: r.seconds_before_reset,
|
|
401
|
+
events: [r.raw_line_id],
|
|
402
|
+
}));
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Loads the readings captured after their window's reset, which no gauge column counts.
|
|
406
|
+
* @param db - Open, migrated database.
|
|
407
|
+
* @returns How many, and which.
|
|
408
|
+
* @see D-024 for why a reading after its reset belongs to no window.
|
|
409
|
+
*/
|
|
410
|
+
function loadAfterReset(db) {
|
|
411
|
+
const row = db
|
|
412
|
+
.prepare(
|
|
413
|
+
// window_instances counts readings_after_reset as these rows grouped by window instance, so
|
|
414
|
+
// counting them ungrouped gives the sum the text report prints, without the headroom view.
|
|
415
|
+
`SELECT COUNT(*) AS count,
|
|
416
|
+
json_group_array(r.raw_line_id ORDER BY ${CANONICAL}) AS ids
|
|
417
|
+
FROM window_readings r
|
|
418
|
+
JOIN raw_lines l ON l.id = r.raw_line_id
|
|
419
|
+
WHERE r.after_reset = 1`)
|
|
420
|
+
.get();
|
|
421
|
+
return { count: row.count, events: parseIds(row.ids) };
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Loads output tokens by model, each with its deduplicated requests.
|
|
425
|
+
* @param db - Open, migrated database.
|
|
426
|
+
* @returns Models by output tokens, largest first, ties by name as the text report orders them.
|
|
427
|
+
* @see D-001 for deduplication, D-007 for per-request attribution.
|
|
428
|
+
*/
|
|
429
|
+
function loadModels(db) {
|
|
430
|
+
const rows = db
|
|
431
|
+
.prepare(`WITH members AS (
|
|
432
|
+
-- obs_usage_events is the set obs_usage_by_model counts, so the IDs re-add to its numbers.
|
|
433
|
+
SELECT e.model, json_group_array(e.raw_line_id ORDER BY ${CANONICAL}) AS request_ids
|
|
434
|
+
FROM obs_usage_events e
|
|
435
|
+
JOIN raw_lines l ON l.id = e.raw_line_id
|
|
436
|
+
GROUP BY e.model
|
|
437
|
+
)
|
|
438
|
+
SELECT m.model, m.requests, m.output_tokens, COALESCE(x.request_ids, '[]') AS request_ids
|
|
439
|
+
FROM obs_usage_by_model m
|
|
440
|
+
LEFT JOIN members x ON x.model = m.model
|
|
441
|
+
ORDER BY m.output_tokens DESC, m.model`)
|
|
442
|
+
.all();
|
|
443
|
+
return rows.map((row) => ({
|
|
444
|
+
model: row.model,
|
|
445
|
+
requests: row.requests,
|
|
446
|
+
outputTokens: row.output_tokens,
|
|
447
|
+
events: parseIds(row.request_ids),
|
|
448
|
+
}));
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Loads output tokens by repository, each with its deduplicated requests (step G2.1).
|
|
452
|
+
* @param db - Open, migrated database.
|
|
453
|
+
* @returns Repositories by output tokens, largest first, ties by name, as the text report orders them.
|
|
454
|
+
* @see D-010 and D-028 for attribution, D-001 for deduplication.
|
|
455
|
+
*/
|
|
456
|
+
function loadRepos(db) {
|
|
457
|
+
const rows = db
|
|
458
|
+
.prepare(`WITH members AS (
|
|
459
|
+
-- obs_usage_events is the set obs_usage_by_repo counts, grouped the same way, so each
|
|
460
|
+
-- list's IDs re-add to its row. explain by-repo matches on the same two columns.
|
|
461
|
+
SELECT e.repository, e.repo_kind,
|
|
462
|
+
json_group_array(e.raw_line_id ORDER BY ${CANONICAL}) AS request_ids
|
|
463
|
+
FROM obs_usage_events e
|
|
464
|
+
JOIN raw_lines l ON l.id = e.raw_line_id
|
|
465
|
+
GROUP BY e.repository, e.repo_kind
|
|
466
|
+
)
|
|
467
|
+
SELECT r.repository, r.repo_kind, r.requests, r.output_tokens,
|
|
468
|
+
COALESCE(x.request_ids, '[]') AS request_ids
|
|
469
|
+
FROM obs_usage_by_repo r
|
|
470
|
+
-- IS, not =: a request with no cwd has a NULL repository, and its group must still match.
|
|
471
|
+
LEFT JOIN members x ON x.repository IS r.repository AND x.repo_kind = r.repo_kind
|
|
472
|
+
ORDER BY r.output_tokens DESC, r.repository`)
|
|
473
|
+
.all();
|
|
474
|
+
return rows.map((row) => ({
|
|
475
|
+
repository: row.repository,
|
|
476
|
+
kind: row.repo_kind,
|
|
477
|
+
requests: row.requests,
|
|
478
|
+
outputTokens: row.output_tokens,
|
|
479
|
+
events: parseIds(row.request_ids),
|
|
480
|
+
}));
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Loads the burn rate of every open window, a projection, each with the reading it starts from
|
|
484
|
+
* (step G2.1). Read by its own function from its own view, never mixed into observed rows
|
|
485
|
+
* (principle 2).
|
|
486
|
+
* @param db - Open, migrated database.
|
|
487
|
+
* @param at - The page's time in Unix seconds: only windows still open at it are projected (D-083).
|
|
488
|
+
* @returns Open windows in the text report's order: latest reset first, then by window.
|
|
489
|
+
* @see D-026 for the premise, D-024 for what an open window is.
|
|
490
|
+
*/
|
|
491
|
+
function loadBurn(db, at) {
|
|
492
|
+
const rows = db
|
|
493
|
+
.prepare(
|
|
494
|
+
// The same join explain burn-rate uses for the reading behind each projection, and the same
|
|
495
|
+
// filter renderProjected prints by (window_open = 1), so the page and the text report agree.
|
|
496
|
+
`SELECT b.window, b.reset_at_utc, b.window_start_utc, b.last_used_percentage,
|
|
497
|
+
b.last_reading_at_utc, b.limit_reached, b.projected_percentage_points_per_hour,
|
|
498
|
+
b.projected_limit_at_utc, b.projected_limit_before_reset, b.covers_from, b.covers_to,
|
|
499
|
+
h.last_reading_raw_line_id
|
|
500
|
+
FROM proj_burn_rate b
|
|
501
|
+
JOIN obs_window_headroom h ON h.window = b.window AND h.reset_at_utc = b.reset_at_utc
|
|
502
|
+
-- Open at the page's time too: a window that has reset has nothing to project (D-083).
|
|
503
|
+
WHERE b.window_open = 1 AND unixepoch(b.reset_at_utc, 'subsec') > :at
|
|
504
|
+
ORDER BY b.reset_at_utc DESC, b.window`)
|
|
505
|
+
.all({ at });
|
|
506
|
+
return rows.map((row) => ({
|
|
507
|
+
window: row.window,
|
|
508
|
+
resetAtUtc: row.reset_at_utc,
|
|
509
|
+
windowStartUtc: row.window_start_utc,
|
|
510
|
+
last: row.last_used_percentage,
|
|
511
|
+
lastAtUtc: row.last_reading_at_utc,
|
|
512
|
+
// NULL means no reading to judge by; nothing says the limit was reached, so it reads false.
|
|
513
|
+
limitReached: row.limit_reached === 1,
|
|
514
|
+
ratePerHour: row.projected_percentage_points_per_hour,
|
|
515
|
+
projectedLimitAtUtc: row.projected_limit_at_utc,
|
|
516
|
+
limitBeforeReset: row.projected_limit_before_reset === 1,
|
|
517
|
+
span: { from: row.covers_from, to: row.covers_to },
|
|
518
|
+
events: single(row.last_reading_raw_line_id),
|
|
519
|
+
}));
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Loads observed tokens at API list price by month, a projection, each with its priced requests.
|
|
523
|
+
* @param db - Open, migrated database.
|
|
524
|
+
* @returns Months in order; empty with no request that has a timestamp.
|
|
525
|
+
* @see D-005 (unpriced is never $0), D-007 (local months), D-027 (repricing is a labeled projection).
|
|
526
|
+
*/
|
|
527
|
+
function loadMonths(db) {
|
|
528
|
+
const rows = db
|
|
529
|
+
.prepare(`WITH members AS (
|
|
530
|
+
-- The same month expression and filters as proj_api_list_price, split by the same
|
|
531
|
+
-- unpriced_reason test as its counts, so each month's lists have exactly priced_requests
|
|
532
|
+
-- and unpriced_requests IDs, and the priced list re-adds to its price.
|
|
533
|
+
SELECT strftime('%Y-%m', e.timestamp_utc, 'localtime') AS month,
|
|
534
|
+
json_group_array(e.raw_line_id ORDER BY ${CANONICAL})
|
|
535
|
+
FILTER (WHERE e.unpriced_reason IS NULL) AS request_ids,
|
|
536
|
+
json_group_array(e.raw_line_id ORDER BY ${CANONICAL})
|
|
537
|
+
FILTER (WHERE e.unpriced_reason IS NOT NULL) AS unpriced_ids
|
|
538
|
+
FROM request_costs e
|
|
539
|
+
JOIN raw_lines l ON l.id = e.raw_line_id
|
|
540
|
+
WHERE e.timestamp_utc IS NOT NULL
|
|
541
|
+
GROUP BY month
|
|
542
|
+
)
|
|
543
|
+
SELECT p.month, p.api_list_price_usd, p.plan_name, p.plan_usd_per_month,
|
|
544
|
+
p.priced_requests, p.unpriced_requests, p.covers_from, p.covers_to,
|
|
545
|
+
COALESCE(x.request_ids, '[]') AS request_ids,
|
|
546
|
+
COALESCE(x.unpriced_ids, '[]') AS unpriced_ids
|
|
547
|
+
FROM proj_api_list_price p
|
|
548
|
+
-- Both group the same rows by the same expression, so every month finds its lists; the
|
|
549
|
+
-- outer join and COALESCE only keep a month's row should that ever stop holding.
|
|
550
|
+
LEFT JOIN members x ON x.month = p.month
|
|
551
|
+
ORDER BY p.month`)
|
|
552
|
+
.all();
|
|
553
|
+
return rows.map((row) => ({
|
|
554
|
+
month: row.month,
|
|
555
|
+
apiListPriceUsd: row.api_list_price_usd,
|
|
556
|
+
planName: row.plan_name,
|
|
557
|
+
planUsdPerMonth: row.plan_usd_per_month,
|
|
558
|
+
pricedRequests: row.priced_requests,
|
|
559
|
+
unpricedRequests: row.unpriced_requests,
|
|
560
|
+
unpricedEvents: parseIds(row.unpriced_ids),
|
|
561
|
+
span: { from: row.covers_from, to: row.covers_to },
|
|
562
|
+
events: parseIds(row.request_ids),
|
|
563
|
+
}));
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Looks up the source file, line, and time of every given raw line.
|
|
567
|
+
* @param db - Open, migrated database.
|
|
568
|
+
* @param ids - Raw line IDs, duplicates allowed.
|
|
569
|
+
* @returns The files, each path stored once, and each event pointing at its file by index.
|
|
570
|
+
*/
|
|
571
|
+
function loadEvents(db, ids) {
|
|
572
|
+
const rows = db
|
|
573
|
+
.prepare(`SELECT l.id, f.relative_path, l.line_number,
|
|
574
|
+
-- A log line's time is its parsed timestamp. A spool line has no parsed line, so its time
|
|
575
|
+
-- is the capture: where the reading was recorded, while the point on the chart is dated by
|
|
576
|
+
-- the response it reports (D-045).
|
|
577
|
+
CASE f.kind
|
|
578
|
+
WHEN 'spool' THEN strftime('%Y-%m-%dT%H:%M:%fZ', s.captured_at_s, 'unixepoch')
|
|
579
|
+
ELSE p.timestamp_utc
|
|
580
|
+
END AS at_utc
|
|
581
|
+
FROM raw_lines l
|
|
582
|
+
JOIN source_files f ON f.id = l.source_file_id
|
|
583
|
+
LEFT JOIN parsed_lines p ON p.raw_line_id = l.id
|
|
584
|
+
LEFT JOIN status_readings s ON s.raw_line_id = l.id
|
|
585
|
+
-- One JSON parameter carries any number of IDs, where a placeholder list would hit SQLite's limit.
|
|
586
|
+
WHERE l.id IN (SELECT value FROM json_each(?))
|
|
587
|
+
ORDER BY ${CANONICAL}`)
|
|
588
|
+
.all(JSON.stringify(ids));
|
|
589
|
+
const files = [];
|
|
590
|
+
const fileIndex = new Map();
|
|
591
|
+
const events = {};
|
|
592
|
+
for (const row of rows) {
|
|
593
|
+
// relative_path, never root: the file must not carry machine-specific absolute paths (CLAUDE.md).
|
|
594
|
+
let file = fileIndex.get(row.relative_path);
|
|
595
|
+
if (file === undefined) {
|
|
596
|
+
file = files.length;
|
|
597
|
+
files.push(row.relative_path);
|
|
598
|
+
fileIndex.set(row.relative_path, file);
|
|
599
|
+
}
|
|
600
|
+
events[row.id] = { file, line: row.line_number, atUtc: row.at_utc };
|
|
601
|
+
}
|
|
602
|
+
return { files, events };
|
|
603
|
+
}
|
|
604
|
+
/**
|
|
605
|
+
* Loads the notes under the monthly amounts with the requests behind each count: the rate-reading
|
|
606
|
+
* counts by the text report's own rule (`loadProjected`'s rateReadings query) and the lower-bound
|
|
607
|
+
* count by `proj_api_list_price`'s (`is_lower_bound = 1` over dated requests).
|
|
608
|
+
* @param db - Open, migrated database.
|
|
609
|
+
* @returns The notes; empty lists and 0 when no request qualifies.
|
|
610
|
+
* @see D-020 for rates verified after a request's date, D-005 for lower-bound pricing.
|
|
611
|
+
*/
|
|
612
|
+
function loadPriceNotes(db) {
|
|
613
|
+
const readings = db
|
|
614
|
+
.prepare(`SELECT c.verified_on, COUNT(*) AS requests,
|
|
615
|
+
json_group_array(c.raw_line_id ORDER BY ${CANONICAL}) AS ids
|
|
616
|
+
FROM request_costs c
|
|
617
|
+
JOIN raw_lines l ON l.id = c.raw_line_id
|
|
618
|
+
WHERE c.timestamp_utc IS NOT NULL AND c.unpriced_reason IS NULL AND c.day_utc < c.verified_on
|
|
619
|
+
GROUP BY c.verified_on
|
|
620
|
+
ORDER BY c.verified_on`)
|
|
621
|
+
.all();
|
|
622
|
+
const lower = db
|
|
623
|
+
.prepare(
|
|
624
|
+
// Ungrouped, this always returns one row, with [] for no rows.
|
|
625
|
+
`SELECT COUNT(*) AS requests, json_group_array(c.raw_line_id ORDER BY ${CANONICAL}) AS ids
|
|
626
|
+
FROM request_costs c
|
|
627
|
+
JOIN raw_lines l ON l.id = c.raw_line_id
|
|
628
|
+
WHERE c.timestamp_utc IS NOT NULL AND c.is_lower_bound = 1`)
|
|
629
|
+
.get();
|
|
630
|
+
return {
|
|
631
|
+
rateReadings: readings.map((row) => ({
|
|
632
|
+
verifiedOn: row.verified_on,
|
|
633
|
+
pricedBeforeVerifiedRequests: row.requests,
|
|
634
|
+
events: parseIds(row.ids),
|
|
635
|
+
})),
|
|
636
|
+
lowerBoundRequests: lower.requests,
|
|
637
|
+
lowerBoundEvents: parseIds(lower.ids),
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
/**
|
|
641
|
+
* Loads everything the HTML report draws, from the same views as the text report.
|
|
642
|
+
* @param db - Open, migrated database.
|
|
643
|
+
* @param meta - The display zone and the production time.
|
|
644
|
+
* @param loaded - What the text report already read from this database in the same transaction;
|
|
645
|
+
* its interruption totals are reused instead of computed again. Omitted, they're read here, with
|
|
646
|
+
* the same result.
|
|
647
|
+
* @returns The report's data: observed charts, projected months, and every event they refer to.
|
|
648
|
+
* @see D-070.
|
|
649
|
+
*/
|
|
650
|
+
export const loadHtmlData = (db, meta, loaded) => {
|
|
651
|
+
// The page's own time, in Unix seconds: "open" is as of it (D-083).
|
|
652
|
+
const at = meta.generatedAt.getTime() / 1000;
|
|
653
|
+
const windows = loadWindows(db, at);
|
|
654
|
+
const gauge = windows.map(toGauge);
|
|
655
|
+
const series = windows.map(toSeries);
|
|
656
|
+
const interruptions = loadInterruptions(db, loaded?.observed);
|
|
657
|
+
// Lockouts carry their plan period only once a plan has been entered, as the split does.
|
|
658
|
+
const lockouts = loadLockouts(db, interruptions.byPlan.length > 0);
|
|
659
|
+
const afterReset = loadAfterReset(db);
|
|
660
|
+
const crossings = loadCrossings(db, loaded?.observed);
|
|
661
|
+
const models = loadModels(db);
|
|
662
|
+
const repos = loadRepos(db);
|
|
663
|
+
// Projections are read by their own function from their own view, never mixed into the rows above.
|
|
664
|
+
const burn = loadBurn(db, at);
|
|
665
|
+
const months = loadMonths(db);
|
|
666
|
+
const priceNotes = loadPriceNotes(db);
|
|
667
|
+
const referenced = [
|
|
668
|
+
...gauge.flatMap((g) => [...g.peakEvents, ...g.lastEvents, ...g.hitEvents]),
|
|
669
|
+
...series.flatMap((s) => [...s.points.map((p) => p.event), ...s.hits.map((h) => h.event)]),
|
|
670
|
+
...lockouts.flatMap((bar) => [
|
|
671
|
+
...bar.events,
|
|
672
|
+
...bar.nextRequestEvents,
|
|
673
|
+
...bar.autoContinuationEvents,
|
|
674
|
+
]),
|
|
675
|
+
// interruptions.events already holds every hit; the two sublists are listed anyway, so the
|
|
676
|
+
// table can't miss one should either list ever reach beyond it.
|
|
677
|
+
...interruptions.events,
|
|
678
|
+
...interruptions.unknownResetEvents,
|
|
679
|
+
...interruptions.hitsInNoColumnEvents,
|
|
680
|
+
...interruptions.byPlan.flatMap((p) => [...p.hitEvents, ...p.lockoutEvents]),
|
|
681
|
+
...afterReset.events,
|
|
682
|
+
...crossings.flatMap((c) => c.events),
|
|
683
|
+
...models.flatMap((bar) => bar.events),
|
|
684
|
+
...repos.flatMap((bar) => bar.events),
|
|
685
|
+
...burn.flatMap((window) => window.events),
|
|
686
|
+
...months.flatMap((bar) => [...bar.events, ...bar.unpricedEvents]),
|
|
687
|
+
...priceNotes.rateReadings.flatMap((reading) => reading.events),
|
|
688
|
+
...priceNotes.lowerBoundEvents,
|
|
689
|
+
];
|
|
690
|
+
const { files, events } = loadEvents(db, [...new Set(referenced)]);
|
|
691
|
+
return {
|
|
692
|
+
timeZone: meta.timeZone,
|
|
693
|
+
lastIngestAt: lastIngestAt(db),
|
|
694
|
+
generatedAtUtc: meta.generatedAt.toISOString(),
|
|
695
|
+
coverage: loadCoverage(db),
|
|
696
|
+
gauge,
|
|
697
|
+
series,
|
|
698
|
+
lockouts,
|
|
699
|
+
interruptions,
|
|
700
|
+
afterReset,
|
|
701
|
+
crossings,
|
|
702
|
+
models,
|
|
703
|
+
repos,
|
|
704
|
+
home: meta.home ?? "",
|
|
705
|
+
// The user's own links, as the text report lists them (D-092).
|
|
706
|
+
repositoryAliases: (loaded?.observed.repositoryAliases ?? listAliases(db)).map((a) => ({
|
|
707
|
+
from: a.from,
|
|
708
|
+
to: a.to,
|
|
709
|
+
})),
|
|
710
|
+
missingHookPath: meta.missingHookPath ?? null,
|
|
711
|
+
burn,
|
|
712
|
+
months,
|
|
713
|
+
priceNotes,
|
|
714
|
+
files,
|
|
715
|
+
events,
|
|
716
|
+
};
|
|
717
|
+
};
|
|
718
|
+
//# sourceMappingURL=html-data.js.map
|