@juice-js/observability 0.0.0-stage → 1.0.0-alpha.1

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 (34) hide show
  1. package/README.md +31 -2
  2. package/fesm2022/juice-js-observability-alert.page-D2OL0zTT.mjs +405 -0
  3. package/fesm2022/juice-js-observability-alert.page-D2OL0zTT.mjs.map +1 -0
  4. package/fesm2022/juice-js-observability-alerts.page-BLJvD_op.mjs +208 -0
  5. package/fesm2022/juice-js-observability-alerts.page-BLJvD_op.mjs.map +1 -0
  6. package/fesm2022/juice-js-observability-logs.page-BWFLleyX.mjs +414 -0
  7. package/fesm2022/juice-js-observability-logs.page-BWFLleyX.mjs.map +1 -0
  8. package/fesm2022/juice-js-observability-metrics.page-D1caPbQq.mjs +268 -0
  9. package/fesm2022/juice-js-observability-metrics.page-D1caPbQq.mjs.map +1 -0
  10. package/fesm2022/juice-js-observability-overview.page-UBwHjlyq.mjs +213 -0
  11. package/fesm2022/juice-js-observability-overview.page-UBwHjlyq.mjs.map +1 -0
  12. package/fesm2022/juice-js-observability-resource.page-BZwLK_vU.mjs +275 -0
  13. package/fesm2022/juice-js-observability-resource.page-BZwLK_vU.mjs.map +1 -0
  14. package/fesm2022/juice-js-observability-resources.page-C0xWOb5I.mjs +210 -0
  15. package/fesm2022/juice-js-observability-resources.page-C0xWOb5I.mjs.map +1 -0
  16. package/fesm2022/juice-js-observability-rules.page-Db_U5rRu.mjs +176 -0
  17. package/fesm2022/juice-js-observability-rules.page-Db_U5rRu.mjs.map +1 -0
  18. package/fesm2022/juice-js-observability-service.page-B-SWJ9I5.mjs +727 -0
  19. package/fesm2022/juice-js-observability-service.page-B-SWJ9I5.mjs.map +1 -0
  20. package/fesm2022/juice-js-observability-services.page-BmWgFnC8.mjs +252 -0
  21. package/fesm2022/juice-js-observability-services.page-BmWgFnC8.mjs.map +1 -0
  22. package/fesm2022/juice-js-observability-telemetry-VA4_3nzr.mjs +1748 -0
  23. package/fesm2022/juice-js-observability-telemetry-VA4_3nzr.mjs.map +1 -0
  24. package/fesm2022/juice-js-observability-topology.page-DEUNp10U.mjs +458 -0
  25. package/fesm2022/juice-js-observability-topology.page-DEUNp10U.mjs.map +1 -0
  26. package/fesm2022/juice-js-observability-trace.page-gBhMG2eQ.mjs +530 -0
  27. package/fesm2022/juice-js-observability-trace.page-gBhMG2eQ.mjs.map +1 -0
  28. package/fesm2022/juice-js-observability-traces.page-BCQg4V4o.mjs +521 -0
  29. package/fesm2022/juice-js-observability-traces.page-BCQg4V4o.mjs.map +1 -0
  30. package/fesm2022/juice-js-observability.mjs +396 -0
  31. package/fesm2022/juice-js-observability.mjs.map +1 -0
  32. package/package.json +48 -4
  33. package/styles/_observability.scss +133 -0
  34. package/types/juice-js-observability.d.ts +387 -0
