@pyric/ui 0.1.0-alpha.11 → 0.1.0-alpha.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/package.json +5 -4
  2. package/src/agents/ContextWindowUsage.tsx +514 -0
  3. package/src/agents/EmptyState.tsx +27 -0
  4. package/src/agents/Fold.tsx +64 -0
  5. package/src/agents/Modal.tsx +65 -0
  6. package/src/agents/PulsingDot.tsx +28 -0
  7. package/src/agents/inbrowser-agent-usage.d.ts +141 -0
  8. package/src/agents/index.ts +28 -0
  9. package/src/auth/authApi.ts +72 -0
  10. package/src/auth/claims.ts +63 -0
  11. package/src/auth/components/AuthProviderToggles.tsx +147 -0
  12. package/src/auth/components/AuthSignInHelper.tsx +197 -0
  13. package/src/auth/components/AuthUserForm.tsx +328 -0
  14. package/src/auth/components/AuthUserList.tsx +219 -0
  15. package/src/auth/components/ClaimsField.tsx +50 -0
  16. package/src/auth/components/confirmActions.tsx +114 -0
  17. package/src/auth/controller.ts +173 -0
  18. package/src/auth/hooks/index.ts +36 -0
  19. package/src/auth/hooks/useAuthFlowHelper.ts +55 -0
  20. package/src/auth/hooks/useAuthProviderConfig.ts +139 -0
  21. package/src/auth/hooks/useAuthUserEditor.ts +76 -0
  22. package/src/auth/hooks/useAuthUsers.ts +154 -0
  23. package/src/auth/index.ts +42 -0
  24. package/src/auth/providers.ts +28 -0
  25. package/src/auth/reducers/userEditor.ts +186 -0
  26. package/src/events/components/ActivityActionItems.tsx +136 -0
  27. package/src/events/components/ActivityGrid.tsx +197 -0
  28. package/src/events/components/ActivityGridRow.tsx +65 -0
  29. package/src/events/components/ProposedChangeDiff.tsx +175 -0
  30. package/src/events/components/format.ts +21 -0
  31. package/src/events/components/index.ts +17 -0
  32. package/src/events/digest.ts +630 -0
  33. package/src/events/hooks/index.ts +9 -0
  34. package/src/events/hooks/useActivityDigest.ts +42 -0
  35. package/src/events/hooks/useActivityStream.ts +86 -0
  36. package/src/events/index.ts +39 -0
  37. package/src/events/types.ts +152 -0
  38. package/src/firestore/components/CollectionList.tsx +78 -0
  39. package/src/firestore/components/DeleteWithConfirm.tsx +88 -0
  40. package/src/firestore/components/DocumentEditor.tsx +350 -0
  41. package/src/firestore/components/DocumentList.tsx +217 -0
  42. package/src/firestore/components/DocumentPreview.tsx +265 -0
  43. package/src/firestore/components/FieldRenderer.tsx +25 -0
  44. package/src/firestore/components/QueryBuilder.tsx +181 -0
  45. package/src/firestore/components/ReferencePicker.tsx +212 -0
  46. package/src/firestore/components/TreeEntry.tsx +58 -0
  47. package/src/firestore/components/context.ts +25 -0
  48. package/src/firestore/fieldEditors/array.tsx +30 -0
  49. package/src/firestore/fieldEditors/boolean.tsx +39 -0
  50. package/src/firestore/fieldEditors/bytes.tsx +53 -0
  51. package/src/firestore/fieldEditors/geopoint.tsx +71 -0
  52. package/src/firestore/fieldEditors/map.tsx +33 -0
  53. package/src/firestore/fieldEditors/null.tsx +27 -0
  54. package/src/firestore/fieldEditors/number.tsx +43 -0
  55. package/src/firestore/fieldEditors/reference.tsx +87 -0
  56. package/src/firestore/fieldEditors/registry.ts +45 -0
  57. package/src/firestore/fieldEditors/string.tsx +33 -0
  58. package/src/firestore/fieldEditors/timestamp.tsx +75 -0
  59. package/src/firestore/fieldEditors/types.ts +68 -0
  60. package/src/firestore/fieldEditors/vector.tsx +142 -0
  61. package/src/firestore/firestoreApi.ts +86 -0
  62. package/src/firestore/hooks/coerceError.ts +34 -0
  63. package/src/firestore/hooks/index.ts +46 -0
  64. package/src/firestore/hooks/useCollectionList.ts +102 -0
  65. package/src/firestore/hooks/useDocumentEditor.ts +161 -0
  66. package/src/firestore/hooks/useDocumentList.ts +242 -0
  67. package/src/firestore/hooks/useDocumentSubcollections.ts +86 -0
  68. package/src/firestore/hooks/useFirestoreCollection.ts +51 -0
  69. package/src/firestore/hooks/useFirestoreDoc.ts +55 -0
  70. package/src/firestore/hooks/useQueryBuilder.ts +188 -0
  71. package/src/firestore/hooks/useRecursiveDelete.ts +77 -0
  72. package/src/firestore/hooks/useReferencePicker.ts +228 -0
  73. package/src/firestore/import/parseImport.ts +137 -0
  74. package/src/firestore/index.ts +87 -0
  75. package/src/firestore/reducers/defaults.ts +38 -0
  76. package/src/firestore/reducers/documentEditor.ts +239 -0
  77. package/src/firestore/reducers/tree.ts +140 -0
  78. package/src/firestore/reducers/types.ts +92 -0
  79. package/src/firestore/reducers/validation.ts +129 -0
  80. package/src/firestore/types.ts +231 -0
  81. package/src/firestore/validation/ids.ts +45 -0
  82. package/src/firestore/valueEquality.ts +43 -0
  83. package/src/index.ts +10 -0
  84. package/src/primitives/Badge.tsx +40 -0
  85. package/src/primitives/ConfirmDialog.tsx +137 -0
  86. package/src/primitives/CopyButton.tsx +62 -0
  87. package/src/primitives/JsonView.tsx +151 -0
  88. package/src/primitives/SegmentedControl.tsx +72 -0
  89. package/src/primitives/Toast.tsx +161 -0
  90. package/src/primitives/VirtualList.tsx +104 -0
  91. package/src/primitives/hooks/useContainerSize.ts +53 -0
  92. package/src/primitives/hooks/useUpdateHighlights.ts +109 -0
  93. package/src/primitives/index.ts +39 -0
  94. package/src/primitives/useConfirm.tsx +113 -0
  95. package/src/rtdb/components/RtdbPathBar.tsx +135 -0
  96. package/src/rtdb/components/RtdbTree.tsx +409 -0
  97. package/src/rtdb/editor.ts +79 -0
  98. package/src/rtdb/hooks/useRtdbTree.ts +137 -0
  99. package/src/rtdb/index.ts +66 -0
  100. package/src/rtdb/pathInput.ts +47 -0
  101. package/src/rtdb/reducers/tree.ts +191 -0
  102. package/src/rtdb/rtdbApi.ts +23 -0
  103. package/src/rtdb/values.ts +188 -0
  104. package/src/rules/components/DenialInspector.tsx +227 -0
  105. package/src/rules/components/format.ts +171 -0
  106. package/src/rules/components/index.ts +12 -0
  107. package/src/rules/components/scope.ts +75 -0
  108. package/src/rules/hooks/index.ts +5 -0
  109. package/src/rules/hooks/useDenialTrace.ts +100 -0
  110. package/src/rules/index.ts +24 -0
  111. package/src/rules/types.ts +91 -0
  112. package/src/storage/collisionRename.ts +114 -0
  113. package/src/storage/components/DeleteSelectionWithConfirm.tsx +193 -0
  114. package/src/storage/components/ObjectBrowser.tsx +219 -0
  115. package/src/storage/components/ObjectInspector.tsx +169 -0
  116. package/src/storage/components/PathBreadcrumb.tsx +84 -0
  117. package/src/storage/components/UploadDropzone.tsx +182 -0
  118. package/src/storage/folderPlaceholder.ts +40 -0
  119. package/src/storage/hooks/index.ts +59 -0
  120. package/src/storage/hooks/useMetadataEditor.ts +329 -0
  121. package/src/storage/hooks/useObjectUpload.ts +262 -0
  122. package/src/storage/hooks/usePathState.ts +94 -0
  123. package/src/storage/hooks/useStorageDelete.ts +195 -0
  124. package/src/storage/hooks/useStorageList.ts +261 -0
  125. package/src/storage/hooks/useStorageObject.ts +162 -0
  126. package/src/storage/hooks/useStorageRulesGate.ts +270 -0
  127. package/src/storage/hooks/useStorageSelection.ts +90 -0
  128. package/src/storage/index.ts +59 -0
  129. package/src/storage/pendingPrefixes.ts +125 -0
  130. package/src/storage/previews.tsx +120 -0
  131. package/src/storage/storageApi.ts +54 -0
  132. package/src/traffic/components/RuleHeatmap.tsx +97 -0
  133. package/src/traffic/components/TrafficDetail.tsx +160 -0
  134. package/src/traffic/components/TrafficGroupRow.tsx +91 -0
  135. package/src/traffic/components/TrafficLineChart.tsx +139 -0
  136. package/src/traffic/components/TrafficLog.tsx +175 -0
  137. package/src/traffic/components/TrafficMetricCards.tsx +77 -0
  138. package/src/traffic/components/TrafficRow.tsx +69 -0
  139. package/src/traffic/components/TrafficStats.tsx +73 -0
  140. package/src/traffic/components/TrafficTimeline.tsx +289 -0
  141. package/src/traffic/components/format.ts +22 -0
  142. package/src/traffic/components/index.ts +22 -0
  143. package/src/traffic/hooks/index.ts +60 -0
  144. package/src/traffic/hooks/useRuleHeatmap.ts +92 -0
  145. package/src/traffic/hooks/useTrafficBuckets.ts +146 -0
  146. package/src/traffic/hooks/useTrafficFilter.ts +74 -0
  147. package/src/traffic/hooks/useTrafficGroups.ts +126 -0
  148. package/src/traffic/hooks/useTrafficMetrics.ts +250 -0
  149. package/src/traffic/hooks/useTrafficMonitor.ts +111 -0
  150. package/src/traffic/hooks/useTrafficStats.ts +77 -0
  151. package/src/traffic/index.ts +13 -0
  152. package/src/traffic/types.ts +85 -0
