@juice-js/observability 1.1.0-alpha.20 → 1.1.0-alpha.22

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 (28) hide show
  1. package/README.md +1 -0
  2. package/fesm2022/{juice-js-observability-alert.page-nEy5Gc6j.mjs → juice-js-observability-alert.page-DOAX9WWa.mjs} +154 -2
  3. package/fesm2022/juice-js-observability-alert.page-DOAX9WWa.mjs.map +1 -0
  4. package/fesm2022/juice-js-observability-incident.page-CCHEWXou.mjs +400 -0
  5. package/fesm2022/juice-js-observability-incident.page-CCHEWXou.mjs.map +1 -0
  6. package/fesm2022/juice-js-observability-incidents.page-CpNqYp2W.mjs +438 -0
  7. package/fesm2022/juice-js-observability-incidents.page-CpNqYp2W.mjs.map +1 -0
  8. package/fesm2022/juice-js-observability-load-runs.page-C2rk2bz9.mjs +349 -0
  9. package/fesm2022/juice-js-observability-load-runs.page-C2rk2bz9.mjs.map +1 -0
  10. package/fesm2022/juice-js-observability-notifications.page-BDqb10C6.mjs +725 -0
  11. package/fesm2022/juice-js-observability-notifications.page-BDqb10C6.mjs.map +1 -0
  12. package/fesm2022/juice-js-observability-rules.page-Df2uDVwz.mjs +580 -0
  13. package/fesm2022/juice-js-observability-rules.page-Df2uDVwz.mjs.map +1 -0
  14. package/fesm2022/{juice-js-observability-service.page-rGAvUK_k.mjs → juice-js-observability-service.page-BFXUe4mX.mjs} +38 -4
  15. package/fesm2022/juice-js-observability-service.page-BFXUe4mX.mjs.map +1 -0
  16. package/fesm2022/juice-js-observability-silences.page-Cg53dJAy.mjs +523 -0
  17. package/fesm2022/juice-js-observability-silences.page-Cg53dJAy.mjs.map +1 -0
  18. package/fesm2022/juice-js-observability-timeline.page-CtU2EoDN.mjs +213 -0
  19. package/fesm2022/juice-js-observability-timeline.page-CtU2EoDN.mjs.map +1 -0
  20. package/fesm2022/juice-js-observability.mjs +141 -3
  21. package/fesm2022/juice-js-observability.mjs.map +1 -1
  22. package/package.json +1 -1
  23. package/styles/_observability.scss +6 -0
  24. package/types/juice-js-observability.d.ts +242 -0
  25. package/fesm2022/juice-js-observability-alert.page-nEy5Gc6j.mjs.map +0 -1
  26. package/fesm2022/juice-js-observability-rules.page-Crb07uWm.mjs +0 -175
  27. package/fesm2022/juice-js-observability-rules.page-Crb07uWm.mjs.map +0 -1
  28. package/fesm2022/juice-js-observability-service.page-rGAvUK_k.mjs.map +0 -1
package/README.md CHANGED
@@ -62,6 +62,7 @@ like juice-layout's dark-mode toggle) repaints the charts and the graph.
62
62
  - The trace page's resource usage and a service's Capacity tab read .NET's `System.Runtime` meter (.NET 9+): add
63
63
  `AddMeter("System.Runtime")` to the host's `WithMetrics`. If traces are sampled, also add `AddAspNetCoreInstrumentation()`
64
64
  there: the Capacity tab then counts requests from `http.server.request.duration`, not from spans, which undercount.
65
+ - The Load runs page reads windows a load test marks through the API: see "Load test runs" in the platform's README.
65
66
  - The router needs `bindToComponentInputs: true`. The pages take their route and query parameters as inputs.
66
67
  - The host's bearer-token interceptor (`@juice-js/auth`) must cover `apiBase`. The token carries the `platform_tenant`
67
68
  claim and a platform role (`viewer`, `operator` or `admin`).
@@ -3,7 +3,7 @@ import { S as apiResource, d as HealthBadge, f as KeyValueTable, h as AgoPipe, o
3
3
  import * as i0 from "@angular/core";
4
4
  import { ChangeDetectionStrategy, Component, computed, inject, input, signal } from "@angular/core";
5
5
  import { RouterLink } from "@angular/router";
6
- import { DatePipe } from "@angular/common";
6
+ import { DatePipe, LowerCasePipe } from "@angular/common";
7
7
  import * as i1 from "@angular/forms";
8
8
  import { FormsModule } from "@angular/forms";
9
9
  import * as i2 from "@angular/material/button";
@@ -26,6 +26,41 @@ var AlertPage = class AlertPage {
26
26
  stream: ({ params }) => this.api.alert(params)
27
27
  });
28
28
  error = computed(() => problemText(this.detail.error()), ...ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []);