@@ -0,0 +1,396 @@
1
+ import * as i0 from "@angular/core";
2
+ import { Injectable, InjectionToken, Pipe, computed, inject, makeEnvironmentProviders } from "@angular/core";
3
+ import { HttpClient, HttpParams } from "@angular/common/http";
4
+ import { EMPTY, expand, filter, map, reduce, shareReplay, startWith, switchMap } from "rxjs";
5
+ import { toSignal } from "@angular/core/rxjs-interop";
6
+ import { NavigationEnd, Router } from "@angular/router";
7
+ const OBSERVABILITY_OPTIONS = new InjectionToken("OBSERVABILITY_OPTIONS", {
8
+ providedIn: "root",
9
+ factory: () => ({
10
+ apiBase: "",
11
+ basePath: "/"
12
+ })
13
+ });
14
+ function provideJuiceObservability(options = {}) {
15
+ return makeEnvironmentProviders([{
16
+ provide: OBSERVABILITY_OPTIONS,
17
+ useValue: {
18
+ apiBase: options.apiBase ?? "",
19
+ basePath: options.basePath ?? "/"
20
+ }
21
+ }]);
22
+ }
23
+ function normalizeBasePath(basePath) {
24
+ const trimmed = basePath.trim().replace(/^\/+|\/+$/g, "");
25
+ return trimmed ? `/${trimmed}` : "/";
26
+ }
27
+ var ObservabilityLinks = class ObservabilityLinks {
28
+ base = normalizeBasePath(inject(OBSERVABILITY_OPTIONS).basePath);
29
+ to(...segments) {
30
+ return [this.base, ...segments];
31
+ }
32
+ static ɵfac = i0.ɵɵngDeclareFactory({
33
+ minVersion: "12.0.0",
34
+ version: "22.2.4",
35
+ ngImport: i0,
36
+ type: ObservabilityLinks,
37
+ deps: [],
38
+ target: i0.ɵɵFactoryTarget.Injectable
39
+ });
40
+ static ɵprov = i0.ɵɵngDeclareInjectable({
41
+ minVersion: "12.0.0",
42
+ version: "22.2.4",
43
+ ngImport: i0,
44
+ type: ObservabilityLinks,
45
+ providedIn: "root"
46
+ });
47
+ };
48
+ i0.ɵɵngDeclareClassMetadata({
49
+ minVersion: "12.0.0",
50
+ version: "22.2.4",
51
+ ngImport: i0,
52
+ type: ObservabilityLinks,
53
+ decorators: [{
54
+ type: Injectable,
55
+ args: [{ providedIn: "root" }]
56
+ }]
57
+ });
58
+ var LinkPipe = class LinkPipe {
59
+ links = inject(ObservabilityLinks);
60
+ transform(segments) {
61
+ return this.links.to(...segments);
62
+ }
63
+ static ɵfac = i0.ɵɵngDeclareFactory({
64
+ minVersion: "12.0.0",
65
+ version: "22.2.4",
66
+ ngImport: i0,
67
+ type: LinkPipe,
68
+ deps: [],
69
+ target: i0.ɵɵFactoryTarget.Pipe
70
+ });
71
+ static ɵpipe = i0.ɵɵngDeclarePipe({
72
+ minVersion: "14.0.0",
73
+ version: "22.2.4",
74
+ ngImport: i0,
75
+ type: LinkPipe,
76
+ isStandalone: true,
77
+ name: "jcLink"
78
+ });
79
+ };
80
+ i0.ɵɵngDeclareClassMetadata({
81
+ minVersion: "12.0.0",
82
+ version: "22.2.4",
83
+ ngImport: i0,
84
+ type: LinkPipe,
85
+ decorators: [{
86
+ type: Pipe,
87
+ args: [{ name: "jcLink" }]
88
+ }]
89
+ });
90
+ var ApiService = class ApiService {
91
+ http = inject(HttpClient);
92
+ base = `${inject(OBSERVABILITY_OPTIONS).apiBase}/api/v1`;
93
+ tenant$ = this.http.get(`${this.base}/tenants`).pipe(map((page) => {
94
+ if (page.items.length === 0) throw new Error("No tenant is authorized for this user.");
95
+ return page.items[0];
96
+ }), shareReplay(1));
97
+ get(path, query = {}) {
98
+ let params = new HttpParams();
99
+ for (const [key, value] of Object.entries(query)) {
100
+ if (value === null || value === void 0 || value === "") continue;
101
+ for (const item of Array.isArray(value) ? value : [value]) params = params.append(key, String(item));
102
+ }
103
+ return this.tenant$.pipe(switchMap((t) => this.http.get(`${this.base}/tenants/${t.id}${path}`, { params })));
104
+ }
105
+ all(path, query = {}, maxPages = 25) {
106
+ let pages = 0;
107
+ return this.get(path, {
108
+ ...query,
109
+ limit: 200
110
+ }).pipe(expand((page) => page.nextCursor && ++pages < maxPages ? this.get(path, {
111
+ ...query,
112
+ limit: 200,
113
+ cursor: page.nextCursor
114
+ }) : EMPTY), reduce((all, page) => ({
115
+ items: [...all.items, ...page.items],
116
+ nextCursor: page.nextCursor
117
+ }), {
118
+ items: [],
119
+ nextCursor: null
120
+ }));
121
+ }
122
+ environments() {
123
+ return this.get("/environments");
124
+ }
125
+ applications() {
126
+ return this.get("/applications", { limit: 200 });
127
+ }
128
+ resources(type) {
129
+ return this.all("/resources", { type });
130
+ }
131
+ resource(id) {
132
+ return this.get(`/resources/${id}`);
133
+ }
134
+ health(query) {
135
+ return this.all("/health", query);
136
+ }
137
+ topology(environment) {
138
+ return this.get("/topology", { environment });
139
+ }
140
+ rules() {
141
+ return this.get("/rules", { limit: 200 });
142
+ }
143
+ alerts(query) {
144
+ return this.get("/alerts", {
145
+ limit: 100,
146
+ ...query
147
+ });
148
+ }
149
+ allAlerts(query) {
150
+ return this.all("/alerts", query);
151
+ }
152
+ alert(id) {
153
+ return this.get(`/alerts/${id}`);
154
+ }
155
+ acknowledge(id, comment) {
156
+ return this.tenant$.pipe(switchMap((t) => this.http.post(`${this.base}/tenants/${t.id}/alerts/${id}/acknowledgement`, { comment }, { headers: { "Idempotency-Key": crypto.randomUUID() } })));
157
+ }
158
+ traces(query) {
159
+ return this.get("/telemetry/traces", query);
160
+ }
161
+ trace(traceId) {
162
+ return this.get(`/telemetry/traces/${traceId}`);
163
+ }
164
+ logs(query) {
165
+ return this.get("/telemetry/logs", query);
166
+ }
167
+ operations(query) {
168
+ return this.get("/telemetry/operations", query);
169
+ }
170
+ metrics(query) {
171
+ return this.get("/telemetry/metrics", query);
172
+ }
173
+ series(query) {
174
+ return this.get("/telemetry/metrics/series", query);
175
+ }
176
+ serviceStats(query) {
177
+ return this.get("/telemetry/service-stats", query);
178
+ }
179
+ dependency(id, query) {
180
+ return this.get(`/telemetry/dependencies/${id}`, query);
181
+ }
182
+ search(q) {
183
+ return this.get("/search", { q });
184
+ }
185
+ static ɵfac = i0.ɵɵngDeclareFactory({
186
+ minVersion: "12.0.0",
187
+ version: "22.2.4",
188
+ ngImport: i0,
189
+ type: ApiService,
190
+ deps: [],
191
+ target: i0.ɵɵFactoryTarget.Injectable
192
+ });
193
+ static ɵprov = i0.ɵɵngDeclareInjectable({
194
+ minVersion: "12.0.0",
195
+ version: "22.2.4",
196
+ ngImport: i0,
197
+ type: ApiService,
198
+ providedIn: "root"
199
+ });
200
+ };
201
+ i0.ɵɵngDeclareClassMetadata({
202
+ minVersion: "12.0.0",
203
+ version: "22.2.4",
204
+ ngImport: i0,
205
+ type: ApiService,
206
+ decorators: [{
207
+ type: Injectable,
208
+ args: [{ providedIn: "root" }]
209
+ }]
210
+ });
211
+ const RANGES = {
212
+ "15m": 9e5,
213
+ "1h": 36e5,
214
+ "6h": 216e5,
215
+ "24h": 864e5,
216
+ "7d": 6048e5
217
+ };
218
+ const MAX_SEARCH_MS = RANGES["24h"];
219
+ var ContextService = class ContextService {
220
+ router = inject(Router);
221
+ params = toSignal(this.router.events.pipe(filter((e) => e instanceof NavigationEnd), startWith(null), map(() => this.leaf().snapshot.queryParams)), { initialValue: {} });
222
+ environment = computed(() => this.params()["env"] ?? "", ...ngDevMode ? [{ debugName: "environment" }] : /* istanbul ignore next */ []);
223
+ range = computed(() => {
224
+ const p = this.params();
225
+ return p["from"] && p["to"] ? "custom" : p["range"] ?? "1h";
226
+ }, ...ngDevMode ? [{ debugName: "range" }] : /* istanbul ignore next */ []);
227
+ absolute = computed(() => {
228
+ const p = this.params();
229
+ return p["from"] && p["to"] ? {
230
+ from: p["from"],
231
+ to: p["to"]
232
+ } : null;
233
+ }, ...ngDevMode ? [{ debugName: "absolute" }] : /* istanbul ignore next */ []);
234
+ window(maxMs) {
235
+ const absolute = this.absolute();
236
+ if (absolute) return absolute;
237
+ const to = Date.now();
238
+ const span = Math.min(RANGES[this.range()] ?? RANGES["1h"], maxMs ?? Number.MAX_SAFE_INTEGER);
239
+ return {
240
+ from: new Date(to - span).toISOString(),
241
+ to: new Date(to).toISOString()
242
+ };
243
+ }
244
+ keep(extra = {}) {
245
+ const p = this.params();
246
+ const context = {};
247
+ for (const key of [
248
+ "env",
249
+ "range",
250
+ "from",
251
+ "to"
252
+ ]) if (p[key]) context[key] = p[key];
253
+ return {
254
+ ...context,
255
+ ...extra
256
+ };
257
+ }
258
+ set(change) {
259
+ const queryParams = { ...change };
260
+ if (change.range) Object.assign(queryParams, {
261
+ from: null,
262
+ to: null
263
+ });
264
+ this.router.navigate([], {
265
+ relativeTo: this.leaf(),
266
+ queryParams,
267
+ queryParamsHandling: "merge"
268
+ });
269
+ }
270
+ around(at, durationMs, paddingMs = 3e5) {
271
+ const start = new Date(at).getTime();
272
+ return this.keep({
273
+ from: new Date(start - paddingMs).toISOString(),
274
+ to: new Date(start + durationMs + paddingMs).toISOString(),
275
+ range: null
276
+ });
277
+ }
278
+ leaf() {
279
+ let route = this.router.routerState.root;
280
+ while (route.firstChild) route = route.firstChild;
281
+ return route;
282
+ }
283
+ static ɵfac = i0.ɵɵngDeclareFactory({
284
+ minVersion: "12.0.0",
285
+ version: "22.2.4",
286
+ ngImport: i0,
287
+ type: ContextService,
288
+ deps: [],
289
+ target: i0.ɵɵFactoryTarget.Injectable
290
+ });
291
+ static ɵprov = i0.ɵɵngDeclareInjectable({
292
+ minVersion: "12.0.0",
293
+ version: "22.2.4",
294
+ ngImport: i0,
295
+ type: ContextService,
296
+ providedIn: "root"
297
+ });
298
+ };
299
+ i0.ɵɵngDeclareClassMetadata({
300
+ minVersion: "12.0.0",
301
+ version: "22.2.4",
302
+ ngImport: i0,
303
+ type: ContextService,
304
+ decorators: [{
305
+ type: Injectable,
306
+ args: [{ providedIn: "root" }]
307
+ }]
308
+ });
309
+ function page(path, title, load) {
310
+ return {
311
+ path,
312
+ title: `${title} · Juice Observability`,
313
+ loadComponent: load
314
+ };
315
+ }
316
+ const OBSERVABILITY_ROUTES = [
317
+ page("", "Overview", () => import("./juice-js-observability-overview.page-UBwHjlyq.mjs").then((m) => m.OverviewPage)),
318
+ page("services", "Services", () => import("./juice-js-observability-services.page-BmWgFnC8.mjs").then((m) => m.ServicesPage)),
319
+ page("services/:id", "Service", () => import("./juice-js-observability-service.page-B-SWJ9I5.mjs").then((m) => m.ServicePage)),
320
+ page("traces", "Traces", () => import("./juice-js-observability-traces.page-BCQg4V4o.mjs").then((m) => m.TracesPage)),
321
+ page("traces/:traceId", "Trace", () => import("./juice-js-observability-trace.page-gBhMG2eQ.mjs").then((m) => m.TracePage)),
322
+ page("topology", "Dependencies", () => import("./juice-js-observability-topology.page-DEUNp10U.mjs").then((m) => m.TopologyPage)),
323
+ page("logs", "Logs", () => import("./juice-js-observability-logs.page-BWFLleyX.mjs").then((m) => m.LogsPage)),
324
+ page("metrics", "Metrics", () => import("./juice-js-observability-metrics.page-D1caPbQq.mjs").then((m) => m.MetricsPage)),
325
+ page("resources", "Resources", () => import("./juice-js-observability-resources.page-C0xWOb5I.mjs").then((m) => m.ResourcesPage)),
326
+ page("resources/:id", "Resource", () => import("./juice-js-observability-resource.page-BZwLK_vU.mjs").then((m) => m.ResourcePage)),
327
+ page("alerts", "Alerts", () => import("./juice-js-observability-alerts.page-BLJvD_op.mjs").then((m) => m.AlertsPage)),
328
+ page("alerts/:id", "Alert", () => import("./juice-js-observability-alert.page-D2OL0zTT.mjs").then((m) => m.AlertPage)),
329
+ page("rules", "Rules", () => import("./juice-js-observability-rules.page-Db_U5rRu.mjs").then((m) => m.RulesPage))
330
+ ];
331
+ function observabilityMenus(basePath = "/", order = 1) {
332
+ const base = normalizeBasePath(basePath);
333
+ const link = (path) => base === "/" ? `/${path}` : path ? `${base}/${path}` : base;
334
+ return [
335
+ {
336
+ id: "overview",
337
+ label: "Overview",
338
+ icon: "dashboard",
339
+ routerLink: link("")
340
+ },
341
+ {
342
+ id: "services",
343
+ label: "Services",
344
+ icon: "hub",
345
+ routerLink: link("services")
346
+ },
347
+ {
348
+ id: "traces",
349
+ label: "Traces",
350
+ icon: "timeline",
351
+ routerLink: link("traces")
352
+ },
353
+ {
354
+ id: "topology",
355
+ label: "Dependencies",
356
+ icon: "account_tree",
357
+ routerLink: link("topology")
358
+ },
359
+ {
360
+ id: "logs",
361
+ label: "Logs",
362
+ icon: "notes",
363
+ routerLink: link("logs")
364
+ },
365
+ {
366
+ id: "metrics",
367
+ label: "Metrics",
368
+ icon: "show_chart",
369
+ routerLink: link("metrics")
370
+ },
371
+ {
372
+ id: "resources",
373
+ label: "Resources",
374
+ icon: "dns",
375
+ routerLink: link("resources")
376
+ },
377
+ {
378
+ id: "alerts",
379
+ label: "Alerts",
380
+ icon: "notifications",
381
+ routerLink: link("alerts")
382
+ },
383
+ {
384
+ id: "rules",
385
+ label: "Rules",
386
+ icon: "rule",
387
+ routerLink: link("rules")
388
+ }
389
+ ].map((menu, index) => ({
390
+ ...menu,
391
+ order: order + index
392
+ }));
393
+ }
394
+ export { ApiService, ContextService, LinkPipe, MAX_SEARCH_MS, OBSERVABILITY_OPTIONS, OBSERVABILITY_ROUTES, ObservabilityLinks, RANGES, normalizeBasePath, observabilityMenus, provideJuiceObservability };
395
+
396
+ //# sourceMappingURL=juice-js-observability.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"juice-js-observability.mjs","names":[],"sources":["../../../../projects/juice-js/observability/src/lib/core/options.ts","../../../../projects/juice-js/observability/src/lib/core/api.service.ts","../../../../projects/juice-js/observability/src/lib/core/context.service.ts","../../../../projects/juice-js/observability/src/lib/observability.routes.ts"],"sourcesContent":["import { EnvironmentProviders, Injectable, InjectionToken, Pipe, PipeTransform, inject, makeEnvironmentProviders } from '@angular/core';\n\n/** How the host app mounts Juice Observability. */\nexport interface ObservabilityOptions {\n /** The Juice API origin; empty when the API serves the app (same origin). */\n apiBase: string;\n /** Where the host mounts {@link OBSERVABILITY_ROUTES}: `/` for the whole app, or a path such as `/observability`. */\n basePath: string;\n}\n\nexport const OBSERVABILITY_OPTIONS = new InjectionToken<ObservabilityOptions>('OBSERVABILITY_OPTIONS', {\n providedIn: 'root',\n factory: () => ({ apiBase: '', basePath: '/' }),\n});\n\n/** Configures the pages: where the API is and where the host mounted the routes. */\nexport function provideJuiceObservability(options: Partial<ObservabilityOptions> = {}): EnvironmentProviders {\n return makeEnvironmentProviders([\n { provide: OBSERVABILITY_OPTIONS, useValue: { apiBase: options.apiBase ?? '', basePath: options.basePath ?? '/' } },\n ]);\n}\n\n/** `/observability/` → `/observability`; `` and `/` → `/`. */\nexport function normalizeBasePath(basePath: string): string {\n const trimmed = basePath.trim().replace(/^\\/+|\\/+$/g, '');\n return trimmed ? `/${trimmed}` : '/';\n}\n\n/** Router links to the pages, under the host's mount path. */\n@Injectable({ providedIn: 'root' })\nexport class ObservabilityLinks {\n readonly base = normalizeBasePath(inject(OBSERVABILITY_OPTIONS).basePath);\n\n /** `to('services', id)` → `['/observability', 'services', id]`. */\n to(...segments: unknown[]): unknown[] { return [this.base, ...segments]; }\n}\n\n/** `['services', id] | jcLink`: the same as {@link ObservabilityLinks.to} in a template. */\n@Pipe({ name: 'jcLink' })\nexport class LinkPipe implements PipeTransform {\n private readonly links = inject(ObservabilityLinks);\n transform(segments: unknown[]): unknown[] { return this.links.to(...segments); }\n}\n","import { HttpClient, HttpParams } from '@angular/common/http';\nimport { Injectable, inject } from '@angular/core';\nimport { EMPTY, Observable, expand, map, reduce, shareReplay, switchMap } from 'rxjs';\nimport { OBSERVABILITY_OPTIONS } from './options';\nimport {\n AlertDetail, Alert, Application, DependencyEvidence, DeploymentEnvironment, Health, LogRecord, MetricInfo, Operation,\n Page, ResourceDetail, ResourceSummary, Rule, SearchHit, Series, ServiceStats, Tenant, TelemetryPage, Topology, Trace,\n TraceSummary,\n} from './models';\n\nexport type Query = Record<string, string | number | boolean | string[] | null | undefined>;\n\n/**\n * The Console's only backend: the Juice API (Console requirements §3). One deployment serves one tenant, the one the\n * signed-in user's token names.\n */\n@Injectable({ providedIn: 'root' })\nexport class ApiService {\n private readonly http = inject(HttpClient);\n private readonly base = `${inject(OBSERVABILITY_OPTIONS).apiBase}/api/v1`;\n\n /** The caller's tenant, fetched once. */\n readonly tenant$: Observable<Tenant> = this.http.get<Page<Tenant>>(`${this.base}/tenants`).pipe(\n map(page => {\n if (page.items.length === 0) throw new Error('No tenant is authorized for this user.');\n return page.items[0];\n }),\n shareReplay(1),\n );\n\n private get<T>(path: string, query: Query = {}): Observable<T> {\n let params = new HttpParams();\n for (const [key, value] of Object.entries(query)) {\n if (value === null || value === undefined || value === '') continue;\n for (const item of Array.isArray(value) ? value : [value]) params = params.append(key, String(item));\n }\n return this.tenant$.pipe(switchMap(t => this.http.get<T>(`${this.base}/tenants/${t.id}${path}`, { params })));\n }\n\n /** Every page of a cursor-paged list (bounded: at most `maxPages` requests). */\n private all<T>(path: string, query: Query = {}, maxPages = 25): Observable<Page<T>> {\n let pages = 0;\n return this.get<Page<T>>(path, { ...query, limit: 200 }).pipe(\n expand(page => page.nextCursor && ++pages < maxPages ? this.get<Page<T>>(path, { ...query, limit: 200, cursor: page.nextCursor }) : EMPTY),\n reduce((all, page) => ({ items: [...all.items, ...page.items], nextCursor: page.nextCursor }), { items: [] as T[], nextCursor: null as string | null }),\n );\n }\n\n environments() { return this.get<Page<DeploymentEnvironment>>('/environments'); }\n applications() { return this.get<Page<Application>>('/applications', { limit: 200 }); }\n resources(type?: string) { return this.all<ResourceSummary>('/resources', { type }); }\n resource(id: string) { return this.get<ResourceDetail>(`/resources/${id}`); }\n health(query: Query) { return this.all<Health>('/health', query); }\n topology(environment?: string | null) { return this.get<Topology>('/topology', { environment }); }\n\n rules() { return this.get<Page<Rule>>('/rules', { limit: 200 }); }\n /** One page of alerts, newest first; pass the previous page's `nextCursor` as `cursor` for the next. */\n alerts(query: Query) { return this.get<Page<Alert>>('/alerts', { limit: 100, ...query }); }\n /** Every alert matching the query (bounded, like the other complete lists). */\n allAlerts(query: Query) { return this.all<Alert>('/alerts', query); }\n alert(id: string) { return this.get<AlertDetail>(`/alerts/${id}`); }\n acknowledge(id: string, comment: string): Observable<Alert> {\n return this.tenant$.pipe(switchMap(t => this.http.post<Alert>(`${this.base}/tenants/${t.id}/alerts/${id}/acknowledgement`,\n { comment }, { headers: { 'Idempotency-Key': crypto.randomUUID() } })));\n }\n\n traces(query: Query) { return this.get<TelemetryPage<TraceSummary>>('/telemetry/traces', query); }\n trace(traceId: string) { return this.get<Trace>(`/telemetry/traces/${traceId}`); }\n logs(query: Query) { return this.get<TelemetryPage<LogRecord>>('/telemetry/logs', query); }\n operations(query: Query) { return this.get<TelemetryPage<Operation>>('/telemetry/operations', query); }\n metrics(query: Query) { return this.get<TelemetryPage<MetricInfo>>('/telemetry/metrics', query); }\n series(query: Query) { return this.get<Series>('/telemetry/metrics/series', query); }\n serviceStats(query: Query) { return this.get<ServiceStats>('/telemetry/service-stats', query); }\n dependency(id: string, query: Query) { return this.get<DependencyEvidence>(`/telemetry/dependencies/${id}`, query); }\n search(q: string) { return this.get<TelemetryPage<SearchHit>>('/search', { q }); }\n}\n","import { Injectable, computed, inject } from '@angular/core';\nimport { toSignal } from '@angular/core/rxjs-interop';\nimport { ActivatedRoute, NavigationEnd, Params, Router } from '@angular/router';\nimport { filter, map, startWith } from 'rxjs';\n\nexport const RANGES: Record<string, number> = {\n '15m': 15 * 60_000, '1h': 3_600_000, '6h': 6 * 3_600_000, '24h': 24 * 3_600_000, '7d': 7 * 24 * 3_600_000,\n};\n\n/** Search ranges are at most 24 hours (the API's bound); series and operations take up to 7 days. */\nexport const MAX_SEARCH_MS = RANGES['24h'];\n\n/**\n * The investigation context that follows the user between pages (Console requirements §17): environment and time\n * range. It lives in the URL (`env`, and `range` or `from`/`to`), so every page is a deep link and the browser's back\n * button restores it.\n */\n@Injectable({ providedIn: 'root' })\nexport class ContextService {\n private readonly router = inject(Router);\n\n // Created on first use, after the first navigation may already have ended: start from the current URL.\n private readonly params = toSignal(this.router.events.pipe(\n filter(e => e instanceof NavigationEnd),\n startWith(null),\n map(() => this.leaf().snapshot.queryParams),\n ), { initialValue: {} as Params });\n\n readonly environment = computed(() => (this.params()['env'] as string | undefined) ?? '');\n readonly range = computed(() => {\n const p = this.params();\n return p['from'] && p['to'] ? 'custom' : ((p['range'] as string | undefined) ?? '1h');\n });\n readonly absolute = computed(() => {\n const p = this.params();\n return p['from'] && p['to'] ? { from: p['from'] as string, to: p['to'] as string } : null;\n });\n\n /** The window as ISO strings, evaluated now for a relative range. */\n window(maxMs?: number): { from: string; to: string } {\n const absolute = this.absolute();\n if (absolute) return absolute;\n const to = Date.now();\n const span = Math.min(RANGES[this.range()] ?? RANGES['1h'], maxMs ?? Number.MAX_SAFE_INTEGER);\n return { from: new Date(to - span).toISOString(), to: new Date(to).toISOString() };\n }\n\n /** Query parameters for a link: the context plus the target page's own. */\n keep(extra: Params = {}): Params {\n const p = this.params();\n const context: Params = {};\n for (const key of ['env', 'range', 'from', 'to']) if (p[key]) context[key] = p[key];\n return { ...context, ...extra };\n }\n\n /** Changes the context on the current page, keeping its other parameters. */\n set(change: { env?: string | null; range?: string | null; from?: string | null; to?: string | null }) {\n const queryParams: Params = { ...change };\n if (change.range) Object.assign(queryParams, { from: null, to: null });\n void this.router.navigate([], { relativeTo: this.leaf(), queryParams, queryParamsHandling: 'merge' });\n }\n\n /** A time window centred on a moment, for \"around this span\" links. */\n around(at: string, durationMs: number, paddingMs = 5 * 60_000): Params {\n const start = new Date(at).getTime();\n return this.keep({\n from: new Date(start - paddingMs).toISOString(),\n to: new Date(start + durationMs + paddingMs).toISOString(),\n range: null,\n });\n }\n\n private leaf(): ActivatedRoute {\n let route = this.router.routerState.root;\n while (route.firstChild) route = route.firstChild;\n return route;\n }\n}\n","import { Routes } from '@angular/router';\nimport { Menus } from '@juice-js/core';\nimport { normalizeBasePath } from './core/options';\n\nfunction page(path: string, title: string, load: Routes[number]['loadComponent']): Routes[number] {\n return { path, title: `${title} · Juice Observability`, loadComponent: load };\n}\n\n/**\n * The pages, investigation first (Console requirements §4): services, traces, topology, logs and metrics; alerts and\n * rules after. Detail pages are deep links (§17) and carry the environment and time range in their query parameters.\n * The host mounts them at its `basePath` (see {@link provideJuiceObservability}) and adds its own guards.\n */\nexport const OBSERVABILITY_ROUTES: Routes = [\n page('', 'Overview', () => import('./pages/overview.page').then(m => m.OverviewPage)),\n page('services', 'Services', () => import('./pages/services.page').then(m => m.ServicesPage)),\n page('services/:id', 'Service', () => import('./pages/service.page').then(m => m.ServicePage)),\n page('traces', 'Traces', () => import('./pages/traces.page').then(m => m.TracesPage)),\n page('traces/:traceId', 'Trace', () => import('./pages/trace.page').then(m => m.TracePage)),\n page('topology', 'Dependencies', () => import('./pages/topology.page').then(m => m.TopologyPage)),\n page('logs', 'Logs', () => import('./pages/logs.page').then(m => m.LogsPage)),\n page('metrics', 'Metrics', () => import('./pages/metrics.page').then(m => m.MetricsPage)),\n page('resources', 'Resources', () => import('./pages/resources.page').then(m => m.ResourcesPage)),\n page('resources/:id', 'Resource', () => import('./pages/resource.page').then(m => m.ResourcePage)),\n page('alerts', 'Alerts', () => import('./pages/alerts.page').then(m => m.AlertsPage)),\n page('alerts/:id', 'Alert', () => import('./pages/alert.page').then(m => m.AlertPage)),\n page('rules', 'Rules', () => import('./pages/rules.page').then(m => m.RulesPage)),\n];\n\n/** The side menu entries (juice-layout), investigation pages first, linking under `basePath`. */\nexport function observabilityMenus(basePath = '/', order = 1): Menus {\n const base = normalizeBasePath(basePath);\n const link = (path: string) => (base === '/' ? `/${path}` : path ? `${base}/${path}` : base);\n return [\n { id: 'overview', label: 'Overview', icon: 'dashboard', routerLink: link('') },\n { id: 'services', label: 'Services', icon: 'hub', routerLink: link('services') },\n { id: 'traces', label: 'Traces', icon: 'timeline', routerLink: link('traces') },\n { id: 'topology', label: 'Dependencies', icon: 'account_tree', routerLink: link('topology') },\n { id: 'logs', label: 'Logs', icon: 'notes', routerLink: link('logs') },\n { id: 'metrics', label: 'Metrics', icon: 'show_chart', routerLink: link('metrics') },\n { id: 'resources', label: 'Resources', icon: 'dns', routerLink: link('resources') },\n { id: 'alerts', label: 'Alerts', icon: 'notifications', routerLink: link('alerts') },\n { id: 'rules', label: 'Rules', icon: 'rule', routerLink: link('rules') },\n ].map((menu, index) => ({ ...menu, order: order + index }));\n}\n"],"mappings":";;;;;;AAUA,MAAa,wBAAwB,IAAI,eAAqC,yBAAyB;CACrG,YAAY;CACZ,gBAAgB;EAAE,SAAS;EAAI,UAAU;CAAG;AAC7C,CAAA;AAGD,SAAgB,0BAA0B,UAAyC,CAAA,GAAE;CACnF,OAAO,yBAAyB,CAC9B;EAAE,SAAS;EAAuB,UAAU;GAAE,SAAS,QAAQ,WAAW;GAAI,UAAU,QAAQ,YAAY;EAAG;CAAE,CAClH,CAAA;AACH;AAGA,SAAgB,kBAAkB,UAAgB;CAChD,MAAM,UAAU,SAAS,KAAI,CAAE,CAAC,QAAQ,cAAc,EAAE;CACxD,OAAO,UAAU,IAAI,YAAY;AACnC;AAIA,IAAa,qBAAb,MAAa,mBAAkB;CACpB,OAAO,kBAAkB,OAAO,qBAAqB,CAAC,CAAC,QAAQ;CAGxE,GAAG,GAAG,UAAmB;EAAe,OAAO,CAAC,KAAK,MAAM,GAAG,QAAQ;CAAG;CAJ9D,OAAA,OAAA,GAAA,mBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAAkB,MAAA,CAAA;EAAA,QAAA,GAAA,gBAAA;CAAA,CAAA;CAAlB,OAAA,QAAA,GAAA,sBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAAkB,YADL;CAAM,CAAA;;AACnB,GAAA,yBAAA;CAAA,YAAA;CAAA,SAAA;CAAA,UAAA;CAAA,MAAA;CAAkB,YAAA,CAAA;EAD9B,MAAA;EAAW,MAAA,CAAA,EAAE,YAAY,OAAM,CAAE;;;AAUlC,IAAa,WAAb,MAAa,SAAQ;CACF,QAAQ,OAAO,kBAAkB;CAClD,UAAU,UAAmB;EAAe,OAAO,KAAK,MAAM,GAAG,GAAG,QAAQ;CAAG;CAFpE,OAAA,OAAA,GAAA,mBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAAQ,MAAA,CAAA;EAAA,QAAA,GAAA,gBAAA;CAAA,CAAA;CAAR,OAAA,QAAA,GAAA,gBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAAQ,cAAA;EAAA,MAAA;CAAA,CAAA;;AAAR,GAAA,yBAAA;CAAA,YAAA;CAAA,SAAA;CAAA,UAAA;CAAA,MAAA;CAAQ,YAAA,CAAA;EADpB,MAAA;EAAK,MAAA,CAAA,EAAE,MAAM,SAAQ,CAAE;;;ACrBxB,IAAa,aAAb,MAAa,WAAU;CACJ,OAAO,OAAO,UAAU;CACxB,OAAO,GAAG,OAAO,qBAAqB,CAAC,CAAC,QAAO;CAGvD,UAA8B,KAAK,KAAK,IAAkB,GAAG,KAAK,KAAI,SAAU,CAAC,CAAC,KACzF,KAAI,SAAO;EACT,IAAI,KAAK,MAAM,WAAW,GAAG,MAAM,IAAI,MAAM,wCAAwC;EACrF,OAAO,KAAK,MAAM;CACpB,CAAC,GACD,YAAY,CAAC,CAAC;CAGR,IAAO,MAAc,QAAe,CAAA,GAAE;EAC5C,IAAI,SAAS,IAAI,WAAU;EAC3B,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,KAAK,GAAG;GAChD,IAAI,UAAU,QAAQ,UAAU,UAAa,UAAU,IAAI;GAC3D,KAAK,MAAM,QAAQ,MAAM,QAAQ,KAAK,IAAI,QAAQ,CAAC,KAAK,GAAG,SAAS,OAAO,OAAO,KAAK,OAAO,IAAI,CAAC;EACrG;EACA,OAAO,KAAK,QAAQ,KAAK,WAAU,MAAK,KAAK,KAAK,IAAO,GAAG,KAAK,KAAI,WAAY,EAAE,KAAK,QAAQ,EAAE,OAAM,CAAE,CAAC,CAAC;CAC9G;CAGQ,IAAO,MAAc,QAAe,CAAA,GAAI,WAAW,IAAE;EAC3D,IAAI,QAAQ;EACZ,OAAO,KAAK,IAAa,MAAM;GAAE,GAAG;GAAO,OAAO;EAAG,CAAE,CAAC,CAAC,KACvD,QAAO,SAAQ,KAAK,cAAc,EAAE,QAAQ,WAAW,KAAK,IAAa,MAAM;GAAE,GAAG;GAAO,OAAO;GAAK,QAAQ,KAAK;EAAU,CAAE,IAAI,KAAK,GACzI,QAAQ,KAAK,UAAU;GAAE,OAAO,CAAC,GAAG,IAAI,OAAO,GAAG,KAAK,KAAK;GAAG,YAAY,KAAK;EAAU,IAAK;GAAE,OAAO,CAAA;GAAW,YAAY;EAAqB,CAAE,CAAC;CAE3J;CAEA,eAAY;EAAK,OAAO,KAAK,IAAiC,eAAe;CAAG;CAChF,eAAY;EAAK,OAAO,KAAK,IAAuB,iBAAiB,EAAE,OAAO,IAAG,CAAE;CAAG;CACtF,UAAU,MAAa;EAAI,OAAO,KAAK,IAAqB,cAAc,EAAE,KAAI,CAAE;CAAG;CACrF,SAAS,IAAU;EAAI,OAAO,KAAK,IAAoB,cAAc,IAAI;CAAG;CAC5E,OAAO,OAAY;EAAI,OAAO,KAAK,IAAY,WAAW,KAAK;CAAG;CAClE,SAAS,aAA2B;EAAI,OAAO,KAAK,IAAc,aAAa,EAAE,YAAW,CAAE;CAAG;CAEjG,QAAK;EAAK,OAAO,KAAK,IAAgB,UAAU,EAAE,OAAO,IAAG,CAAE;CAAG;CAEjE,OAAO,OAAY;EAAI,OAAO,KAAK,IAAiB,WAAW;GAAE,OAAO;GAAK,GAAG;EAAK,CAAE;CAAG;CAE1F,UAAU,OAAY;EAAI,OAAO,KAAK,IAAW,WAAW,KAAK;CAAG;CACpE,MAAM,IAAU;EAAI,OAAO,KAAK,IAAiB,WAAW,IAAI;CAAG;CACnE,YAAY,IAAY,SAAe;EACrC,OAAO,KAAK,QAAQ,KAAK,WAAU,MAAK,KAAK,KAAK,KAAY,GAAG,KAAK,KAAI,WAAY,EAAE,GAAE,UAAW,GAAE,mBACrG,EAAE,QAAO,GAAI,EAAE,SAAS,EAAE,mBAAmB,OAAO,WAAU,EAAE,EAAE,CAAE,CAAC,CAAC;CAC1E;CAEA,OAAO,OAAY;EAAI,OAAO,KAAK,IAAiC,qBAAqB,KAAK;CAAG;CACjG,MAAM,SAAe;EAAI,OAAO,KAAK,IAAW,qBAAqB,SAAS;CAAG;CACjF,KAAK,OAAY;EAAI,OAAO,KAAK,IAA8B,mBAAmB,KAAK;CAAG;CAC1F,WAAW,OAAY;EAAI,OAAO,KAAK,IAA8B,yBAAyB,KAAK;CAAG;CACtG,QAAQ,OAAY;EAAI,OAAO,KAAK,IAA+B,sBAAsB,KAAK;CAAG;CACjG,OAAO,OAAY;EAAI,OAAO,KAAK,IAAY,6BAA6B,KAAK;CAAG;CACpF,aAAa,OAAY;EAAI,OAAO,KAAK,IAAkB,4BAA4B,KAAK;CAAG;CAC/F,WAAW,IAAY,OAAY;EAAI,OAAO,KAAK,IAAwB,2BAA2B,MAAM,KAAK;CAAG;CACpH,OAAO,GAAS;EAAI,OAAO,KAAK,IAA8B,WAAW,EAAE,EAAC,CAAE;CAAG;CAzDtE,OAAA,OAAA,GAAA,mBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAAU,MAAA,CAAA;EAAA,QAAA,GAAA,gBAAA;CAAA,CAAA;CAAV,OAAA,QAAA,GAAA,sBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAAU,YADG;CAAM,CAAA;;AACnB,GAAA,yBAAA;CAAA,YAAA;CAAA,SAAA;CAAA,UAAA;CAAA,MAAA;CAAU,YAAA,CAAA;EADtB,MAAA;EAAW,MAAA,CAAA,EAAE,YAAY,OAAM,CAAE;;;ACXlC,MAAa,SAAiC;CAC5C,OAAO;CAAa,MAAM;CAAW,MAAM;CAAe,OAAO;CAAgB,MAAM;AACxF;AAGD,MAAa,gBAAgB,OAAO;AAQpC,IAAa,iBAAb,MAAa,eAAc;CACR,SAAS,OAAO,MAAM;CAGtB,SAAS,SAAS,KAAK,OAAO,OAAO,KACpD,QAAO,MAAK,aAAa,aAAa,GACtC,UAAU,IAAI,GACd,UAAU,KAAK,KAAI,CAAE,CAAC,SAAS,WAAW,CAAC,GAC1C,EAAE,cAAc,CAAA,EAAY,CAAE;CAExB,cAAc,eAAgB,KAAK,OAAM,CAAE,CAAC,UAAiC,IAAG,GAAA,YAAA,CAAA,EAAA,WAAA,cAAA,CAAA,+BAAA,CAAA,CAAA;CAChF,QAAQ,eAAc;EAC7B,MAAM,IAAI,KAAK,OAAM;EACrB,OAAO,EAAE,WAAW,EAAE,QAAQ,WAAa,EAAE,YAAmC;CAClF,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,QAAA,CAAA,+BAAA,CAAA,CAAA;CACO,WAAW,eAAc;EAChC,MAAM,IAAI,KAAK,OAAM;EACrB,OAAO,EAAE,WAAW,EAAE,QAAQ;GAAE,MAAM,EAAE;GAAmB,IAAI,EAAE;EAAe,IAAK;CACvF,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,WAAA,CAAA,+BAAA,CAAA,CAAA;CAGF,OAAO,OAAc;EACnB,MAAM,WAAW,KAAK,SAAQ;EAC9B,IAAI,UAAU,OAAO;EACrB,MAAM,KAAK,KAAK,IAAG;EACnB,MAAM,OAAO,KAAK,IAAI,OAAO,KAAK,MAAK,MAAO,OAAO,OAAO,SAAS,OAAO,gBAAgB;EAC5F,OAAO;GAAE,MAAM,IAAI,KAAK,KAAK,IAAI,CAAC,CAAC,YAAW;GAAI,IAAI,IAAI,KAAK,EAAE,CAAC,CAAC,YAAW;EAAE;CAClF;CAGA,KAAK,QAAgB,CAAA,GAAE;EACrB,MAAM,IAAI,KAAK,OAAM;EACrB,MAAM,UAAkB,CAAA;EACxB,KAAK,MAAM,OAAO;GAAC;GAAO;GAAS;GAAQ;EAAI,GAAG,IAAI,EAAE,MAAM,QAAQ,OAAO,EAAE;EAC/E,OAAO;GAAE,GAAG;GAAS,GAAG;EAAK;CAC/B;CAGA,IAAI,QAAgG;EAClG,MAAM,cAAsB,EAAE,GAAG,OAAM;EACvC,IAAI,OAAO,OAAO,OAAO,OAAO,aAAa;GAAE,MAAM;GAAM,IAAI;EAAI,CAAE;EACrE,AAAK,KAAK,OAAO,SAAS,CAAA,GAAI;GAAE,YAAY,KAAK,KAAI;GAAI;GAAa,qBAAqB;EAAO,CAAE;CACtG;CAGA,OAAO,IAAY,YAAoB,YAAY,KAAU;EAC3D,MAAM,QAAQ,IAAI,KAAK,EAAE,CAAC,CAAC,QAAO;EAClC,OAAO,KAAK,KAAK;GACf,MAAM,IAAI,KAAK,QAAQ,SAAS,CAAC,CAAC,YAAW;GAC7C,IAAI,IAAI,KAAK,QAAQ,aAAa,SAAS,CAAC,CAAC,YAAW;GACxD,OAAO;EACR,CAAA;CACH;CAEQ,OAAI;EACV,IAAI,QAAQ,KAAK,OAAO,YAAY;EACpC,OAAO,MAAM,YAAY,QAAQ,MAAM;EACvC,OAAO;CACT;CA1DW,OAAA,OAAA,GAAA,mBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAAc,MAAA,CAAA;EAAA,QAAA,GAAA,gBAAA;CAAA,CAAA;CAAd,OAAA,QAAA,GAAA,sBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAAc,YADD;CAAM,CAAA;;AACnB,GAAA,yBAAA;CAAA,YAAA;CAAA,SAAA;CAAA,UAAA;CAAA,MAAA;CAAc,YAAA,CAAA;EAD1B,MAAA;EAAW,MAAA,CAAA,EAAE,YAAY,OAAM,CAAE;;;ACblC,SAAS,KAAK,MAAc,OAAe,MAAqC;CAC9E,OAAO;EAAE;EAAM,OAAO,GAAG,MAAK;EAA0B,eAAe;CAAI;AAC7E;AAOA,MAAa,uBAA+B;CAC1C,KAAK,IAAI,kBAAkB,OAAO,sDAAwB,CAAC,MAAK,MAAK,EAAE,YAAY,CAAC;CACpF,KAAK,YAAY,kBAAkB,OAAO,sDAAwB,CAAC,MAAK,MAAK,EAAE,YAAY,CAAC;CAC5F,KAAK,gBAAgB,iBAAiB,OAAO,qDAAuB,CAAC,MAAK,MAAK,EAAE,WAAW,CAAC;CAC7F,KAAK,UAAU,gBAAgB,OAAO,oDAAsB,CAAC,MAAK,MAAK,EAAE,UAAU,CAAC;CACpF,KAAK,mBAAmB,eAAe,OAAO,mDAAqB,CAAC,MAAK,MAAK,EAAE,SAAS,CAAC;CAC1F,KAAK,YAAY,sBAAsB,OAAO,sDAAwB,CAAC,MAAK,MAAK,EAAE,YAAY,CAAC;CAChG,KAAK,QAAQ,cAAc,OAAO,kDAAoB,CAAC,MAAK,MAAK,EAAE,QAAQ,CAAC;CAC5E,KAAK,WAAW,iBAAiB,OAAO,qDAAuB,CAAC,MAAK,MAAK,EAAE,WAAW,CAAC;CACxF,KAAK,aAAa,mBAAmB,OAAO,uDAAyB,CAAC,MAAK,MAAK,EAAE,aAAa,CAAC;CAChG,KAAK,iBAAiB,kBAAkB,OAAO,sDAAwB,CAAC,MAAK,MAAK,EAAE,YAAY,CAAC;CACjG,KAAK,UAAU,gBAAgB,OAAO,oDAAsB,CAAC,MAAK,MAAK,EAAE,UAAU,CAAC;CACpF,KAAK,cAAc,eAAe,OAAO,mDAAqB,CAAC,MAAK,MAAK,EAAE,SAAS,CAAC;CACrF,KAAK,SAAS,eAAe,OAAO,mDAAqB,CAAC,MAAK,MAAK,EAAE,SAAS,CAAC;AACjF;AAGD,SAAgB,mBAAmB,WAAW,KAAK,QAAQ,GAAC;CAC1D,MAAM,OAAO,kBAAkB,QAAQ;CACvC,MAAM,QAAQ,SAAkB,SAAS,MAAM,IAAI,SAAS,OAAO,GAAG,KAAI,GAAI,SAAS;CACvF,OAAO;EACL;GAAE,IAAI;GAAY,OAAO;GAAY,MAAM;GAAa,YAAY,KAAK,EAAE;EAAC;EAC5E;GAAE,IAAI;GAAY,OAAO;GAAY,MAAM;GAAO,YAAY,KAAK,UAAU;EAAC;EAC9E;GAAE,IAAI;GAAU,OAAO;GAAU,MAAM;GAAY,YAAY,KAAK,QAAQ;EAAC;EAC7E;GAAE,IAAI;GAAY,OAAO;GAAgB,MAAM;GAAgB,YAAY,KAAK,UAAU;EAAC;EAC3F;GAAE,IAAI;GAAQ,OAAO;GAAQ,MAAM;GAAS,YAAY,KAAK,MAAM;EAAC;EACpE;GAAE,IAAI;GAAW,OAAO;GAAW,MAAM;GAAc,YAAY,KAAK,SAAS;EAAC;EAClF;GAAE,IAAI;GAAa,OAAO;GAAa,MAAM;GAAO,YAAY,KAAK,WAAW;EAAC;EACjF;GAAE,IAAI;GAAU,OAAO;GAAU,MAAM;GAAiB,YAAY,KAAK,QAAQ;EAAC;EAClF;GAAE,IAAI;GAAS,OAAO;GAAS,MAAM;GAAQ,YAAY,KAAK,OAAO;EAAC;CACvE,CAAA,CAAC,KAAK,MAAM,WAAW;EAAE,GAAG;EAAM,OAAO,QAAQ;CAAK,EAAG;AAC5D"}
package/package.json CHANGED
@@ -1,6 +1,50 @@
1
1
  {
2
2
  "name": "@juice-js/observability",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.0-alpha.1",
4
+ "description": "Juice Observability pages (services, traces, topology, logs, metrics, resources, alerts, rules) for an Angular app on juice-layout",
5
+ "author": "jack@creatorflow.io",
6
+ "license": "MIT",
7
+ "bugs": {
8
+ "url": "https://github.com/creatorflow-io/Juice.Observability/issues"
9
+ },
10
+ "homepage": "https://github.com/creatorflow-io/Juice.Observability#readme",
11
+ "publishConfig": {
12
+ "access": "public",
13
+ "provenance": true
14
+ },
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/creatorflow-io/Juice.Observability.git",
18
+ "directory": "console/projects/juice-js/observability"
19
+ },
20
+ "peerDependencies": {
21
+ "@angular/cdk": "^22.0.0",
22
+ "@angular/common": "^22.0.0",
23
+ "@angular/core": "^22.0.0",
24
+ "@angular/forms": "^22.0.0",
25
+ "@angular/material": "^22.0.0",
26
+ "@angular/router": "^22.0.0",
27
+ "@juice-js/core": "^2.0.1",
28
+ "@juice-js/layout": "^2.0.1",
29
+ "rxjs": "^7.8.0"
30
+ },
31
+ "dependencies": {
32
+ "cytoscape": "^3.34.4",
33
+ "echarts": "^6.1.0",
34
+ "tslib": "^2.3.0"
35
+ },
36
+ "sideEffects": false,
37
+ "exports": {
38
+ ".": {
39
+ "sass": "./styles/_observability.scss",
40
+ "types": "./types/juice-js-observability.d.ts",
41
+ "default": "./fesm2022/juice-js-observability.mjs"
42
+ },
43
+ "./package.json": {
44
+ "default": "./package.json"
45
+ }
46
+ },
47
+ "module": "fesm2022/juice-js-observability.mjs",
48
+ "typings": "types/juice-js-observability.d.ts",
49
+ "type": "module"
50
+ }
@@ -0,0 +1,133 @@
1
+ // Juice Observability page styles: design tokens (light, and body.dark from juice-layout's dark-mode toggle) and the
2
+ // jc-* classes the pages use. Global by design: @use it once from the host app's styles.
3
+
4
+ // ─── Juice Console design tokens ───
5
+ :root {
6
+ --jc-bg: #f7f8fa; --jc-surface: #fff; --jc-border: #e3e6ea; --jc-text: #1f2328; --jc-muted: #667085;
7
+ --jc-accent: #0f766e; --jc-select: #e6f4f1;
8
+ --jc-ok: #15803d; --jc-ok-bg: #dcfce7; --jc-warn: #b45309; --jc-warn-bg: #fef3c7;
9
+ --jc-bad: #b91c1c; --jc-bad-bg: #fee2e2; --jc-crit: #7f1d1d; --jc-crit-bg: #fecaca;
10
+ --jc-off: #475569; --jc-off-bg: #e2e8f0; --jc-none: #64748b; --jc-none-bg: #f1f5f9;
11
+ }
12
+ body.dark {
13
+ --jc-bg: #111418; --jc-surface: #1a1e23; --jc-border: #2c323a; --jc-text: #e6e8eb; --jc-muted: #98a2b3;
14
+ --jc-accent: #5eead4; --jc-select: #12302c;
15
+ --jc-ok: #4ade80; --jc-ok-bg: #12301d; --jc-warn: #fbbf24; --jc-warn-bg: #3a2c0b;
16
+ --jc-bad: #f87171; --jc-bad-bg: #3b1515; --jc-crit: #fecaca; --jc-crit-bg: #5c1a1a;
17
+ --jc-off: #cbd5e1; --jc-off-bg: #2a3240; --jc-none: #94a3b8; --jc-none-bg: #232a33;
18
+ }
19
+
20
+ .jc-muted { color: var(--jc-muted); }
21
+ .jc-small { font-size: 12px; }
22
+ .jc-mono { font-family: 'JetBrains Mono', Consolas, monospace; font-size: 13px; }
23
+ .jc-nowrap { white-space: nowrap; }
24
+ .jc-bad { color: var(--jc-bad); }
25
+ .jc-error { color: var(--jc-bad); }
26
+ // Under a paged list: how many rows are shown, and the button for the next page.
27
+ .jc-more { display: flex; align-items: center; gap: 12px; padding: 8px 0; font-size: 13px; }
28
+ .jc-notice { padding: 8px 12px; border-left: 3px solid var(--jc-warn); background: var(--jc-warn-bg); font-size: 13px; }
29
+ .jc-spacer { flex: 1; }
30
+
31
+ .jc-context { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; padding: 4px 0 12px; border-bottom: 1px solid var(--jc-border); margin-bottom: 12px; }
32
+ .jc-crumb { color: var(--jc-muted); }
33
+ .jc-crumb-sep { color: var(--jc-muted); }
34
+ .jc-field-sm { width: 150px; }
35
+ .jc-field-wide { min-width: 320px; flex: 1; }
36
+ .jc-lookup { display: flex; align-items: center; gap: 4px; border: 1px solid var(--jc-border); border-radius: 6px; padding: 2px 8px; background: var(--jc-surface);
37
+ input { border: 0; outline: 0; background: transparent; color: inherit; width: 240px; font: inherit; padding: 6px 0; }
38
+ mat-icon { color: var(--jc-muted); font-size: 18px; height: 18px; width: 18px; } }
39
+ .jc-lookup-message { text-align: right; margin: -8px 0 8px; }
40
+
41
+ .jc-card { background: var(--jc-surface); border: 1px solid var(--jc-border); border-radius: 8px; padding: 12px 16px; margin-bottom: 12px; }
42
+ .jc-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
43
+ .jc-grid-2 { display: grid; grid-template-columns: repeat(auto-fit, minmax(360px, 1fr)); gap: 12px; }
44
+ .jc-grid-3 { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 12px; }
45
+ .jc-tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 10px; margin-bottom: 12px; }
46
+ .jc-tile { display: flex; flex-direction: column; gap: 4px; align-items: flex-start; strong { font-size: 24px; } }
47
+ .jc-list-row { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; padding: 6px 0; border-bottom: 1px solid var(--jc-border); }
48
+ .jc-list-row:last-child { border-bottom: 0; }
49
+ .jc-summary { display: flex; flex-wrap: wrap; gap: 8px 16px; align-items: center; margin-bottom: 8px; }
50
+ .jc-actions { display: flex; flex-wrap: wrap; gap: 6px; margin: 8px 0; }
51
+ .jc-filter-form { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin-bottom: 12px; }
52
+ .jc-filters { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 8px; }
53
+ .jc-chip { display: inline-flex; gap: 6px; align-items: center; border: 1px solid var(--jc-border); border-radius: 14px; padding: 2px 10px; font-size: 13px; color: var(--jc-text); }
54
+ .jc-chip.jc-selected { background: var(--jc-select); border-color: var(--jc-accent); }
55
+
56
+ .jc-badge { display: inline-block; padding: 1px 8px; border-radius: 10px; font-size: 12px; font-weight: 500; white-space: nowrap; background: var(--jc-none-bg); color: var(--jc-none); }
57
+ .jc-health-healthy { background: var(--jc-ok-bg); color: var(--jc-ok); }
58
+ .jc-health-degraded { background: var(--jc-warn-bg); color: var(--jc-warn); }
59
+ .jc-health-unhealthy { background: var(--jc-bad-bg); color: var(--jc-bad); }
60
+ .jc-health-critical { background: var(--jc-crit-bg); color: var(--jc-crit); }
61
+ .jc-health-offline { background: var(--jc-off-bg); color: var(--jc-off); }
62
+ .jc-health-unknown, .jc-health-none { background: var(--jc-none-bg); color: var(--jc-none); }
63
+ .jc-stale { font-weight: 400; }
64
+ .jc-observed { background: var(--jc-select); color: var(--jc-accent); }
65
+ .jc-inferred { background: transparent; color: var(--jc-muted); border: 1px dashed var(--jc-muted); }
66
+
67
+ .jc-table { width: 100%; border-collapse: collapse; background: var(--jc-surface); font-size: 13px; margin-bottom: 8px;
68
+ th { text-align: left; font-weight: 500; color: var(--jc-muted); padding: 6px 8px; border-bottom: 1px solid var(--jc-border); white-space: nowrap; }
69
+ td { padding: 6px 8px; border-bottom: 1px solid var(--jc-border); vertical-align: top; }
70
+ tr.jc-selected td { background: var(--jc-select); } }
71
+ .jc-bar { display: inline-block; height: 6px; border-radius: 3px; background: var(--jc-accent); opacity: .5; margin-right: 6px; max-width: 80px; vertical-align: middle; }
72
+ .jc-envs { display: flex; flex-wrap: wrap; gap: 4px 12px; }
73
+ .jc-body { margin: 0; white-space: pre-wrap; word-break: break-word; font-family: 'JetBrains Mono', Consolas, monospace; font-size: 12px; max-height: 8em; overflow: auto; }
74
+ .jc-kv { width: 100%; border-collapse: collapse; font-size: 12px;
75
+ th { text-align: left; color: var(--jc-muted); font-weight: 400; padding: 3px 8px 3px 0; vertical-align: top; width: 38%; word-break: break-all; }
76
+ td { padding: 3px 0; word-break: break-word; font-family: 'JetBrains Mono', Consolas, monospace; } }
77
+ .jc-facts { display: grid; grid-template-columns: max-content 1fr; gap: 4px 12px; margin: 8px 0; dt { color: var(--jc-muted); } dd { margin: 0; } }
78
+ .jc-reasons { margin: 6px 0; padding-left: 18px; font-size: 13px; }
79
+
80
+ .jc-health-strip { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 8px; }
81
+ .jc-health-card { min-width: 240px; flex: 1; max-width: 420px; }
82
+ .jc-health-card.jc-selected { border-color: var(--jc-accent); }
83
+
84
+ .jc-chart-title { font-size: 13px; color: var(--jc-muted); margin: 4px 0; }
85
+ .jc-chart { height: 180px; width: 100%; background: var(--jc-surface); border: 1px solid var(--jc-border); border-radius: 6px; }
86
+ .jc-chart-empty { display: flex; align-items: center; justify-content: center; color: var(--jc-muted); font-size: 13px; }
87
+
88
+ // ─── Waterfall ───
89
+ .jc-legend { display: flex; flex-wrap: wrap; gap: 12px; font-size: 12px; margin-bottom: 6px; i { display: inline-block; width: 10px; height: 10px; border-radius: 2px; margin-right: 4px; } }
90
+ .jc-trace { display: grid; grid-template-columns: 1fr; gap: 12px; }
91
+ .jc-trace.jc-with-detail { grid-template-columns: minmax(0, 1.6fr) minmax(340px, 1fr); }
92
+ .jc-waterfall { background: var(--jc-surface); border: 1px solid var(--jc-border); border-radius: 8px; font-size: 12px; overflow: hidden; }
93
+ .jc-wf-head, .jc-wf-row { display: grid; grid-template-columns: minmax(260px, 40%) 1fr; align-items: center; }
94
+ .jc-wf-head { color: var(--jc-muted); border-bottom: 1px solid var(--jc-border); padding: 4px 8px; }
95
+ .jc-wf-scale { position: relative; height: 16px; span { position: absolute; transform: translateX(-2px); } }
96
+ .jc-wf-row { padding: 2px 8px; cursor: pointer; border-bottom: 1px solid transparent; }
97
+ .jc-wf-row:hover { background: var(--jc-select); }
98
+ .jc-wf-row.jc-selected { background: var(--jc-select); border-bottom-color: var(--jc-accent); }
99
+ .jc-wf-row.jc-wf-error .jc-wf-name { color: var(--jc-bad); }
100
+ .jc-wf-name { display: flex; align-items: center; gap: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
101
+ .jc-wf-service { color: var(--jc-muted); }
102
+ .jc-wf-toggle { width: 16px; border: 0; background: transparent; color: inherit; cursor: pointer; padding: 0; font-size: 11px; }
103
+ .jc-wf-icon { font-size: 14px; height: 14px; width: 14px; }
104
+ .jc-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; }
105
+ .jc-wf-track { position: relative; height: 18px; }
106
+ .jc-wf-bar { position: absolute; top: 4px; height: 10px; border-radius: 2px; opacity: .85; }
107
+ .jc-wf-error .jc-wf-bar { outline: 2px solid var(--jc-bad); }
108
+ .jc-wf-time { position: absolute; top: 2px; font-size: 10px; color: var(--jc-muted); transform: translateY(12px); }
109
+ .jc-span-detail { position: sticky; top: 8px; max-height: calc(100vh - 140px); overflow: auto; }
110
+ // The trace page fills the window and scrolls inside its panes (waterfall, span), never the whole page as well.
111
+ mat-sidenav-content > .jc-fill-page { box-sizing: border-box; height: 100%; display: flex; flex-direction: column; overflow: hidden; }
112
+ .jc-fill-page > .jc-trace { flex: 1; min-height: 0; grid-template-rows: minmax(0, 1fr); }
113
+ .jc-fill-page > .jc-trace > .jc-waterfall, .jc-fill-page > .jc-trace > .jc-span-detail { overflow: auto; max-height: none; position: static; }
114
+ .jc-fill-page .jc-wf-head { position: sticky; top: 0; z-index: 1; background: var(--jc-surface); }
115
+ .jc-span-facts { display: flex; gap: 12px; align-items: center; margin-bottom: 6px; }
116
+ .jc-event { border-left: 3px solid var(--jc-border); padding-left: 8px; margin: 6px 0; }
117
+ .jc-event.jc-wf-error { border-left-color: var(--jc-bad); }
118
+
119
+ // ─── Topology ───
120
+ .jc-topology { display: grid; grid-template-columns: minmax(0, 1fr) 340px; gap: 12px; }
121
+ .jc-graph { height: calc(100vh - 220px); min-height: 420px; background: var(--jc-surface); border: 1px solid var(--jc-border); border-radius: 8px; }
122
+ .jc-graph-legend { display: flex; gap: 16px; align-items: center; font-size: 12px; color: var(--jc-muted); padding: 4px; }
123
+ .jc-line { display: inline-block; width: 24px; border-top: 2px solid var(--jc-muted); vertical-align: middle; }
124
+ .jc-line.jc-dashed { border-top-style: dashed; }
125
+ .jc-side { align-self: start; }
126
+
127
+ @media (max-width: 900px) {
128
+ .jc-trace.jc-with-detail, .jc-topology { grid-template-columns: 1fr; }
129
+ // Stacked panes: the page scrolls as a whole again.
130
+ mat-sidenav-content > .jc-fill-page { height: auto; display: block; overflow: visible; }
131
+ .jc-fill-page > .jc-trace > .jc-waterfall, .jc-fill-page > .jc-trace > .jc-span-detail { overflow: visible; }
132
+ .jc-field-wide { min-width: 0; }
133
+ }