@@ -0,0 +1,289 @@
1
+ import { useId, useState, type ReactNode } from 'react';
2
+ import { useTrafficBuckets } from '../hooks/useTrafficBuckets.js';
3
+ import type {
4
+ TimeWindow,
5
+ TrafficBucket,
6
+ UseTrafficBucketsResult,
7
+ } from '../hooks/useTrafficBuckets.js';
8
+ import type { TrafficEvent } from '../types.js';
9
+
10
+ export interface TrafficTimelineProps {
11
+ /**
12
+ * Raw traffic events to bucket. Ignored when `buckets` is supplied
13
+ * (the consumer pre-bucketed). Exactly one of `events` / `buckets`
14
+ * should drive the histogram.
15
+ */
16
+ events?: TrafficEvent[];
17
+ /**
18
+ * Pre-bucketed counts — the escape hatch when the caller already
19
+ * ran {@link useTrafficBuckets} (or `bucketTraffic`) upstream, e.g.
20
+ * to share one bucketing pass across the timeline + a stats header.
21
+ * Takes precedence over `events`.
22
+ */
23
+ buckets?: UseTrafficBucketsResult;
24
+ /** The time range the histogram spans. */
25
+ window: TimeWindow;
26
+ /** Number of bars. Default 30. Only used on the `events` path. */
27
+ bucketCount?: number;
28
+ /**
29
+ * A brushed sub-range (`[start, end)`) drawn as an overlay over the
30
+ * bars. Position is derived from where it falls inside `window`, so
31
+ * a partly-out-of-window brush clamps to the chart edges.
32
+ */
33
+ brush?: TimeWindow;
34
+ /**
35
+ * Fired when a bar inside the brush region is clicked-through — the
36
+ * component itself is presentation-agnostic about drag, so the
37
+ * primary brush gesture is owned by the consumer. As a built-in
38
+ * affordance, clicking a bucket calls this with a one-bucket-wide
39
+ * window so a bare consumer still gets a working selection.
40
+ */
41
+ onBrush?: (window: TimeWindow) => void;
42
+ /**
43
+ * Direct annotation shown while a bucket is hovered or keyboard-focused.
44
+ * The timeline owns preview state and positioning; the consumer owns the
45
+ * words and number formatting.
46
+ */
47
+ renderBucketSummary?: (bucket: TrafficBucket) => ReactNode;
48
+ /**
49
+ * Where the live edge marker sits, in epoch-ms. Defaults to
50
+ * `window.end` (the right edge = "now"). Omit / pass `null` to hide
51
+ * the marker entirely (e.g. a frozen, non-live window).
52
+ */
53
+ liveAt?: number | null;
54
+ /**
55
+ * Header slot — title, deny summary, live label. Rendered above the
56
+ * bars inside `[data-pyric-timeline-header]`. The mock puts
57
+ * "142 requests · 16 denied · live" here.
58
+ */
59
+ header?: ReactNode;
60
+ /**
61
+ * Axis slot — tick labels below the bars inside
62
+ * `[data-pyric-timeline-axis]`. The mock puts "14m ago · 7m · now"
63
+ * here. Receives the resolved window so labels can be derived.
64
+ */
65
+ axis?: ReactNode | ((window: TimeWindow) => ReactNode);
66
+ emptyState?: ReactNode;
67
+ className?: string;
68
+ }
69
+
70
+ /** Clamp a 0..1 fraction. */
71
+ function clamp01(n: number): number {
72
+ if (n < 0) return 0;
73
+ if (n > 1) return 1;
74
+ return n;
75
+ }
76
+
77
+ /** Fraction (0..1) of `t` across `[window.start, window.end)`. */
78
+ function fraction(t: number, window: TimeWindow): number {
79
+ const span = window.end - window.start;
80
+ if (span <= 0) return 0;
81
+ return clamp01((t - window.start) / span);
82
+ }
83
+
84
+ /**
85
+ * Headless volume-over-time histogram — the time axis for the traffic
86
+ * lens. Buckets events into N bars; each bar stacks denies (dark, at
87
+ * the base) under the allow remainder, matching the Studio mock.
88
+ *
89
+ * Per-bucket styling channels (on `[data-pyric-bucket]`):
90
+ * - `data-pyric-bucket-count` / `-denies` — raw integers.
91
+ * - `--pyric-bucket-h` — full bar height, 0..1 of the tallest bucket.
92
+ * - `--pyric-bucket-deny-h` — deny sub-stack height, same scale.
93
+ * The deny segment (`[data-pyric-bucket-deny]`) and allow segment
94
+ * (`[data-pyric-bucket-allow]`) are separate children so the consumer
95
+ * colors them independently.
96
+ *
97
+ * A `[data-pyric-brush]` overlay marks a sub-range via
98
+ * `--pyric-brush-left` / `--pyric-brush-right` (0..1 fractions). The
99
+ * `[data-pyric-live]` edge marker sits at `--pyric-live-x`.
100
+ *
101
+ * Styling hooks: `[data-pyric-ui="traffic-timeline"]`,
102
+ * `[data-pyric-timeline-header]`, `[data-pyric-timeline-bars]`,
103
+ * `[data-pyric-bucket]` (with `data-pyric-bucket-index`,
104
+ * `data-pyric-has-denies`, `data-pyric-bucket-selected`),
105
+ * `[data-pyric-bucket-summary]`, `[data-pyric-brush]`, `[data-pyric-live]`,
106
+ * `[data-pyric-timeline-axis]`.
107
+ */
108
+ export function TrafficTimeline({
109
+ events,
110
+ buckets: prebucketed,
111
+ window,
112
+ bucketCount = 30,
113
+ brush,
114
+ onBrush,
115
+ renderBucketSummary,
116
+ liveAt,
117
+ header,
118
+ axis,
119
+ emptyState,
120
+ className,
121
+ }: TrafficTimelineProps) {
122
+ const summaryId = useId();
123
+ const [hoveredBucket, setHoveredBucket] = useState<number | null>(null);
124
+ const [focusedBucket, setFocusedBucket] = useState<number | null>(null);
125
+ // Hook is always called (rules of hooks); its result is discarded
126
+ // when the caller pre-bucketed.
127
+ const computed = useTrafficBuckets({
128
+ events: events ?? [],
129
+ window,
130
+ bucketCount,
131
+ });
132
+ const result = prebucketed ?? computed;
133
+ const { buckets, total } = result;
134
+ const activeBucketIndex = hoveredBucket ?? focusedBucket;
135
+ const activeBucket =
136
+ activeBucketIndex === null ? null : (buckets.find((bucket) => bucket.index === activeBucketIndex) ?? null);
137
+ const activeBucketSelected =
138
+ activeBucket !== null && brush?.start === activeBucket.start && brush.end === activeBucket.end;
139
+
140
+ if (buckets.length === 0) {
141
+ return (
142
+ <div
143
+ className={className}
144
+ data-pyric-ui="traffic-timeline"
145
+ data-pyric-empty=""
146
+ >
147
+ {emptyState}
148
+ </div>
149
+ );
150
+ }
151
+
152
+ const isEmpty = total === 0;
153
+ const livePoint = liveAt === undefined ? window.end : liveAt;
154
+ const liveX = livePoint == null ? null : fraction(livePoint, window);
155
+
156
+ const resolvedAxis = typeof axis === 'function' ? axis(window) : axis;
157
+
158
+ return (
159
+ <div
160
+ className={className}
161
+ data-pyric-ui="traffic-timeline"
162
+ data-pyric-empty={isEmpty ? '' : undefined}
163
+ >
164
+ {header !== undefined && (
165
+ <div data-pyric-timeline-header="">{header}</div>
166
+ )}
167
+
168
+ <div data-pyric-timeline-chart="">
169
+ <div
170
+ data-pyric-timeline-bars=""
171
+ role={onBrush ? 'group' : 'img'}
172
+ aria-label="request volume over time"
173
+ >
174
+ {buckets.map((bucket) => (
175
+ <Bar
176
+ key={bucket.index}
177
+ bucket={bucket}
178
+ window={window}
179
+ onBrush={onBrush}
180
+ onHover={renderBucketSummary ? setHoveredBucket : undefined}
181
+ onFocus={renderBucketSummary ? setFocusedBucket : undefined}
182
+ summaryId={
183
+ renderBucketSummary && !activeBucketSelected && activeBucket?.index === bucket.index
184
+ ? summaryId
185
+ : undefined
186
+ }
187
+ selected={brush?.start === bucket.start && brush.end === bucket.end}
188
+ />
189
+ ))}
190
+ </div>
191
+
192
+ {activeBucket && renderBucketSummary && !activeBucketSelected ? (
193
+ <div
194
+ id={summaryId}
195
+ role="tooltip"
196
+ data-pyric-bucket-summary=""
197
+ style={
198
+ {
199
+ '--pyric-bucket-summary-x': (activeBucket.index + 0.5) / Math.max(buckets.length, 1),
200
+ } as React.CSSProperties
201
+ }
202
+ >
203
+ {renderBucketSummary(activeBucket)}
204
+ </div>
205
+ ) : null}
206
+
207
+ {brush && (
208
+ <div
209
+ data-pyric-brush=""
210
+ aria-hidden="true"
211
+ style={
212
+ {
213
+ '--pyric-brush-left': fraction(brush.start, window),
214
+ '--pyric-brush-right': 1 - fraction(brush.end, window),
215
+ } as React.CSSProperties
216
+ }
217
+ />
218
+ )}
219
+
220
+ {liveX != null && (
221
+ <div
222
+ data-pyric-live=""
223
+ aria-hidden="true"
224
+ style={{ '--pyric-live-x': liveX } as React.CSSProperties}
225
+ />
226
+ )}
227
+ </div>
228
+
229
+ {resolvedAxis !== undefined && resolvedAxis !== null && (
230
+ <div data-pyric-timeline-axis="">{resolvedAxis}</div>
231
+ )}
232
+ </div>
233
+ );
234
+ }
235
+
236
+ function Bar({
237
+ bucket,
238
+ window,
239
+ onBrush,
240
+ onHover,
241
+ onFocus,
242
+ summaryId,
243
+ selected,
244
+ }: {
245
+ bucket: TrafficBucket;
246
+ window: TimeWindow;
247
+ onBrush?: (window: TimeWindow) => void;
248
+ onHover?: (index: number | null) => void;
249
+ onFocus?: (index: number | null) => void;
250
+ summaryId?: string;
251
+ selected: boolean;
252
+ }) {
253
+ const interactive = onBrush !== undefined;
254
+ const requestLabel = `${bucket.count} ${bucket.count === 1 ? 'request' : 'requests'}`;
255
+ const denyLabel = `${bucket.denies} denied`;
256
+ return (
257
+ <button
258
+ type="button"
259
+ data-pyric-bucket=""
260
+ data-pyric-bucket-index={bucket.index}
261
+ data-pyric-bucket-count={bucket.count}
262
+ data-pyric-bucket-denies={bucket.denies}
263
+ data-pyric-has-denies={bucket.denies > 0 ? '' : undefined}
264
+ data-pyric-bucket-selected={selected ? '' : undefined}
265
+ disabled={!interactive}
266
+ aria-label={`${requestLabel}, ${denyLabel}`}
267
+ aria-describedby={summaryId}
268
+ aria-pressed={interactive ? selected : undefined}
269
+ onPointerEnter={onHover ? () => onHover(bucket.index) : undefined}
270
+ onPointerLeave={onHover ? () => onHover(null) : undefined}
271
+ onFocus={onFocus ? () => onFocus(bucket.index) : undefined}
272
+ onBlur={onFocus ? () => onFocus(null) : undefined}
273
+ onClick={
274
+ interactive
275
+ ? () => onBrush?.({ start: bucket.start, end: bucket.end })
276
+ : undefined
277
+ }
278
+ style={
279
+ {
280
+ '--pyric-bucket-h': bucket.heightRatio,
281
+ '--pyric-bucket-deny-h': bucket.denyHeightRatio,
282
+ } as React.CSSProperties
283
+ }
284
+ >
285
+ <span data-pyric-bucket-allow="" aria-hidden="true" />
286
+ <span data-pyric-bucket-deny="" aria-hidden="true" />
287
+ </button>
288
+ );
289
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Default `HH:MM:SS` timestamp formatter. Components accept a
3
+ * `formatTime` prop to override — this is just the fallback so the
4
+ * library has no hard locale dependency.
5
+ */
6
+ export function defaultFormatTime(at: number): string {
7
+ const d = new Date(at);
8
+ const pad = (n: number) => String(n).padStart(2, '0');
9
+ return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
10
+ }
11
+
12
+ /**
13
+ * Classifies a simulator debug line as a deny / allow / neutral
14
+ * verdict so consumers can tint reason rows via
15
+ * `[data-pyric-reason-verdict="…"]`.
16
+ */
17
+ export function reasonVerdict(reason: string): 'deny' | 'allow' | 'neutral' {
18
+ const lower = reason.toLowerCase();
19
+ if (lower.includes('deny')) return 'deny';
20
+ if (lower.includes('allow')) return 'allow';
21
+ return 'neutral';
22
+ }
@@ -0,0 +1,22 @@
1
+ export { TrafficRow, type TrafficRowProps } from './TrafficRow.js';
2
+ export { TrafficLog, type TrafficLogProps } from './TrafficLog.js';
3
+ export {
4
+ TrafficGroupRow,
5
+ type TrafficGroupRowProps,
6
+ } from './TrafficGroupRow.js';
7
+ export { TrafficDetail, type TrafficDetailProps } from './TrafficDetail.js';
8
+ export { RuleHeatmap, type RuleHeatmapProps } from './RuleHeatmap.js';
9
+ export { TrafficStats, type TrafficStatsProps } from './TrafficStats.js';
10
+ export {
11
+ TrafficTimeline,
12
+ type TrafficTimelineProps,
13
+ } from './TrafficTimeline.js';
14
+ export { defaultFormatTime, reasonVerdict } from './format.js';
15
+ export {
16
+ TrafficMetricCards,
17
+ type TrafficMetricCardsProps,
18
+ } from './TrafficMetricCards.js';
19
+ export {
20
+ TrafficLineChart,
21
+ type TrafficLineChartProps,
22
+ } from './TrafficLineChart.js';
@@ -0,0 +1,60 @@
1
+ export {
2
+ useTrafficMonitor,
3
+ type UseTrafficMonitorOptions,
4
+ type UseTrafficMonitorResult,
5
+ type TrafficCounts,
6
+ } from './useTrafficMonitor.js';
7
+ export {
8
+ useTrafficFilter,
9
+ type UseTrafficFilterOptions,
10
+ type UseTrafficFilterResult,
11
+ type TrafficFilterState,
12
+ type TrafficOriginFilter,
13
+ type TrafficResultFilter,
14
+ } from './useTrafficFilter.js';
15
+ export {
16
+ useRuleHeatmap,
17
+ type UseRuleHeatmapOptions,
18
+ type UseRuleHeatmapResult,
19
+ type RuleHeatmapEntry,
20
+ } from './useRuleHeatmap.js';
21
+ export {
22
+ useTrafficStats,
23
+ type UseTrafficStatsOptions,
24
+ type TrafficStatsSummary,
25
+ type TrafficStatBucket,
26
+ } from './useTrafficStats.js';
27
+ export {
28
+ useTrafficGroups,
29
+ type UseTrafficGroupsOptions,
30
+ type UseTrafficGroupsResult,
31
+ type TrafficLogItem,
32
+ type TrafficGroup,
33
+ type TrafficSingle,
34
+ type TrafficGroupKind,
35
+ } from './useTrafficGroups.js';
36
+ export {
37
+ useTrafficBuckets,
38
+ bucketTraffic,
39
+ type UseTrafficBucketsOptions,
40
+ type UseTrafficBucketsResult,
41
+ type TrafficBucket,
42
+ type TimeWindow,
43
+ } from './useTrafficBuckets.js';
44
+ export {
45
+ useBillableMetrics,
46
+ useRulesMetrics,
47
+ bucketBillableMetrics,
48
+ bucketRulesMetrics,
49
+ classifyBillable,
50
+ classifyRules,
51
+ isAdminEvent,
52
+ BILLABLE_SERIES_DEFS,
53
+ RULES_SERIES_DEFS,
54
+ type UseTrafficMetricsOptions,
55
+ type TrafficMetricsResult,
56
+ type MetricPoint,
57
+ type MetricSeries,
58
+ type BillableSeriesKey,
59
+ type RulesSeriesKey,
60
+ } from './useTrafficMetrics.js';
@@ -0,0 +1,92 @@
1
+ import { useMemo } from 'react';
2
+ import type { TrafficEvent } from '../types.js';
3
+
4
+ export interface RuleHeatmapEntry {
5
+ /** The rule's index in the rules file. */
6
+ ruleIndex: number;
7
+ /** Union of all operations seen matching this rule. */
8
+ operations: string[];
9
+ /** Total events that matched this rule. */
10
+ total: number;
11
+ allows: number;
12
+ denies: number;
13
+ unsupported: number;
14
+ /** `denies / total` — 0 when the rule never denied. */
15
+ denyRatio: number;
16
+ }
17
+
18
+ export interface UseRuleHeatmapOptions {
19
+ events: TrafficEvent[];
20
+ }
21
+
22
+ export interface UseRuleHeatmapResult {
23
+ /** Per-rule rollup, sorted by `total` descending (busiest first),
24
+ * ties broken by `ruleIndex` ascending. */
25
+ entries: RuleHeatmapEntry[];
26
+ /** Events that matched no rule — counted here, not attributed to
27
+ * any entry. */
28
+ unmatchedCount: number;
29
+ }
30
+
31
+ interface Accumulator {
32
+ ruleIndex: number;
33
+ operations: Set<string>;
34
+ total: number;
35
+ allows: number;
36
+ denies: number;
37
+ unsupported: number;
38
+ }
39
+
40
+ /**
41
+ * Rolls a traffic buffer up by `matchedRule.ruleIndex`: how often
42
+ * each rule fired, and how that split across allow / deny /
43
+ * unsupported. Pure derivation — feed it the filtered or full
44
+ * event list depending on what the heatmap should reflect.
45
+ */
46
+ export function useRuleHeatmap({
47
+ events,
48
+ }: UseRuleHeatmapOptions): UseRuleHeatmapResult {
49
+ return useMemo(() => {
50
+ const byRule = new Map<number, Accumulator>();
51
+ let unmatchedCount = 0;
52
+
53
+ for (const event of events) {
54
+ if (!event.matchedRule) {
55
+ unmatchedCount++;
56
+ continue;
57
+ }
58
+ const { ruleIndex, operations } = event.matchedRule;
59
+ let acc = byRule.get(ruleIndex);
60
+ if (!acc) {
61
+ acc = {
62
+ ruleIndex,
63
+ operations: new Set(),
64
+ total: 0,
65
+ allows: 0,
66
+ denies: 0,
67
+ unsupported: 0,
68
+ };
69
+ byRule.set(ruleIndex, acc);
70
+ }
71
+ for (const op of operations) acc.operations.add(op);
72
+ acc.total++;
73
+ if (event.result === 'allow') acc.allows++;
74
+ else if (event.result === 'deny') acc.denies++;
75
+ else acc.unsupported++;
76
+ }
77
+
78
+ const entries: RuleHeatmapEntry[] = [...byRule.values()]
79
+ .map((acc) => ({
80
+ ruleIndex: acc.ruleIndex,
81
+ operations: [...acc.operations],
82
+ total: acc.total,
83
+ allows: acc.allows,
84
+ denies: acc.denies,
85
+ unsupported: acc.unsupported,
86
+ denyRatio: acc.total === 0 ? 0 : acc.denies / acc.total,
87
+ }))
88
+ .sort((a, b) => b.total - a.total || a.ruleIndex - b.ruleIndex);
89
+
90
+ return { entries, unmatchedCount };
91
+ }, [events]);
92
+ }
@@ -0,0 +1,146 @@
1
+ import { useMemo } from 'react';
2
+ import type { TrafficEvent } from '../types.js';
3
+
4
+ /**
5
+ * A half-open time window `[start, end)` in epoch-ms. The timeline
6
+ * buckets events whose `at` falls inside it; events outside are
7
+ * dropped from the histogram (but still counted in `outOfWindow`).
8
+ */
9
+ export interface TimeWindow {
10
+ start: number;
11
+ end: number;
12
+ }
13
+
14
+ export interface TrafficBucket {
15
+ /** 0-based bucket index, left (oldest) to right (newest). */
16
+ index: number;
17
+ /** Half-open bounds of this bucket `[start, end)` in epoch-ms. */
18
+ start: number;
19
+ end: number;
20
+ /** Total events that fell in this bucket. */
21
+ count: number;
22
+ /** How many of `count` were denied. */
23
+ denies: number;
24
+ /** `count - denies` — allowed + unsupported. The "non-deny" stack. */
25
+ allows: number;
26
+ /**
27
+ * `count / maxCount` across all buckets — 0..1. The full bar
28
+ * height as a fraction of the tallest bucket. Drives
29
+ * `--pyric-bucket-h`.
30
+ */
31
+ heightRatio: number;
32
+ /**
33
+ * `denies / maxCount` — 0..1. The deny sub-stack height as a
34
+ * fraction of the tallest bucket, so the deny segment is drawn to
35
+ * the same scale as the full bar. Drives `--pyric-bucket-deny-h`.
36
+ */
37
+ denyHeightRatio: number;
38
+ }
39
+
40
+ export interface UseTrafficBucketsOptions {
41
+ events: TrafficEvent[];
42
+ /** The time range to bucket over. */
43
+ window: TimeWindow;
44
+ /** Number of buckets to divide the window into. Default 30. */
45
+ bucketCount?: number;
46
+ }
47
+
48
+ export interface UseTrafficBucketsResult {
49
+ buckets: TrafficBucket[];
50
+ /** Sum of `count` across buckets (events inside the window). */
51
+ total: number;
52
+ /** Sum of `denies` across buckets. */
53
+ denies: number;
54
+ /** The largest single-bucket `count` — the height-ratio divisor. */
55
+ maxCount: number;
56
+ /** Events whose `at` fell outside `[window.start, window.end)`. */
57
+ outOfWindow: number;
58
+ }
59
+
60
+ const EMPTY_RESULT: UseTrafficBucketsResult = {
61
+ buckets: [],
62
+ total: 0,
63
+ denies: 0,
64
+ maxCount: 0,
65
+ outOfWindow: 0,
66
+ };
67
+
68
+ /**
69
+ * Buckets a traffic buffer into `bucketCount` equal time slices over
70
+ * `window`, counting total + denied events per slice. Pure
71
+ * derivation — the histogram component renders the result, this hook
72
+ * (and `bucketTraffic` under it) owns the math.
73
+ *
74
+ * Each bucket carries a `heightRatio` and `denyHeightRatio`
75
+ * (0..1, scaled to the tallest bucket) so the consumer can map them
76
+ * straight onto a bar height without re-finding the max.
77
+ */
78
+ export function useTrafficBuckets({
79
+ events,
80
+ window,
81
+ bucketCount = 30,
82
+ }: UseTrafficBucketsOptions): UseTrafficBucketsResult {
83
+ return useMemo(
84
+ () => bucketTraffic(events, window, bucketCount),
85
+ [events, window.start, window.end, bucketCount],
86
+ );
87
+ }
88
+
89
+ /**
90
+ * The pure bucketing kernel behind {@link useTrafficBuckets} — usable
91
+ * outside React. Returns an empty result for a non-positive
92
+ * `bucketCount` or a zero/negative-width window.
93
+ */
94
+ export function bucketTraffic(
95
+ events: TrafficEvent[],
96
+ window: TimeWindow,
97
+ bucketCount = 30,
98
+ ): UseTrafficBucketsResult {
99
+ const span = window.end - window.start;
100
+ if (bucketCount <= 0 || span <= 0) return EMPTY_RESULT;
101
+
102
+ const width = span / bucketCount;
103
+ const counts = new Array<number>(bucketCount).fill(0);
104
+ const denyCounts = new Array<number>(bucketCount).fill(0);
105
+
106
+ let total = 0;
107
+ let denies = 0;
108
+ let outOfWindow = 0;
109
+
110
+ for (const event of events) {
111
+ const at = event.at;
112
+ if (at < window.start || at >= window.end) {
113
+ outOfWindow++;
114
+ continue;
115
+ }
116
+ // Floor into a bucket; clamp the right edge so an `at` exactly at
117
+ // `window.end - epsilon` never spills past the last bucket.
118
+ let i = Math.floor((at - window.start) / width);
119
+ if (i >= bucketCount) i = bucketCount - 1;
120
+ counts[i]++;
121
+ total++;
122
+ if (event.result === 'deny') {
123
+ denyCounts[i]++;
124
+ denies++;
125
+ }
126
+ }
127
+
128
+ let maxCount = 0;
129
+ for (const c of counts) if (c > maxCount) maxCount = c;
130
+
131
+ const buckets: TrafficBucket[] = counts.map((count, index) => {
132
+ const denyCount = denyCounts[index];
133
+ return {
134
+ index,
135
+ start: window.start + index * width,
136
+ end: window.start + (index + 1) * width,
137
+ count,
138
+ denies: denyCount,
139
+ allows: count - denyCount,
140
+ heightRatio: maxCount === 0 ? 0 : count / maxCount,
141
+ denyHeightRatio: maxCount === 0 ? 0 : denyCount / maxCount,
142
+ };
143
+ });
144
+
145
+ return { buckets, total, denies, maxCount, outOfWindow };
146
+ }