29
+ related = apiResource({
30
+ params: () => this.id(),
31
+ stream: ({ params }) => this.api.relatedToAlert(params)
32
+ });
33
+ relatedError = computed(() => problemText(this.related.error()), ...ngDevMode ? [{ debugName: "relatedError" }] : /* istanbul ignore next */ []);
34
+ notifications = apiResource({
35
+ params: () => this.id(),
36
+ stream: ({ params }) => this.api.alertNotifications(params)
37
+ });
38
+ notificationsError = computed(() => problemText(this.notifications.error()), ...ngDevMode ? [{ debugName: "notificationsError" }] : /* istanbul ignore next */ []);
39
+ incidentLink(alert, rule, subject) {
40
+ return this.ctx.keep({
41
+ alert,
42
+ title: `${rule} on ${subject}`
43
+ });
44
+ }
45
+ silenceLink(subject, environment, type) {
46
+ const name = type === "service" ? subject.replace(/^service:/, "").replace(/@[^@]*$/, "") : null;
47
+ const host = type === "host" ? subject.replace(/^host:/, "") : null;
48
+ return this.ctx.keep({
49
+ service: name,
50
+ environment,
51
+ host
52
+ });
53
+ }
54
+ timelineLink(service, environment, breach) {
55
+ const at = new Date(breach).getTime();
56
+ return this.ctx.keep({
57
+ service,
58
+ env: environment,
59
+ range: null,
60
+ from: (/* @__PURE__ */ new Date(at - 72e5)).toISOString(),
61
+ to: new Date(at + 36e5).toISOString()
62
+ });
63
+ }
29
64
  investigate(environment, extra = {}) {
30
65
  const alert = this.detail.value()?.alert;
31
66
  const start = alert?.breachStartedAt ? new Date(alert.breachStartedAt).getTime() - 9e5 : null;
@@ -48,6 +83,7 @@ var AlertPage = class AlertPage {
48
83
  next: () => {
49
84
  this.sending.set(false);
50
85
  this.detail.reload();
86
+ this.notifications.reload();
51
87
  },
52
88
  error: (e) => {
53
89
  this.sending.set(false);
@@ -95,7 +131,19 @@ var AlertPage = class AlertPage {
95
131
  <a mat-stroked-button [routerLink]="['services', d.alert.subjectId] | jcLink" [queryParams]="investigate(d.alert.environment, { tab: 'traces', status: 'error' })">Error traces</a>
96
132
  }
97
133
  @for (l of d.links; track l.url) { <a mat-button [href]="l.url" target="_blank" rel="noopener">{{ l.title }} ↗</a> }
134
+ @if (d.alert.state !== 'Resolved') {
135
+ <a mat-stroked-button data-testid="alert-open-incident" [routerLink]="['incidents'] | jcLink" [queryParams]="incidentLink(d.alert.id, d.alert.rule, d.alert.subjectKey)">Open an incident</a>
136
+ <a mat-stroked-button data-testid="alert-silence" [routerLink]="['silences'] | jcLink" [queryParams]="silenceLink(d.alert.subjectKey, d.alert.environment, d.alert.subjectType)">Silence this</a>
137
+ }
98
138
  </div>
139
+ @if (notifications.value(); as n) {
140
+ @for (s of n.silences; track s.id) {
141
+ <p class="jc-notice" data-testid="alert-silenced">Notifications are held until {{ s.endsAt | date: 'MMM d, HH:mm' }}: “{{ s.comment }}” ({{ s.createdBy }}). The alert itself still fires and resolves.</p>
142
+ }
143
+ @for (i of n.incidents; track i.id) {
144
+ <p>In incident <a [routerLink]="['incidents', i.id] | jcLink" [queryParams]="ctx.keep()">{{ i.title }}</a> ({{ i.state }})</p>
145
+ }
146
+ }
99
147
  <div class="jc-grid-2">
100
148
  <section class="jc-card"><h2>Evidence</h2><jc-kv [values]="d.evidence" /></section>
101
149
  <section class="jc-card">
@@ -106,6 +154,49 @@ var AlertPage = class AlertPage {
106
154
  } @empty { <p class="jc-muted">No health for this subject.</p> }
107
155
  </section>
108
156
  </div>
157
+ <section class="jc-card" data-testid="alert-related">
158
+ <h2>Around this alert</h2>
159
+ @if (related.value(); as r) {
160
+ <p class="jc-notice" data-testid="alert-related-note">{{ r.note }}</p>
161
+ <table class="jc-table"><tbody>
162
+ @for (i of r.items; track i.type + i.id) {
163
+ <tr>
164
+ <td class="jc-nowrap">{{ i.at | date: 'MMM d, HH:mm:ss' }}</td>
165
+ <td><strong>{{ i.title }}</strong>@if (i.detail) { <div class="jc-muted jc-small">{{ i.detail }}</div> }</td>
166
+ <td class="jc-small"><ul class="jc-reasons">@for (x of i.reasons; track x.code) { <li>{{ x.text }} <span class="jc-muted">(+{{ x.points }})</span></li> }</ul></td>
167
+ </tr>
168
+ } @empty { <tr><td class="jc-muted">No changes or dependency health changes in the 2 hours before this alert.</td></tr> }
169
+ </tbody></table>
170
+ @if (d.alert.subjectId && d.alert.subjectType === 'service') {
171
+ <a mat-stroked-button [routerLink]="['timeline'] | jcLink" [queryParams]="timelineLink(d.alert.subjectId, d.alert.environment, r.breachStartedAt)">Open the timeline</a>
172
+ }
173
+ }
174
+ @if (related.error()) { <p class="jc-error">{{ relatedError() }}</p> }
175
+ </section>
176
+ <section class="jc-card" data-testid="alert-notifications">
177
+ <h2>Notifications</h2>
178
+ @if (notifications.value(); as n) {
179
+ @for (e of n.escalations; track e.policy) {
180
+ <p>Escalation <strong>{{ e.name }}</strong>: {{ e.stepsSent }} of {{ e.steps }} steps sent, {{ e.state | lowercase }}@if (e.stopReason) { ({{ e.stopReason | reason }}) }@if (e.nextAt) {, next at {{ e.nextAt | date: 'HH:mm:ss' }} }</p>
181
+ }
182
+ <table class="jc-table">
183
+ <thead><tr><th>To</th><th>Event</th><th>Status</th><th>Attempts</th><th>Detail</th><th>When</th></tr></thead>
184
+ <tbody>
185
+ @for (x of n.deliveries; track x.delivery.id) {
186
+ <tr>
187
+ <td>{{ x.endpointKey }} <span class="jc-muted jc-small">{{ x.kind }}</span></td>
188
+ <td>{{ x.delivery.event }}@if (x.delivery.escalationStep) { step {{ x.delivery.escalationStep }} }</td>
189
+ <td [class.jc-bad]="x.delivery.status === 'Failed'">{{ x.delivery.status }}</td>
190
+ <td>{{ x.delivery.attempts }}</td>
191
+ <td class="jc-small">{{ x.delivery.lastError }}@if (x.delivery.nextAttemptAt && x.delivery.status === 'Retrying') { · next {{ x.delivery.nextAttemptAt | date: 'HH:mm:ss' }} }</td>
192
+ <td class="jc-nowrap">{{ x.delivery.createdAt | date: 'MMM d, HH:mm:ss' }}</td>
193
+ </tr>
194
+ } @empty { <tr><td colspan="6" class="jc-muted">Nothing sent about this alert: no channel takes it.</td></tr> }
195
+ </tbody>
196
+ </table>
197
+ }
198
+ @if (notifications.error()) { <p class="jc-error">{{ notificationsError() }}</p> }
199
+ </section>
109
200
  <section class="jc-card">
110
201
  <h2>History</h2>
111
202
  <table class="jc-table"><tbody>
@@ -300,6 +391,11 @@ var AlertPage = class AlertPage {
300
391
  type: DatePipe,
301
392
  name: "date"
302
393
  },
394
+ {
395
+ kind: "pipe",
396
+ type: LowerCasePipe,
397
+ name: "lowercase"
398
+ },
303
399
  {
304
400
  kind: "pipe",
305
401
  type: AgoPipe,
@@ -327,6 +423,7 @@ i0.ɵɵngDeclareClassMetadata({
327
423
  LinkPipe,
328
424
  RouterLink,
329
425
  DatePipe,
426
+ LowerCasePipe,
330
427
  FormsModule,
331
428
  MatButtonModule,
332
429
  MatFormFieldModule,
@@ -356,7 +453,19 @@ i0.ɵɵngDeclareClassMetadata({
356
453
  <a mat-stroked-button [routerLink]="['services', d.alert.subjectId] | jcLink" [queryParams]="investigate(d.alert.environment, { tab: 'traces', status: 'error' })">Error traces</a>
357
454
  }
358
455
  @for (l of d.links; track l.url) { <a mat-button [href]="l.url" target="_blank" rel="noopener">{{ l.title }} ↗</a> }
456
+ @if (d.alert.state !== 'Resolved') {
457
+ <a mat-stroked-button data-testid="alert-open-incident" [routerLink]="['incidents'] | jcLink" [queryParams]="incidentLink(d.alert.id, d.alert.rule, d.alert.subjectKey)">Open an incident</a>
458
+ <a mat-stroked-button data-testid="alert-silence" [routerLink]="['silences'] | jcLink" [queryParams]="silenceLink(d.alert.subjectKey, d.alert.environment, d.alert.subjectType)">Silence this</a>
459
+ }
359
460
  </div>
461
+ @if (notifications.value(); as n) {
462
+ @for (s of n.silences; track s.id) {
463
+ <p class="jc-notice" data-testid="alert-silenced">Notifications are held until {{ s.endsAt | date: 'MMM d, HH:mm' }}: “{{ s.comment }}” ({{ s.createdBy }}). The alert itself still fires and resolves.</p>
464
+ }
465
+ @for (i of n.incidents; track i.id) {
466
+ <p>In incident <a [routerLink]="['incidents', i.id] | jcLink" [queryParams]="ctx.keep()">{{ i.title }}</a> ({{ i.state }})</p>
467
+ }
468
+ }
360
469
  <div class="jc-grid-2">
361
470
  <section class="jc-card"><h2>Evidence</h2><jc-kv [values]="d.evidence" /></section>
362
471
  <section class="jc-card">
@@ -367,6 +476,49 @@ i0.ɵɵngDeclareClassMetadata({
367
476
  } @empty { <p class="jc-muted">No health for this subject.</p> }
368
477
  </section>
369
478
  </div>
479
+ <section class="jc-card" data-testid="alert-related">
480
+ <h2>Around this alert</h2>
481
+ @if (related.value(); as r) {
482
+ <p class="jc-notice" data-testid="alert-related-note">{{ r.note }}</p>
483
+ <table class="jc-table"><tbody>
484
+ @for (i of r.items; track i.type + i.id) {
485
+ <tr>
486
+ <td class="jc-nowrap">{{ i.at | date: 'MMM d, HH:mm:ss' }}</td>
487
+ <td><strong>{{ i.title }}</strong>@if (i.detail) { <div class="jc-muted jc-small">{{ i.detail }}</div> }</td>
488
+ <td class="jc-small"><ul class="jc-reasons">@for (x of i.reasons; track x.code) { <li>{{ x.text }} <span class="jc-muted">(+{{ x.points }})</span></li> }</ul></td>
489
+ </tr>
490
+ } @empty { <tr><td class="jc-muted">No changes or dependency health changes in the 2 hours before this alert.</td></tr> }
491
+ </tbody></table>
492
+ @if (d.alert.subjectId && d.alert.subjectType === 'service') {
493
+ <a mat-stroked-button [routerLink]="['timeline'] | jcLink" [queryParams]="timelineLink(d.alert.subjectId, d.alert.environment, r.breachStartedAt)">Open the timeline</a>
494
+ }
495
+ }
496
+ @if (related.error()) { <p class="jc-error">{{ relatedError() }}</p> }
497
+ </section>
498
+ <section class="jc-card" data-testid="alert-notifications">
499
+ <h2>Notifications</h2>
500
+ @if (notifications.value(); as n) {
501
+ @for (e of n.escalations; track e.policy) {
502
+ <p>Escalation <strong>{{ e.name }}</strong>: {{ e.stepsSent }} of {{ e.steps }} steps sent, {{ e.state | lowercase }}@if (e.stopReason) { ({{ e.stopReason | reason }}) }@if (e.nextAt) {, next at {{ e.nextAt | date: 'HH:mm:ss' }} }</p>
503
+ }
504
+ <table class="jc-table">
505
+ <thead><tr><th>To</th><th>Event</th><th>Status</th><th>Attempts</th><th>Detail</th><th>When</th></tr></thead>
506
+ <tbody>
507
+ @for (x of n.deliveries; track x.delivery.id) {
508
+ <tr>
509
+ <td>{{ x.endpointKey }} <span class="jc-muted jc-small">{{ x.kind }}</span></td>
510
+ <td>{{ x.delivery.event }}@if (x.delivery.escalationStep) { step {{ x.delivery.escalationStep }} }</td>
511
+ <td [class.jc-bad]="x.delivery.status === 'Failed'">{{ x.delivery.status }}</td>
512
+ <td>{{ x.delivery.attempts }}</td>
513
+ <td class="jc-small">{{ x.delivery.lastError }}@if (x.delivery.nextAttemptAt && x.delivery.status === 'Retrying') { · next {{ x.delivery.nextAttemptAt | date: 'HH:mm:ss' }} }</td>
514
+ <td class="jc-nowrap">{{ x.delivery.createdAt | date: 'MMM d, HH:mm:ss' }}</td>
515
+ </tr>
516
+ } @empty { <tr><td colspan="6" class="jc-muted">Nothing sent about this alert: no channel takes it.</td></tr> }
517
+ </tbody>
518
+ </table>
519
+ }
520
+ @if (notifications.error()) { <p class="jc-error">{{ notificationsError() }}</p> }
521
+ </section>
370
522
  <section class="jc-card">
371
523
  <h2>History</h2>
372
524
  <table class="jc-table"><tbody>
@@ -401,4 +553,4 @@ i0.ɵɵngDeclareClassMetadata({
401
553
  });
402
554
  export { AlertPage };
403
555
 
404
- //# sourceMappingURL=juice-js-observability-alert.page-nEy5Gc6j.mjs.map
556
+ //# sourceMappingURL=juice-js-observability-alert.page-DOAX9WWa.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"juice-js-observability-alert.page-DOAX9WWa.mjs","names":[],"sources":["../../../../projects/juice-js/observability/src/lib/pages/alert.page.ts"],"sourcesContent":["import { LinkPipe } from '../core/options';\nimport { ChangeDetectionStrategy, Component, computed, inject, input, signal } from '@angular/core';\nimport { apiResource } from '../shared/resource';\nimport { DatePipe, LowerCasePipe } from '@angular/common';\nimport { FormsModule } from '@angular/forms';\nimport { MatButtonModule } from '@angular/material/button';\nimport { MatFormFieldModule } from '@angular/material/form-field';\nimport { MatInputModule } from '@angular/material/input';\nimport { MatProgressBarModule } from '@angular/material/progress-bar';\nimport { RouterLink } from '@angular/router';\nimport { ApiService } from '../core/api.service';\nimport { ContextService } from '../core/context.service';\nimport { AgoPipe, ReasonPipe } from '../shared/format';\nimport { ContextBar, HealthBadge, KeyValueTable } from '../shared/ui';\nimport { problemText } from '../shared/telemetry';\n\n/**\n * One alert (Console requirements §14): evidence, lifecycle history, the subject's health and reasons, the way into\n * the investigation, and acknowledgement for operators.\n */\n@Component({\n changeDetection: ChangeDetectionStrategy.OnPush,\n imports: [LinkPipe, RouterLink, DatePipe, LowerCasePipe, FormsModule, MatButtonModule, MatFormFieldModule, MatInputModule, MatProgressBarModule,\n ContextBar, HealthBadge, KeyValueTable, AgoPipe, ReasonPipe],\n template: `\n <jc-context-bar>\n <a [routerLink]=\"['alerts'] | jcLink\" [queryParams]=\"ctx.keep()\" class=\"jc-crumb\">Alerts</a><span class=\"jc-crumb-sep\">›</span>\n <h1>{{ detail.value()?.alert?.rule ?? '…' }}</h1>\n </jc-context-bar>\n @if (detail.isLoading()) { <mat-progress-bar mode=\"indeterminate\" /> }\n @if (detail.error()) { <p class=\"jc-error\">{{ error() }}</p> }\n @if (detail.value(); as d) {\n <div class=\"jc-summary\">\n <span class=\"jc-badge\" [class.jc-health-unhealthy]=\"d.alert.severity === 'Critical'\" [class.jc-health-degraded]=\"d.alert.severity === 'Warning'\">{{ d.alert.severity }}</span>\n <strong>{{ d.alert.state }}</strong>\n <span>{{ d.alert.subjectKey }}</span>\n @if (d.alert.noData) { <span class=\"jc-muted\">no data at the last evaluation: state kept</span> }\n @if (d.alert.subjectType === 'service' && d.alert.subjectId) {\n <a mat-flat-button [routerLink]=\"['services', d.alert.subjectId] | jcLink\" [queryParams]=\"investigate(d.alert.environment)\">Investigate</a>\n <a mat-stroked-button [routerLink]=\"['services', d.alert.subjectId] | jcLink\" [queryParams]=\"investigate(d.alert.environment, { tab: 'traces', status: 'error' })\">Error traces</a>\n }\n @for (l of d.links; track l.url) { <a mat-button [href]=\"l.url\" target=\"_blank\" rel=\"noopener\">{{ l.title }} ↗</a> }\n @if (d.alert.state !== 'Resolved') {\n <a mat-stroked-button data-testid=\"alert-open-incident\" [routerLink]=\"['incidents'] | jcLink\" [queryParams]=\"incidentLink(d.alert.id, d.alert.rule, d.alert.subjectKey)\">Open an incident</a>\n <a mat-stroked-button data-testid=\"alert-silence\" [routerLink]=\"['silences'] | jcLink\" [queryParams]=\"silenceLink(d.alert.subjectKey, d.alert.environment, d.alert.subjectType)\">Silence this</a>\n }\n </div>\n @if (notifications.value(); as n) {\n @for (s of n.silences; track s.id) {\n <p class=\"jc-notice\" data-testid=\"alert-silenced\">Notifications are held until {{ s.endsAt | date: 'MMM d, HH:mm' }}: “{{ s.comment }}” ({{ s.createdBy }}). The alert itself still fires and resolves.</p>\n }\n @for (i of n.incidents; track i.id) {\n <p>In incident <a [routerLink]=\"['incidents', i.id] | jcLink\" [queryParams]=\"ctx.keep()\">{{ i.title }}</a> ({{ i.state }})</p>\n }\n }\n <div class=\"jc-grid-2\">\n <section class=\"jc-card\"><h2>Evidence</h2><jc-kv [values]=\"d.evidence\" /></section>\n <section class=\"jc-card\">\n <h2>Subject health</h2>\n @for (h of d.subjectHealth; track h.subjectId + h.environment) {\n <p><jc-health [state]=\"h.state\" [stale]=\"h.stale\" /> {{ h.environment }}</p>\n <ul class=\"jc-reasons\">@for (r of h.reasons; track r) { <li>{{ r | reason }}</li> }</ul>\n } @empty { <p class=\"jc-muted\">No health for this subject.</p> }\n </section>\n </div>\n <section class=\"jc-card\" data-testid=\"alert-related\">\n <h2>Around this alert</h2>\n @if (related.value(); as r) {\n <p class=\"jc-notice\" data-testid=\"alert-related-note\">{{ r.note }}</p>\n <table class=\"jc-table\"><tbody>\n @for (i of r.items; track i.type + i.id) {\n <tr>\n <td class=\"jc-nowrap\">{{ i.at | date: 'MMM d, HH:mm:ss' }}</td>\n <td><strong>{{ i.title }}</strong>@if (i.detail) { <div class=\"jc-muted jc-small\">{{ i.detail }}</div> }</td>\n <td class=\"jc-small\"><ul class=\"jc-reasons\">@for (x of i.reasons; track x.code) { <li>{{ x.text }} <span class=\"jc-muted\">(+{{ x.points }})</span></li> }</ul></td>\n </tr>\n } @empty { <tr><td class=\"jc-muted\">No changes or dependency health changes in the 2 hours before this alert.</td></tr> }\n </tbody></table>\n @if (d.alert.subjectId && d.alert.subjectType === 'service') {\n <a mat-stroked-button [routerLink]=\"['timeline'] | jcLink\" [queryParams]=\"timelineLink(d.alert.subjectId, d.alert.environment, r.breachStartedAt)\">Open the timeline</a>\n }\n }\n @if (related.error()) { <p class=\"jc-error\">{{ relatedError() }}</p> }\n </section>\n <section class=\"jc-card\" data-testid=\"alert-notifications\">\n <h2>Notifications</h2>\n @if (notifications.value(); as n) {\n @for (e of n.escalations; track e.policy) {\n <p>Escalation <strong>{{ e.name }}</strong>: {{ e.stepsSent }} of {{ e.steps }} steps sent, {{ e.state | lowercase }}@if (e.stopReason) { ({{ e.stopReason | reason }}) }@if (e.nextAt) {, next at {{ e.nextAt | date: 'HH:mm:ss' }} }</p>\n }\n <table class=\"jc-table\">\n <thead><tr><th>To</th><th>Event</th><th>Status</th><th>Attempts</th><th>Detail</th><th>When</th></tr></thead>\n <tbody>\n @for (x of n.deliveries; track x.delivery.id) {\n <tr>\n <td>{{ x.endpointKey }} <span class=\"jc-muted jc-small\">{{ x.kind }}</span></td>\n <td>{{ x.delivery.event }}@if (x.delivery.escalationStep) { step {{ x.delivery.escalationStep }} }</td>\n <td [class.jc-bad]=\"x.delivery.status === 'Failed'\">{{ x.delivery.status }}</td>\n <td>{{ x.delivery.attempts }}</td>\n <td class=\"jc-small\">{{ x.delivery.lastError }}@if (x.delivery.nextAttemptAt && x.delivery.status === 'Retrying') { · next {{ x.delivery.nextAttemptAt | date: 'HH:mm:ss' }} }</td>\n <td class=\"jc-nowrap\">{{ x.delivery.createdAt | date: 'MMM d, HH:mm:ss' }}</td>\n </tr>\n } @empty { <tr><td colspan=\"6\" class=\"jc-muted\">Nothing sent about this alert: no channel takes it.</td></tr> }\n </tbody>\n </table>\n }\n @if (notifications.error()) { <p class=\"jc-error\">{{ notificationsError() }}</p> }\n </section>\n <section class=\"jc-card\">\n <h2>History</h2>\n <table class=\"jc-table\"><tbody>\n @for (t of d.history; track t.at + t.to) {\n <tr><td>{{ t.at | date: 'MMM d, HH:mm:ss' }}</td><td>{{ t.from ?? '—' }} → <strong>{{ t.to }}</strong></td><td>{{ t.reason | reason }}</td><td>{{ t.value ?? '—' }}</td></tr>\n }\n </tbody></table>\n </section>\n <section class=\"jc-card\">\n <h2>Acknowledgement</h2>\n @if (d.alert.acknowledgedAt) {\n <p>By <strong>{{ d.alert.acknowledgedBy }}</strong> {{ d.alert.acknowledgedAt | ago }}@if (d.acknowledgeComment) {: “{{ d.acknowledgeComment }}”}</p>\n } @else if (d.alert.state !== 'Resolved') {\n <form (ngSubmit)=\"acknowledge()\" class=\"jc-filter-form\">\n <mat-form-field subscriptSizing=\"dynamic\" class=\"jc-field-wide\"><mat-label>Comment</mat-label><input matInput name=\"comment\" [(ngModel)]=\"comment\" maxlength=\"1000\"></mat-form-field>\n <button mat-flat-button type=\"submit\" [disabled]=\"sending()\">Acknowledge</button>\n </form>\n @if (ackError()) { <p class=\"jc-error\">{{ ackError() }}</p> }\n } @else { <p class=\"jc-muted\">Resolved without acknowledgement.</p> }\n </section>\n }`,\n})\nexport class AlertPage {\n protected readonly ctx = inject(ContextService);\n private readonly api = inject(ApiService);\n readonly id = input.required<string>();\n protected comment = '';\n protected readonly sending = signal(false);\n protected readonly ackError = signal('');\n\n protected readonly detail = apiResource({ params: () => this.id(), stream: ({ params }) => this.api.alert(params) });\n protected readonly error = computed(() => problemText(this.detail.error()));\n protected readonly related = apiResource({ params: () => this.id(), stream: ({ params }) => this.api.relatedToAlert(params) });\n protected readonly relatedError = computed(() => problemText(this.related.error()));\n protected readonly notifications = apiResource({ params: () => this.id(), stream: ({ params }) => this.api.alertNotifications(params) });\n protected readonly notificationsError = computed(() => problemText(this.notifications.error()));\n\n incidentLink(alert: string, rule: string, subject: string) { return this.ctx.keep({ alert, title: `${rule} on ${subject}` }); }\n\n /** The new-silence form, filled in for this alert's subject. */\n silenceLink(subject: string, environment: string | null, type: string) {\n const name = type === 'service' ? subject.replace(/^service:/, '').replace(/@[^@]*$/, '') : null;\n const host = type === 'host' ? subject.replace(/^host:/, '') : null;\n return this.ctx.keep({ service: name, environment, host });\n }\n\n /** The service page over the alert's environment, from shortly before the breach. */\n /** The service's timeline from 2 hours before the breach to 1 hour after. */\n timelineLink(service: string, environment: string | null, breach: string) {\n const at = new Date(breach).getTime();\n return this.ctx.keep({ service, env: environment, range: null, from: new Date(at - 2 * 3_600_000).toISOString(), to: new Date(at + 3_600_000).toISOString() });\n }\n\n investigate(environment: string | null, extra: Record<string, string> = {}) {\n const alert = this.detail.value()?.alert;\n const start = alert?.breachStartedAt ? new Date(alert.breachStartedAt).getTime() - 15 * 60_000 : null;\n const end = alert?.resolvedAt ? new Date(alert.resolvedAt).getTime() + 5 * 60_000 : Date.now();\n const window = start && end - start <= 24 * 3_600_000\n ? { from: new Date(start).toISOString(), to: new Date(end).toISOString(), range: null } : {};\n return this.ctx.keep({ env: environment, ...window, ...extra });\n }\n\n acknowledge() {\n this.sending.set(true);\n this.ackError.set('');\n this.api.acknowledge(this.id(), this.comment).subscribe({\n next: () => { this.sending.set(false); this.detail.reload(); this.notifications.reload(); },\n error: e => {\n this.sending.set(false);\n this.ackError.set((e as { status?: number }).status === 403 ? 'Acknowledging needs the operator or admin role.' : problemText(e));\n },\n });\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAkIA,IAAa,YAAb,MAAa,UAAS;CACD,MAAM,OAAO,cAAc;CAC7B,MAAM,OAAO,UAAU;CAC/B,KAAK,MAAM,SAAkB,GAAA,YAAA,CAAA,EAAA,WAAA,KAAA,CAAA,+BAAA,CAAA,CAAA;CAC5B,UAAU;CACD,UAAU,OAAO,OAAM,GAAA,YAAA,CAAA,EAAA,WAAA,UAAA,CAAA,+BAAA,CAAA,CAAA;CACvB,WAAW,OAAO,IAAG,GAAA,YAAA,CAAA,EAAA,WAAA,WAAA,CAAA,+BAAA,CAAA,CAAA;CAErB,SAAS,YAAY;EAAE,cAAc,KAAK,GAAE;EAAI,SAAS,EAAE,aAAa,KAAK,IAAI,MAAM,MAAM;CAAC,CAAE;CAChG,QAAQ,eAAe,YAAY,KAAK,OAAO,MAAK,CAAE,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,QAAA,CAAA,+BAAA,CAAA,CAAA;CACxD,UAAU,YAAY;EAAE,cAAc,KAAK,GAAE;EAAI,SAAS,EAAE,aAAa,KAAK,IAAI,eAAe,MAAM;CAAC,CAAE;CAC1G,eAAe,eAAe,YAAY,KAAK,QAAQ,MAAK,CAAE,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,eAAA,CAAA,+BAAA,CAAA,CAAA;CAChE,gBAAgB,YAAY;EAAE,cAAc,KAAK,GAAE;EAAI,SAAS,EAAE,aAAa,KAAK,IAAI,mBAAmB,MAAM;CAAC,CAAE;CACpH,qBAAqB,eAAe,YAAY,KAAK,cAAc,MAAK,CAAE,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,qBAAA,CAAA,+BAAA,CAAA,CAAA;CAE/F,aAAa,OAAe,MAAc,SAAe;EAAI,OAAO,KAAK,IAAI,KAAK;GAAE;GAAO,OAAO,GAAG,KAAI,MAAO;EAAS,CAAE;CAAG;CAG9H,YAAY,SAAiB,aAA4B,MAAY;EACnE,MAAM,OAAO,SAAS,YAAY,QAAQ,QAAQ,aAAa,EAAE,CAAC,CAAC,QAAQ,WAAW,EAAE,IAAI;EAC5F,MAAM,OAAO,SAAS,SAAS,QAAQ,QAAQ,UAAU,EAAE,IAAI;EAC/D,OAAO,KAAK,IAAI,KAAK;GAAE,SAAS;GAAM;GAAa;EAAI,CAAE;CAC3D;CAIA,aAAa,SAAiB,aAA4B,QAAc;EACtE,MAAM,KAAK,IAAI,KAAK,MAAM,CAAC,CAAC,QAAO;EACnC,OAAO,KAAK,IAAI,KAAK;GAAE;GAAS,KAAK;GAAa,OAAO;GAAM,uBAAM,IAAI,KAAK,KAAK,IAAa,EAAC,CAAC,YAAW;GAAI,IAAI,IAAI,KAAK,KAAK,IAAS,CAAC,CAAC,YAAW;EAAE,CAAE;CAC/J;CAEA,YAAY,aAA4B,QAAgC,CAAA,GAAE;EACxE,MAAM,QAAQ,KAAK,OAAO,MAAK,CAAE,EAAE;EACnC,MAAM,QAAQ,OAAO,kBAAkB,IAAI,KAAK,MAAM,eAAe,CAAC,CAAC,QAAO,IAAK,MAAc;EACjG,MAAM,MAAM,OAAO,aAAa,IAAI,KAAK,MAAM,UAAU,CAAC,CAAC,QAAO,IAAK,MAAa,KAAK,IAAG;EAC5F,MAAM,SAAS,SAAS,MAAM,SAAS,QACnC;GAAE,MAAM,IAAI,KAAK,KAAK,CAAC,CAAC,YAAW;GAAI,IAAI,IAAI,KAAK,GAAG,CAAC,CAAC,YAAW;GAAI,OAAO;EAAI,IAAK,CAAA;EAC5F,OAAO,KAAK,IAAI,KAAK;GAAE,KAAK;GAAa,GAAG;GAAQ,GAAG;EAAK,CAAE;CAChE;CAEA,cAAW;EACT,KAAK,QAAQ,IAAI,IAAI;EACrB,KAAK,SAAS,IAAI,EAAE;EACpB,KAAK,IAAI,YAAY,KAAK,GAAE,GAAI,KAAK,OAAO,CAAC,CAAC,UAAU;GACtD,YAAW;IAAG,KAAK,QAAQ,IAAI,KAAK;IAAG,KAAK,OAAO,OAAM;IAAI,KAAK,cAAc,OAAM;GAAI;GAC1F,QAAO,MAAI;IACT,KAAK,QAAQ,IAAI,KAAK;IACtB,KAAK,SAAS,IAAK,EAA0B,WAAW,MAAM,oDAAoD,YAAY,CAAC,CAAC;GAClI;EACD,CAAA;CACH;CAlDW,OAAA,OAAA,GAAA,mBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAAS,MAAA,CAAA;EAAA,QAAA,GAAA,gBAAA;CAAA,CAAA;CAAT,OAAA,OAAA,GAAA,qBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,MAAA;EAAS,cAAA;EAAA,UAAA;EAAA,QAAA,EAAA,IAAA;GAAA,mBAAA;GAAA,YAAA;GAAA,UAAA;GAAA,YAAA;GAAA,mBAAA;EAAA,EAAA;EAAA,UAAA;EAAA,UA1GV;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAwGN,UAAA;EAAA,cAAA;GAAA;IAAA,MAAA;IAAA,MA1GgB;IAAU,UAAA;IAAA,QAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;IAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAA2B;GAAW;GAAA;IAAA,MAAA;IAAA,MAAA,GAAA;IAAA,UAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAA,GAAA;IAAA,UAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAA,GAAA;IAAA,UAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAA,GAAA;IAAA,UAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAA,GAAA;IAAA,UAAA;IAAA,QAAA,CAAA,WAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAA,GAAA;IAAA,UAAA;IAAA,QAAA;KAAA;KAAA;KAAA;KAAA;IAAA;IAAA,SAAA,CAAA,eAAA;IAAA,UAAA,CAAA,SAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAA,GAAA;IAAA,UAAA;IAAA,QAAA,CAAA,eAAA;IAAA,SAAA,CAAA,UAAA;IAAA,UAAA,CAAA,QAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAE;GAAe;GAAA;IAAA,MAAA;IAAA,MAAA,GAAA;IAAA,UAAA;IAAA,QAAA,CAAA,WAAA;IAAA,UAAA,CAAA,aAAA,WAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAE;GAAkB;GAAA;IAAA,MAAA;IAAA,MAAA,GAAA;IAAA,UAAA;IAAA,QAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;IAAA;IAAA,UAAA,CAAA,cAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAA,GAAA;IAAA,UAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAE;GAAc;GAAA;IAAA,MAAA;IAAA,MAAA,GAAA;IAAA,UAAA;IAAA,QAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;IAAA;IAAA,UAAA,CAAA,UAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAE;GAAoB;GAAA;IAAA,MAAA;IAAA,MAAA,GAAA;IAAA,UAAA;IAAA,QAAA;KAAA;KAAA;KAAA;KAAA;IAAA;IAAA,SAAA,CAAA,cAAA;IAAA,UAAA,CAAA,gBAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAC7I;IAAU,UAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAE;IAAW,UAAA;IAAA,QAAA,CAAA,SAAA,OAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAE;IAAa,UAAA;IAAA,QAAA,CAAA,UAAA,OAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAD9B;IAAQ,MAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAc;IAAQ,MAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAE;IAAa,MAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MACb;IAAO,MAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAE;IAAU,MAAA;GAAA;EAAA;EAAA,iBAAA,GAAA,wBAAA;CAAA,CAAA;;AA2GlD,GAAA,yBAAA;CAAA,YAAA;CAAA,SAAA;CAAA,UAAA;CAAA,MAAA;CAAS,YAAA,CAAA;EA9GrB,MAAA;EAAU,MAAA,CAAA;GACT,iBAAiB,wBAAwB;GACzC,SAAS;IAAC;IAAU;IAAY;IAAU;IAAe;IAAa;IAAiB;IAAoB;IAAgB;IACzH;IAAY;IAAa;IAAe;IAAS;GAAU;GAC7D,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAyGX,CAAA"}