@trismegisto/spine 3.3.0 → 3.3.2

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 (92) hide show
  1. package/README.md +19 -11
  2. package/fesm2022/trismegisto-spine-audit.mjs +16 -12
  3. package/fesm2022/trismegisto-spine-audit.mjs.map +1 -1
  4. package/fesm2022/{trismegisto-spine-auto-service-auto-service-audit.component-tfGcVR3i.mjs → trismegisto-spine-auto-service-auto-service-audit.component-D8HUpZtA.mjs} +4 -4
  5. package/fesm2022/{trismegisto-spine-auto-service-auto-service-audit.component-tfGcVR3i.mjs.map → trismegisto-spine-auto-service-auto-service-audit.component-D8HUpZtA.mjs.map} +1 -1
  6. package/fesm2022/trismegisto-spine-auto-service-auto-service-client-picker.component-NawbfdLW.mjs +65 -0
  7. package/fesm2022/trismegisto-spine-auto-service-auto-service-client-picker.component-NawbfdLW.mjs.map +1 -0
  8. package/fesm2022/{trismegisto-spine-auto-service-auto-service-client.component-IATeedbr.mjs → trismegisto-spine-auto-service-auto-service-client.component-BAfeLyTB.mjs} +27 -19
  9. package/fesm2022/trismegisto-spine-auto-service-auto-service-client.component-BAfeLyTB.mjs.map +1 -0
  10. package/fesm2022/trismegisto-spine-auto-service-auto-service-groups.component-BYjIQ9ID.mjs +635 -0
  11. package/fesm2022/trismegisto-spine-auto-service-auto-service-groups.component-BYjIQ9ID.mjs.map +1 -0
  12. package/fesm2022/{trismegisto-spine-auto-service-auto-service-login-attempts.component-CRvxfSWD.mjs → trismegisto-spine-auto-service-auto-service-login-attempts.component-DoyhkSYc.mjs} +4 -4
  13. package/fesm2022/{trismegisto-spine-auto-service-auto-service-login-attempts.component-CRvxfSWD.mjs.map → trismegisto-spine-auto-service-auto-service-login-attempts.component-DoyhkSYc.mjs.map} +1 -1
  14. package/fesm2022/trismegisto-spine-auto-service-auto-service-news-page.component-CBemH4hr.mjs +432 -0
  15. package/fesm2022/trismegisto-spine-auto-service-auto-service-news-page.component-CBemH4hr.mjs.map +1 -0
  16. package/fesm2022/trismegisto-spine-auto-service-auto-service-ratings-page.component-C_THGTkw.mjs +769 -0
  17. package/fesm2022/trismegisto-spine-auto-service-auto-service-ratings-page.component-C_THGTkw.mjs.map +1 -0
  18. package/fesm2022/trismegisto-spine-auto-service-auto-service-suggestions-page.component-DIeunpQ9.mjs +2123 -0
  19. package/fesm2022/trismegisto-spine-auto-service-auto-service-suggestions-page.component-DIeunpQ9.mjs.map +1 -0
  20. package/fesm2022/{trismegisto-spine-auto-service-auto-service.component-DGrs_Jvw.mjs → trismegisto-spine-auto-service-auto-service.component-QsIQ61w2.mjs} +31 -23
  21. package/fesm2022/trismegisto-spine-auto-service-auto-service.component-QsIQ61w2.mjs.map +1 -0
  22. package/fesm2022/{trismegisto-spine-auto-service-client.usage-C_2zBCRN.mjs → trismegisto-spine-auto-service-client.usage-CJxn9Ysz.mjs} +5 -3
  23. package/fesm2022/trismegisto-spine-auto-service-client.usage-CJxn9Ysz.mjs.map +1 -0
  24. package/fesm2022/{trismegisto-spine-auto-service-groups.technical-9aPPulhy.mjs → trismegisto-spine-auto-service-groups.technical-_jQrg1Qh.mjs} +7 -5
  25. package/fesm2022/trismegisto-spine-auto-service-groups.technical-_jQrg1Qh.mjs.map +1 -0
  26. package/fesm2022/{trismegisto-spine-auto-service-groups.usage-CYh61LFL.mjs → trismegisto-spine-auto-service-groups.usage-OvWjNeeT.mjs} +7 -6
  27. package/fesm2022/{trismegisto-spine-auto-service-groups.usage-CYh61LFL.mjs.map → trismegisto-spine-auto-service-groups.usage-OvWjNeeT.mjs.map} +1 -1
  28. package/fesm2022/trismegisto-spine-auto-service-home.component-rQ3dIGIE.mjs +315 -0
  29. package/fesm2022/trismegisto-spine-auto-service-home.component-rQ3dIGIE.mjs.map +1 -0
  30. package/fesm2022/trismegisto-spine-auto-service-home.technical-Ce7oYRdg.mjs +46 -0
  31. package/fesm2022/trismegisto-spine-auto-service-home.technical-Ce7oYRdg.mjs.map +1 -0
  32. package/fesm2022/trismegisto-spine-auto-service-home.usage-D9R_ZMuU.mjs +48 -0
  33. package/fesm2022/trismegisto-spine-auto-service-home.usage-D9R_ZMuU.mjs.map +1 -0
  34. package/fesm2022/trismegisto-spine-auto-service-news.technical-DV9UpobG.mjs +38 -0
  35. package/fesm2022/trismegisto-spine-auto-service-news.technical-DV9UpobG.mjs.map +1 -0
  36. package/fesm2022/trismegisto-spine-auto-service-news.usage-CeuZnG0V.mjs +48 -0
  37. package/fesm2022/trismegisto-spine-auto-service-news.usage-CeuZnG0V.mjs.map +1 -0
  38. package/fesm2022/trismegisto-spine-auto-service-ratings.technical-QIVj6HBU.mjs +52 -0
  39. package/fesm2022/trismegisto-spine-auto-service-ratings.technical-QIVj6HBU.mjs.map +1 -0
  40. package/fesm2022/trismegisto-spine-auto-service-ratings.usage-BkteY8lz.mjs +45 -0
  41. package/fesm2022/trismegisto-spine-auto-service-ratings.usage-BkteY8lz.mjs.map +1 -0
  42. package/fesm2022/trismegisto-spine-auto-service-suggestions.technical-BtUK6sKD.mjs +69 -0
  43. package/fesm2022/trismegisto-spine-auto-service-suggestions.technical-BtUK6sKD.mjs.map +1 -0
  44. package/fesm2022/trismegisto-spine-auto-service-suggestions.usage-BwO9Hjsx.mjs +72 -0
  45. package/fesm2022/trismegisto-spine-auto-service-suggestions.usage-BwO9Hjsx.mjs.map +1 -0
  46. package/fesm2022/trismegisto-spine-auto-service.mjs +49 -19
  47. package/fesm2022/trismegisto-spine-auto-service.mjs.map +1 -1
  48. package/fesm2022/trismegisto-spine-graphql.mjs +11 -2
  49. package/fesm2022/trismegisto-spine-graphql.mjs.map +1 -1
  50. package/fesm2022/trismegisto-spine-layout.mjs +3113 -1221
  51. package/fesm2022/trismegisto-spine-layout.mjs.map +1 -1
  52. package/fesm2022/trismegisto-spine-modal.mjs +7 -5
  53. package/fesm2022/trismegisto-spine-modal.mjs.map +1 -1
  54. package/fesm2022/trismegisto-spine-routing.mjs +12 -20
  55. package/fesm2022/trismegisto-spine-routing.mjs.map +1 -1
  56. package/fesm2022/trismegisto-spine-shell.mjs +15 -39
  57. package/fesm2022/trismegisto-spine-shell.mjs.map +1 -1
  58. package/fesm2022/trismegisto-spine-side-panel.mjs +7 -5
  59. package/fesm2022/trismegisto-spine-side-panel.mjs.map +1 -1
  60. package/fesm2022/trismegisto-spine.mjs +1969 -279
  61. package/fesm2022/trismegisto-spine.mjs.map +1 -1
  62. package/package.json +6 -6
  63. package/types/trismegisto-spine-audit.d.ts +2 -12
  64. package/types/trismegisto-spine-audit.d.ts.map +1 -1
  65. package/types/trismegisto-spine-auto-service.d.ts +22 -5
  66. package/types/trismegisto-spine-auto-service.d.ts.map +1 -1
  67. package/types/trismegisto-spine-layout.d.ts +1083 -283
  68. package/types/trismegisto-spine-layout.d.ts.map +1 -1
  69. package/types/trismegisto-spine-modal.d.ts +1 -0
  70. package/types/trismegisto-spine-modal.d.ts.map +1 -1
  71. package/types/trismegisto-spine-routing.d.ts +3 -12
  72. package/types/trismegisto-spine-routing.d.ts.map +1 -1
  73. package/types/trismegisto-spine-shell.d.ts +2 -15
  74. package/types/trismegisto-spine-shell.d.ts.map +1 -1
  75. package/types/trismegisto-spine-side-panel.d.ts +1 -0
  76. package/types/trismegisto-spine-side-panel.d.ts.map +1 -1
  77. package/types/trismegisto-spine.d.ts +739 -165
  78. package/types/trismegisto-spine.d.ts.map +1 -1
  79. package/fesm2022/trismegisto-spine-auto-service-auto-service-client-picker.component-Cl7ahisu.mjs +0 -65
  80. package/fesm2022/trismegisto-spine-auto-service-auto-service-client-picker.component-Cl7ahisu.mjs.map +0 -1
  81. package/fesm2022/trismegisto-spine-auto-service-auto-service-client.component-IATeedbr.mjs.map +0 -1
  82. package/fesm2022/trismegisto-spine-auto-service-auto-service-groups.component-D6L0gTmd.mjs +0 -605
  83. package/fesm2022/trismegisto-spine-auto-service-auto-service-groups.component-D6L0gTmd.mjs.map +0 -1
  84. package/fesm2022/trismegisto-spine-auto-service-auto-service.component-DGrs_Jvw.mjs.map +0 -1
  85. package/fesm2022/trismegisto-spine-auto-service-client.usage-C_2zBCRN.mjs.map +0 -1
  86. package/fesm2022/trismegisto-spine-auto-service-groups.technical-9aPPulhy.mjs.map +0 -1
  87. package/fesm2022/trismegisto-spine-auto-service-home.component-5_aji1hf.mjs +0 -239
  88. package/fesm2022/trismegisto-spine-auto-service-home.component-5_aji1hf.mjs.map +0 -1
  89. package/fesm2022/trismegisto-spine-auto-service-home.technical-BrVhcgDO.mjs +0 -35
  90. package/fesm2022/trismegisto-spine-auto-service-home.technical-BrVhcgDO.mjs.map +0 -1
  91. package/fesm2022/trismegisto-spine-auto-service-home.usage-DWYGtngX.mjs +0 -43
  92. package/fesm2022/trismegisto-spine-auto-service-home.usage-DWYGtngX.mjs.map +0 -1
package/README.md CHANGED
@@ -3,8 +3,8 @@
3
3
  Shell de aplicação pronto sobre o `@trismegisto/angular`: cabeçalho, menu lateral, área de trabalho em abas
4
4
  (`hm-workspace`), roteamento hierárquico a partir da árvore de rotas do servidor de autorização, telas de
5
5
  login/estado (sem permissão, bloqueado, carregando), modal, painel lateral e notificações por serviço,
6
- configurações (tema, família, densidade, fonte, atalhos), tour de boas-vindas, novidades e documentação por
7
- tela.
6
+ configurações (tema, família, densidade, fonte, atalhos), configuração inicial em tela cheia, novidades e
7
+ documentação por tela.
8
8
 
9
9
  Na cadeia de camadas (`tokens ← icons ← primitives ← core ← angular ← maps · charts · spine`) é a ponta: monta
10
10
  peças grandes sobre o `@trismegisto/angular` e é o que os apps usam como esqueleto. Ao contrário
@@ -30,7 +30,7 @@ Peers: os cinco pacotes base do Hermes (mesma versão), `@angular/core`, `common
30
30
  import { ApplicationConfig } from '@angular/core';
31
31
  import { provideRouter } from '@angular/router';
32
32
  import { provideAuth } from '@orfeu/angular/core';
33
- import { provideUi, provideSpineNews } from '@trismegisto/spine';
33
+ import { provideUi } from '@trismegisto/spine';
34
34
  import { APP_ROUTES, provideShell } from '@trismegisto/spine/shell';
35
35
  import { components } from './features/features.routes';
36
36
  import { environment } from './environments/environment';
@@ -42,12 +42,14 @@ export const appConfig: ApplicationConfig = {
42
42
  provideRouter(APP_ROUTES),
43
43
  provideAuth(environment.authorizationServer),
44
44
  provideShell({ appComponents: components, packageInfo }),
45
- // provideSpineNews([...]), // novidades exibidas uma vez por pessoa
45
+ // Novidades: cadastradas no Argos (auto-serviço › Novidades) e lidas de /me/news com SPINE_ADMIN_API
46
+ // (o provideAutoService já fornece): aviso discreto uma vez por novidade, painel lateral no "Ver".
46
47
  ],
47
48
  };
48
49
  ```
49
50
 
50
- - **`provideUi(config)`** registra o catálogo de ícones inteiro (o shell usa ícones de vários grupos), aplica
51
+ - **`provideUi(config)`** registra os ícones que o próprio spine desenha (`SPINE_ICONS`) e pede o resto do catálogo
52
+ sob demanda (`provideHermesIconLoader`, chunk à parte logo depois do bootstrap; ver "Ícones do app"), aplica
51
53
  o tema no `<body>` e usa `clientId` como prefixo de **todas** as chaves salvas (tema, menu lateral, abas):
52
54
  apps diferentes na mesma origem não misturam preferências. Opções: `clientId`, `appName` (nome ao lado do
53
55
  logo no cabeçalho), `logo`, `showAppName` e `favicon` (ver "Logo e favicon" abaixo), `technicalDocs` (mostra a
@@ -197,7 +199,7 @@ antes de "Sobre".
197
199
  | Notificações | `notificacoes` | Tipos de aviso do app, aviso do navegador | Com `SpineConfig.features.notices` |
198
200
  | Aparência | `aparencia` | Tema, cor, densidade, fonte, menu lateral | Sempre |
199
201
  | Região | `regiao` | Idioma, fuso horário | Sempre |
200
- | Atalhos | `atalhos` | Lista de atalhos, tour guiado | Sempre |
202
+ | Atalhos | `atalhos` | Lista de atalhos | Sempre |
201
203
  | Sobre | `sobre` | Aplicação, versão, build, ambiente, links, dependências, informações de suporte | Sempre |
202
204
 
203
205
  Linhas da seção **Sessões**: "Sessão" sempre; "Token de acesso" só com `SpineConfig.technicalDocs` (sem configuração,
@@ -221,7 +223,7 @@ O grupo de administração do projeto (crachá Administrador, o do auto-serviço
221
223
  grupo recebe o crachá dele nesta aplicação no próximo token.
222
224
 
223
225
  **Chave antiga**: até a versão 2.1.2 a seção "Conta e segurança" (`seguranca`) reunia MFA e sessões, e "Sessão" e os
224
- tokens ficavam em "Conta". `?settings=seguranca` (e `SPINE_SETTINGS_KEYS.security`, agora `@deprecated`) abre a seção
226
+ tokens ficavam em "Conta". `?settings=seguranca` (a constante `SPINE_SETTINGS_KEYS.security` saiu na 3.3.1) abre a seção
225
227
  **MFA** (`SPINE_SETTINGS_ALIASES`, `resolveSettingsSection`); sem a API da conta, como qualquer chave sem seção, abre
226
228
  a primeira. `?settings=conta` continua abrindo Conta.
227
229
 
@@ -280,14 +282,14 @@ ou código de recuperação); o Argos guarda essa prova por 5 minutos.
280
282
 
281
283
  | Import | Conteúdo |
282
284
  | --- | --- |
283
- | `@trismegisto/spine` | `provideUi`, `SpineConfig`, `ThemeService`, `DensityService`, `SpinePreferencesService`, `provideSpineNews`, textos (`SPINE_MESSAGES`, `provideSpineMessages`) |
285
+ | `@trismegisto/spine` | `provideUi`, `SpineConfig`, `ThemeService`, `DensityService`, `SpinePreferencesService`, textos (`SPINE_MESSAGES`, `provideSpineMessages`) |
284
286
  | `@trismegisto/spine/shell` | `APP_ROUTES`, `PUBLIC_ROUTES`, `PRIVATE_ROUTES`, `provideShell`, telas de login e de estado |
285
287
  | `@trismegisto/spine/routing` | `ComponentRoute`, `COMPONENT_ROUTES`, `ViewGuard`, `RouteIdInterceptor`, resolução de rota |
286
- | `@trismegisto/spine/layout` | `LayoutComponent` (`spine-layout`), cabeçalho, menu lateral, onboarding, atalhos, configurações, novidades, documentação, `SpineLogoutService` |
288
+ | `@trismegisto/spine/layout` | `LayoutComponent` (`spine-layout`), cabeçalho, menu lateral, configuração inicial, atalhos, configurações, novidades, documentação, `SpineLogoutService` |
287
289
  | `@trismegisto/spine/modal` | `ModalService` (`open(config)` devolve um `Observable` com o resultado), `ModalContent` |
288
290
  | `@trismegisto/spine/side-panel` | `SidePanelService`, `SidePanelContent` |
289
291
  | `@trismegisto/spine/notification` | `NotificationService` (`addNotification`) |
290
- | `@trismegisto/spine/auto-service` | `provideAutoService({ adminApi })`: telas prontas de autosserviço (início, grupos, aplicações, cliente e, no menu Auditoria, ações e tentativas de login), sempre carregadas sob demanda (`AUTO_SERVICE_SCREENS`); as classes das telas não são exportadas. Cada tela já vem com a documentação (manual de uso e especificação técnica, em `AUTO_SERVICE_DOCUMENTS`, também sob demanda) no botão de documentação da página. `AutoServiceService` (com os tipos de grupo `AutoServiceGroup`, `AutoServiceGroupType`, `AutoServiceGroupCreate`, `AutoServiceGroupUpdate`) e o tipo `AutoServiceClient` são exportados |
292
+ | `@trismegisto/spine/auto-service` | `provideAutoService({ adminApi })`: telas prontas de autosserviço (início, novidades, sugestões e avaliações na raiz do menu; grupos, aplicações e cliente em Administração; ações e tentativas de login em Auditoria), sempre carregadas sob demanda (`AUTO_SERVICE_SCREENS`); as classes das telas não são exportadas. Cada tela já vem com a documentação (manual de uso e especificação técnica, em `AUTO_SERVICE_DOCUMENTS`, também sob demanda) no botão de documentação da página. `AutoServiceService` (com os tipos de grupo `AutoServiceGroup`, `AutoServiceGroupType`, `AutoServiceGroupCreate`, `AutoServiceGroupUpdate`) e o tipo `AutoServiceClient` são exportados |
291
293
  | `@trismegisto/spine/audit` | As telas de auditoria — Ações (`SpineAuditComponent`, `spine-audit`) e Tentativas de login (`SpineLoginAttemptsComponent`, `spine-login-attempts`) —, as mesmas para o painel central (`scope="admin"`) e o auto-serviço (`scope="client"`), + `SpineAuditService` e os tipos/funções puras (`AuditEvent`, `AuditQuery`, `buildAuditQuery`, `auditApiBase`…). **Exporta o componente: importe só de código lazy** |
292
294
  | `@trismegisto/spine/graphql` | `provideGraphql({ endpoints })`: um cliente Apollo nomeado por backend |
293
295
 
@@ -345,7 +347,13 @@ component: async () => (await import('./admin-audit-page.component')).AdminAudit
345
347
  - **Idioma:** os textos do spine seguem `HermesI18nService.setLocale` (o idioma em si se configura com
346
348
  `provideHermesI18n` do `@trismegisto/angular`). `provideSpineMessages({ override, catalogs })` só sobrescreve
347
349
  textos ou acrescenta idiomas.
348
- - **Ícones do app** já estão cobertos pelo catálogo inteiro que o `provideUi` registra.
350
+ - **Ícones do app** (desde a 3.3.1): o `provideUi` registra no bootstrap só `SPINE_ICONS` (os ícones do próprio spine,
351
+ gerados por `scripts/generate-spine-icons.mjs` no build) e carrega o catálogo inteiro sob demanda. Ícone do app que
352
+ ninguém registrou (o da rota vinda do servidor, um `[icon]` de botão) aparece quando o chunk chega: até lá, o
353
+ genérico. Para desenhar já no primeiro quadro, registre os do app com `provideHermesIcons(...)`.
354
+ - **Detecção de mudanças:** todos os componentes do spine são `OnPush`. O componente que o app abre no
355
+ `ModalService`/`SidePanelService` (como as telas no layout) precisa atualizar a view por signal, `async` ou
356
+ `markForCheck()` quando o estado muda depois de `await`/`subscribe`.
349
357
  - `ModalService`/`SidePanelService` abrem um componente que implementa `ModalContent` (`getResult()`,
350
358
  `isValid?()`). Para diálogos fora do shell, o `@trismegisto/angular` tem `HermesDialogService`.
351
359
 
@@ -3,7 +3,7 @@ import * as i0 from '@angular/core';
3
3
  import { inject, Injectable, input, output, computed, ChangeDetectionStrategy, Component, signal, effect, untracked, DestroyRef } from '@angular/core';
4
4
  import { firstValueFrom, merge, debounceTime, distinctUntilChanged } from 'rxjs';
5
5
  import { AuthorizationService } from '@orfeu/angular/core';
6
- import { IconButtonComponent, SpinnerComponent, AlertComponent, ButtonComponent, DataGridCellDirective, DataGridComponent, DrawerComponent, FilterBarComponent, ProgressComponent, SegmentedControlComponent, StatCardComponent, StateScreenComponent, SwitchComponent, TagComponent, CardComponent, ComboboxComponent, DateRangePickerComponent, InputComponent } from '@trismegisto/angular';
6
+ import { IconButtonComponent, SkeletonComponent, SpinnerComponent, AlertComponent, ButtonComponent, DataGridCellDirective, DataGridComponent, DrawerComponent, FilterBarComponent, ProgressComponent, SegmentedControlComponent, StatCardComponent, StateScreenComponent, SwitchComponent, TagComponent, AccountAvatarComponent, CardComponent, ComboboxComponent, DateRangePickerComponent, InputComponent } from '@trismegisto/angular';
7
7
  import { formatMessage, pluralMessage } from '@trismegisto/primitives';
8
8
  import { Router } from '@angular/router';
9
9
  import { SPINE_MESSAGES, SPINE_ADMIN_API } from '@trismegisto/spine';
@@ -462,8 +462,10 @@ class SpineAuditTopCardComponent {
462
462
  @if (failed()) {
463
463
  <p class="muted top__error" role="status">{{ text.topsError }}</p>
464
464
  }
465
+ } @else if (loading()) {
466
+ <hm-skeleton shape="list" [count]="pageSize()" [label]="text.loading" />
465
467
  } @else {
466
- <p class="muted">{{ loading() ? text.loading : failed() ? text.topsError : text.noTop }}</p>
468
+ <p class="muted">{{ failed() ? text.topsError : text.noTop }}</p>
467
469
  }
468
470
  @if (total() > 0) {
469
471
  <div class="top__pager">
@@ -479,11 +481,11 @@ class SpineAuditTopCardComponent {
479
481
  </div>
480
482
  }
481
483
  </section>
482
- `, isInline: true, styles: [":host{display:block;min-inline-size:0}.card{border:1px solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface)}.card-block{padding:var(--hm-space-sm, 8px)}.muted{color:var(--hm-muted)}.top{display:flex;flex-direction:column;block-size:100%;box-sizing:border-box}.top__head{display:flex;align-items:center;justify-content:space-between;gap:var(--hm-space-sm, 8px);margin:0 0 var(--hm-space-sm, 8px);min-block-size:24px}.top__title{margin:0;font-size:14px;font-weight:600;color:var(--hm-text)}.top__error{margin:var(--hm-space-xs, 4px) 0 0;font-size:12px}.top__pager{display:flex;align-items:center;justify-content:flex-end;gap:var(--hm-space-xs, 4px);margin-block-start:auto;padding-block-start:var(--hm-space-sm, 8px)}.top__range{font-size:12px;color:var(--hm-muted);font-variant-numeric:tabular-nums}.top__buttons{display:inline-flex;gap:2px}.rank{margin:0;padding:0;list-style:none;display:flex;flex-direction:column;gap:2px;transition:opacity .12s ease}.rank--busy{opacity:.55}.rank__item{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:2px var(--hm-space-sm, 8px);inline-size:100%;padding:6px 8px;border:0;border-radius:var(--hm-radius-control);background:transparent;color:var(--hm-text);font:inherit;font-size:13px;text-align:start;cursor:pointer}.rank__item:hover:not(:disabled){background:color-mix(in srgb,var(--hm-action) 8%,transparent)}.rank__item:focus-visible{outline:2px solid var(--hm-action);outline-offset:1px}.rank__item:disabled{cursor:default}.rank__name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.rank__value{font-variant-numeric:tabular-nums;white-space:nowrap}.rank__fail{color:var(--hm-danger)}.rank__denied{color:var(--hm-warning, #c98a00)}.rank__track{grid-column:1/-1;display:block;block-size:4px;border-radius:2px;background:color-mix(in srgb,var(--hm-text) 8%,transparent);overflow:hidden}.rank__bar{display:flex;justify-content:flex-end;inline-size:100%;block-size:100%;background:var(--hm-success)}.rank__seg{display:block;block-size:100%}.rank__seg--fail{background:var(--hm-danger)}.rank__seg--denied{background:var(--hm-warning)}@media(prefers-reduced-motion:reduce){.rank{transition:none}}:host-context([data-motion=reduce]) .rank{transition:none}\n"], dependencies: [{ kind: "component", type: IconButtonComponent, selector: "hm-icon-button", inputs: ["label", "icon", "solid", "iconDirection", "emphasis", "intent", "size", "disabled", "toggle", "pressed", "keyShortcuts"], outputs: ["pressedChange", "clicked"] }, { kind: "component", type: SpinnerComponent, selector: "hm-spinner", inputs: ["size", "inverse", "label", "labelVisible", "decorative", "done", "doneLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
484
+ `, isInline: true, styles: [":host{display:block;min-inline-size:0}.card{border:1px solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface-raised)}.card-block{padding:var(--hm-space-sm, 8px)}.muted{color:var(--hm-muted)}.top{display:flex;flex-direction:column;block-size:100%;box-sizing:border-box}.top__head{display:flex;align-items:center;justify-content:space-between;gap:var(--hm-space-sm, 8px);margin:0 0 var(--hm-space-sm, 8px);min-block-size:24px}.top__title{margin:0;font-size:14px;font-weight:600;color:var(--hm-text)}.top__error{margin:var(--hm-space-xs, 4px) 0 0;font-size:12px}.top__pager{display:flex;align-items:center;justify-content:flex-end;gap:var(--hm-space-xs, 4px);margin-block-start:auto;padding-block-start:var(--hm-space-sm, 8px)}.top__range{font-size:12px;color:var(--hm-muted);font-variant-numeric:tabular-nums}.top__buttons{display:inline-flex;gap:2px}.rank{margin:0;padding:0;list-style:none;display:flex;flex-direction:column;gap:2px;transition:opacity .12s ease}.rank--busy{opacity:.55}.rank__item{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:2px var(--hm-space-sm, 8px);inline-size:100%;padding:6px 8px;border:0;border-radius:var(--hm-radius-control);background:transparent;color:var(--hm-text);font:inherit;font-size:13px;text-align:start;cursor:pointer}.rank__item:hover:not(:disabled){background:color-mix(in srgb,var(--hm-action) 8%,transparent)}.rank__item:focus-visible{outline:2px solid var(--hm-action);outline-offset:1px}.rank__item:disabled{cursor:default}.rank__name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.rank__value{font-variant-numeric:tabular-nums;white-space:nowrap}.rank__fail{color:var(--hm-danger)}.rank__denied{color:var(--hm-warning, #c98a00)}.rank__track{grid-column:1/-1;display:block;block-size:4px;border-radius:2px;background:color-mix(in srgb,var(--hm-text) 8%,transparent);overflow:hidden}.rank__bar{display:flex;justify-content:flex-end;inline-size:100%;block-size:100%;background:var(--hm-success)}.rank__seg{display:block;block-size:100%}.rank__seg--fail{background:var(--hm-danger)}.rank__seg--denied{background:var(--hm-warning)}@media(prefers-reduced-motion:reduce){.rank{transition:none}}:host-context([data-motion=reduce]) .rank{transition:none}\n"], dependencies: [{ kind: "component", type: IconButtonComponent, selector: "hm-icon-button", inputs: ["label", "icon", "solid", "iconDirection", "emphasis", "intent", "size", "disabled", "toggle", "pressed", "keyShortcuts"], outputs: ["pressedChange", "clicked"] }, { kind: "component", type: SkeletonComponent, selector: "hm-skeleton", inputs: ["shape", "count", "label"] }, { kind: "component", type: SpinnerComponent, selector: "hm-spinner", inputs: ["size", "inverse", "label", "labelVisible", "decorative", "done", "doneLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
483
485
  }
484
486
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SpineAuditTopCardComponent, decorators: [{
485
487
  type: Component,
486
- args: [{ selector: 'spine-audit-top-card', imports: [IconButtonComponent, SpinnerComponent], template: `
488
+ args: [{ selector: 'spine-audit-top-card', imports: [IconButtonComponent, SkeletonComponent, SpinnerComponent], template: `
487
489
  <section class="card card-block top" [attr.aria-labelledby]="headingId" [attr.aria-busy]="loading()">
488
490
  <div class="top__head">
489
491
  <h3 class="top__title" [id]="headingId">{{ heading() }}</h3>
@@ -520,8 +522,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImpo
520
522
  @if (failed()) {
521
523
  <p class="muted top__error" role="status">{{ text.topsError }}</p>
522
524
  }
525
+ } @else if (loading()) {
526
+ <hm-skeleton shape="list" [count]="pageSize()" [label]="text.loading" />
523
527
  } @else {
524
- <p class="muted">{{ loading() ? text.loading : failed() ? text.topsError : text.noTop }}</p>
528
+ <p class="muted">{{ failed() ? text.topsError : text.noTop }}</p>
525
529
  }
526
530
  @if (total() > 0) {
527
531
  <div class="top__pager">
@@ -537,7 +541,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImpo
537
541
  </div>
538
542
  }
539
543
  </section>
540
- `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{display:block;min-inline-size:0}.card{border:1px solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface)}.card-block{padding:var(--hm-space-sm, 8px)}.muted{color:var(--hm-muted)}.top{display:flex;flex-direction:column;block-size:100%;box-sizing:border-box}.top__head{display:flex;align-items:center;justify-content:space-between;gap:var(--hm-space-sm, 8px);margin:0 0 var(--hm-space-sm, 8px);min-block-size:24px}.top__title{margin:0;font-size:14px;font-weight:600;color:var(--hm-text)}.top__error{margin:var(--hm-space-xs, 4px) 0 0;font-size:12px}.top__pager{display:flex;align-items:center;justify-content:flex-end;gap:var(--hm-space-xs, 4px);margin-block-start:auto;padding-block-start:var(--hm-space-sm, 8px)}.top__range{font-size:12px;color:var(--hm-muted);font-variant-numeric:tabular-nums}.top__buttons{display:inline-flex;gap:2px}.rank{margin:0;padding:0;list-style:none;display:flex;flex-direction:column;gap:2px;transition:opacity .12s ease}.rank--busy{opacity:.55}.rank__item{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:2px var(--hm-space-sm, 8px);inline-size:100%;padding:6px 8px;border:0;border-radius:var(--hm-radius-control);background:transparent;color:var(--hm-text);font:inherit;font-size:13px;text-align:start;cursor:pointer}.rank__item:hover:not(:disabled){background:color-mix(in srgb,var(--hm-action) 8%,transparent)}.rank__item:focus-visible{outline:2px solid var(--hm-action);outline-offset:1px}.rank__item:disabled{cursor:default}.rank__name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.rank__value{font-variant-numeric:tabular-nums;white-space:nowrap}.rank__fail{color:var(--hm-danger)}.rank__denied{color:var(--hm-warning, #c98a00)}.rank__track{grid-column:1/-1;display:block;block-size:4px;border-radius:2px;background:color-mix(in srgb,var(--hm-text) 8%,transparent);overflow:hidden}.rank__bar{display:flex;justify-content:flex-end;inline-size:100%;block-size:100%;background:var(--hm-success)}.rank__seg{display:block;block-size:100%}.rank__seg--fail{background:var(--hm-danger)}.rank__seg--denied{background:var(--hm-warning)}@media(prefers-reduced-motion:reduce){.rank{transition:none}}:host-context([data-motion=reduce]) .rank{transition:none}\n"] }]
544
+ `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{display:block;min-inline-size:0}.card{border:1px solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface-raised)}.card-block{padding:var(--hm-space-sm, 8px)}.muted{color:var(--hm-muted)}.top{display:flex;flex-direction:column;block-size:100%;box-sizing:border-box}.top__head{display:flex;align-items:center;justify-content:space-between;gap:var(--hm-space-sm, 8px);margin:0 0 var(--hm-space-sm, 8px);min-block-size:24px}.top__title{margin:0;font-size:14px;font-weight:600;color:var(--hm-text)}.top__error{margin:var(--hm-space-xs, 4px) 0 0;font-size:12px}.top__pager{display:flex;align-items:center;justify-content:flex-end;gap:var(--hm-space-xs, 4px);margin-block-start:auto;padding-block-start:var(--hm-space-sm, 8px)}.top__range{font-size:12px;color:var(--hm-muted);font-variant-numeric:tabular-nums}.top__buttons{display:inline-flex;gap:2px}.rank{margin:0;padding:0;list-style:none;display:flex;flex-direction:column;gap:2px;transition:opacity .12s ease}.rank--busy{opacity:.55}.rank__item{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:2px var(--hm-space-sm, 8px);inline-size:100%;padding:6px 8px;border:0;border-radius:var(--hm-radius-control);background:transparent;color:var(--hm-text);font:inherit;font-size:13px;text-align:start;cursor:pointer}.rank__item:hover:not(:disabled){background:color-mix(in srgb,var(--hm-action) 8%,transparent)}.rank__item:focus-visible{outline:2px solid var(--hm-action);outline-offset:1px}.rank__item:disabled{cursor:default}.rank__name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.rank__value{font-variant-numeric:tabular-nums;white-space:nowrap}.rank__fail{color:var(--hm-danger)}.rank__denied{color:var(--hm-warning, #c98a00)}.rank__track{grid-column:1/-1;display:block;block-size:4px;border-radius:2px;background:color-mix(in srgb,var(--hm-text) 8%,transparent);overflow:hidden}.rank__bar{display:flex;justify-content:flex-end;inline-size:100%;block-size:100%;background:var(--hm-success)}.rank__seg{display:block;block-size:100%}.rank__seg--fail{background:var(--hm-danger)}.rank__seg--denied{background:var(--hm-warning)}@media(prefers-reduced-motion:reduce){.rank{transition:none}}:host-context([data-motion=reduce]) .rank{transition:none}\n"] }]
541
545
  }], propDecorators: { heading: [{ type: i0.Input, args: [{ isSignal: true, alias: "heading", required: true }] }], rows: [{ type: i0.Input, args: [{ isSignal: true, alias: "rows", required: true }] }], total: [{ type: i0.Input, args: [{ isSignal: true, alias: "total", required: false }] }], offset: [{ type: i0.Input, args: [{ isSignal: true, alias: "offset", required: false }] }], pageSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageSize", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], failed: [{ type: i0.Input, args: [{ isSignal: true, alias: "failed", required: false }] }], picked: [{ type: i0.Output, args: ["picked"] }], offsetChange: [{ type: i0.Output, args: ["offsetChange"] }] } });
542
546
 
543
547
  /** Eventos por página da tabela, de saída (o servidor aceita até 200 — `AUDIT_MAX_PAGE_SIZE`). */
@@ -1329,15 +1333,15 @@ class SpineAuditComponent {
1329
1333
  return outcome === 'success' ? 'success' : outcome === 'failure' ? 'danger' : outcome === 'denied' ? 'warning' : 'neutral';
1330
1334
  }
1331
1335
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SpineAuditComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1332
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: SpineAuditComponent, isStandalone: true, selector: "spine-audit", inputs: { scope: { classPropertyName: "scope", publicName: "scope", isSignal: true, isRequired: true, transformFunction: null }, clientId: { classPropertyName: "clientId", publicName: "clientId", isSignal: true, isRequired: false, transformFunction: null }, adminApi: { classPropertyName: "adminApi", publicName: "adminApi", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"audit\" [attr.aria-busy]=\"loading() || pageLoading()\">\n <!-- Per\u00EDodo (escolha r\u00E1pida; as datas da faixa de filtros vencem) + atualizar. -->\n <div class=\"audit__toolbar\">\n <!-- Unidade: por a\u00E7\u00E3o (uma linha por trace, padr\u00E3o) ou por evento (cada registro). -->\n <hm-segmented-control\n [label]=\"text.unitLabel\"\n [options]=\"unitOptions\"\n [value]=\"unit()\"\n (valueChange)=\"setUnit($event)\"\n size=\"small\"\n emphasis=\"action\"\n />\n <hm-segmented-control\n [label]=\"text.period\"\n [options]=\"periodOptions\"\n [value]=\"period()\"\n (valueChange)=\"setPeriod($event)\"\n size=\"small\"\n emphasis=\"action\"\n />\n @if (customDates()) {\n <span class=\"muted audit__period-note\">{{ text.periodFromFilters }}</span>\n }\n <span class=\"audit__toolbar-end\">\n @if (lastUpdated(); as at) {\n <span class=\"muted audit__updated\" aria-live=\"polite\">{{ fmt(text.updatedAt, { time: clock(at) }) }}</span>\n }\n <hm-switch [label]=\"text.autoRefresh\" [value]=\"autoRefresh()\" (valueChange)=\"autoRefresh.set($event)\" />\n <hm-button emphasis=\"medium\" size=\"small\" icon=\"refresh\" [loading]=\"loading()\" (clicked)=\"reload()\">{{ text.refresh }}</hm-button>\n </span>\n </div>\n\n <hm-filter-bar\n class=\"audit__filters\"\n [label]=\"text.filters\"\n [filters]=\"filterFields()\"\n [value]=\"filterValue()\"\n (valueChange)=\"setFilters($event)\"\n [savedFilters]=\"true\"\n [storageKey]=\"storageKey()\"\n />\n\n @if (errorKind()) {\n <hm-state-screen\n class=\"audit__state\"\n [situation]=\"errorSituation()\"\n [heading]=\"text.errorTitle\"\n [description]=\"errorText()\"\n [action]=\"errorKind() === 'generic' ? text.retry : (hasFilters() ? text.clearFilters : '')\"\n actionIcon=\"refresh\"\n (actionTriggered)=\"errorKind() === 'generic' ? reload() : clearFilters()\"\n />\n } @else if (empty()) {\n <hm-state-screen\n class=\"audit__state\"\n situation=\"empty\"\n [heading]=\"unitText().emptyTitle\"\n [description]=\"text.emptyText\"\n [action]=\"hasFilters() ? text.clearFilters : ''\"\n (actionTriggered)=\"clearFilters()\"\n />\n } @else {\n <!-- KPIs: falhas/negados s\u00E3o filtros (altern\u00E2ncia). -->\n <section class=\"audit__kpis\" [attr.aria-label]=\"text.kpis\">\n <hm-stat-card\n icon=\"list\"\n [label]=\"unitText().kpiTotal\"\n [value]=\"number(stats()?.total ?? 0)\"\n [description]=\"unitText().kpiTotalDescription\"\n [loading]=\"loading() && !stats()\"\n />\n <hm-stat-card\n icon=\"error-standard\"\n [label]=\"text.kpiFailures\"\n [value]=\"number(stats()?.by_outcome?.failure ?? 0)\"\n [intent]=\"(stats()?.by_outcome?.failure ?? 0) > 0 ? 'danger' : 'neutral'\"\n [description]=\"fmt(text.kpiFilterHint, { share: share(stats()?.by_outcome?.failure ?? 0) })\"\n [loading]=\"loading() && !stats()\"\n selectable\n [selected]=\"selectedOutcome() === 'failure'\"\n (selectedChange)=\"toggleOutcome('failure', $event)\"\n />\n <hm-stat-card\n icon=\"ban\"\n [label]=\"text.kpiDenied\"\n [value]=\"number(stats()?.by_outcome?.denied ?? 0)\"\n [intent]=\"(stats()?.by_outcome?.denied ?? 0) > 0 ? 'warning' : 'neutral'\"\n [description]=\"fmt(text.kpiFilterHint, { share: share(stats()?.by_outcome?.denied ?? 0) })\"\n [loading]=\"loading() && !stats()\"\n selectable\n [selected]=\"selectedOutcome() === 'denied'\"\n (selectedChange)=\"toggleOutcome('denied', $event)\"\n />\n </section>\n\n <!-- Linha do tempo: barras empilhadas em CSS (sucesso embaixo, falha, negado). Cada barra tem\n o resumo em texto (title + aria-label); o gr\u00E1fico n\u00E3o \u00E9 a \u00FAnica forma de ler o n\u00FAmero. -->\n <section class=\"card card-block audit__timeline\" aria-labelledby=\"audit-timeline-title\">\n <h3 class=\"audit__section-title\" id=\"audit-timeline-title\">{{ text.timeline }}</h3>\n @if (hasTimeline()) {\n <ol class=\"timeline\" [attr.aria-label]=\"fmt(unitText().timelineSummary, { from: timelineEdges()?.from ?? '', to: timelineEdges()?.to ?? '' })\">\n @for (bar of timelineBars(); track bar.key) {\n <li class=\"timeline__bar\" [attr.title]=\"bar.summary\" [attr.aria-label]=\"bar.summary\">\n <span class=\"timeline__seg timeline__seg--denied\" [style.height.%]=\"bar.denied\"></span>\n <span class=\"timeline__seg timeline__seg--fail\" [style.height.%]=\"bar.fail\"></span>\n <span class=\"timeline__seg timeline__seg--ok\" [style.height.%]=\"bar.ok\"></span>\n </li>\n }\n </ol>\n <div class=\"timeline__axis\" aria-hidden=\"true\">\n <span>{{ timelineEdges()?.from }}</span>\n <span>{{ timelineEdges()?.to }}</span>\n </div>\n <ul class=\"timeline__legend\">\n <li><span class=\"swatch swatch--ok\"></span>{{ text.outcomeSuccess }}</li>\n <li><span class=\"swatch swatch--fail\"></span>{{ text.outcomeFailure }}</li>\n <li><span class=\"swatch swatch--denied\"></span>{{ text.outcomeDenied }}</li>\n </ul>\n } @else {\n <p class=\"muted\">{{ loading() ? text.loading : unitText().timelineEmpty }}</p>\n }\n </section>\n\n <!-- Rankings paginados no servidor (tops/:kind). Clicar num item vira filtro. -->\n <div class=\"audit__tops\" [class.audit__tops--three]=\"multiApp()\">\n @if (multiApp()) {\n <spine-audit-top-card\n class=\"audit__top audit__top--apps\"\n [heading]=\"text.topApps\"\n [rows]=\"topAppRows()\"\n [total]=\"tops.apps().total\"\n [offset]=\"tops.apps().offset\"\n [max]=\"tops.apps().max\"\n [pageSize]=\"topPageSize\"\n [loading]=\"tops.apps().loading\"\n [failed]=\"tops.apps().failed\"\n (offsetChange)=\"loadTop('apps', $event)\"\n (picked)=\"pickTop('apps', $event)\"\n />\n }\n <spine-audit-top-card\n class=\"audit__top audit__top--operations\"\n [heading]=\"text.topOperations\"\n [rows]=\"topOperationRows()\"\n [total]=\"tops.operations().total\"\n [offset]=\"tops.operations().offset\"\n [max]=\"tops.operations().max\"\n [pageSize]=\"topPageSize\"\n [loading]=\"tops.operations().loading\"\n [failed]=\"tops.operations().failed\"\n (offsetChange)=\"loadTop('operations', $event)\"\n (picked)=\"pickTop('operations', $event)\"\n />\n <spine-audit-top-card\n class=\"audit__top audit__top--users\"\n [heading]=\"text.topUsers\"\n [rows]=\"topUserRows()\"\n [total]=\"tops.users().total\"\n [offset]=\"tops.users().offset\"\n [max]=\"tops.users().max\"\n [pageSize]=\"topPageSize\"\n [loading]=\"tops.users().loading\"\n [failed]=\"tops.users().failed\"\n (offsetChange)=\"loadTop('users', $event)\"\n (picked)=\"pickTop('users', $event)\"\n />\n </div>\n\n <!-- Tabela: pagina\u00E7\u00E3o no servidor. A grade n\u00E3o corta nada ([paginated]=false): mostra a p\u00E1gina que veio\n (limit = itens por p\u00E1gina) e o rodap\u00E9 usa o total do resumo (mesma consulta). Esqueleto s\u00F3 na 1\u00AA\n carga; nas seguintes as linhas ficam e o indicador aparece por cima (sem piscar). -->\n <div class=\"audit__grid\" [class.audit__grid--busy]=\"gridBusy()\" [attr.aria-busy]=\"gridBusy()\">\n @if (gridBusy()) {\n <hm-progress class=\"audit__grid-progress\" indeterminate [label]=\"text.updating\" labelHidden />\n }\n <hm-data-grid\n [caption]=\"unitText().caption\"\n [columns]=\"columns()\"\n [rows]=\"events()\"\n [rowId]=\"rowId\"\n [loading]=\"gridSkeleton()\"\n [filterable]=\"false\"\n [paginated]=\"false\"\n [totalItems]=\"total()\"\n [pageState]=\"pageState()\"\n (pageStateChange)=\"changePage($event)\"\n [pageSizes]=\"pageSizes\"\n [itemsLabel]=\"unitText().pageItems\"\n [pageSizeLabel]=\"text.pageSize\"\n remoteSort\n rowClickable\n (rowClicked)=\"openRow($event)\"\n >\n <ng-template hmCell=\"operation\" let-item>\n <span class=\"audit__op\" [attr.title]=\"item.operation_label || null\">{{ operationName(item) }}</span>\n </ng-template>\n <ng-template hmCell=\"spans\" let-item>\n <span class=\"audit__spans\" [attr.title]=\"spansHint(item) || null\">\n {{ number(item.span_count ?? 1) }}\n @if (item.orphan) {\n <hm-tag class=\"audit__orphan\" [name]=\"text.orphanTag\" color=\"neutral\" />\n }\n </span>\n </ng-template>\n <ng-template hmCell=\"outcome\" let-item>\n <hm-tag [name]=\"outcomeLabel(item.outcome)\" [color]=\"outcomeColor(item.outcome)\" />\n </ng-template>\n </hm-data-grid>\n </div>\n\n @if (pageFailed()) {\n <hm-alert class=\"audit__page-error\" variant=\"error\" size=\"small\">{{ unitText().pageError }}</hm-alert>\n }\n }\n</div>\n\n<!-- Detalhe do evento + a cadeia (trace). -->\n<hm-drawer [open]=\"detailOpen()\" (openChange)=\"onDetailOpenChange($event)\" [heading]=\"showingAction() ? text.actionDetailHeading : text.detailHeading\">\n @if (selected(); as ev) {\n <div class=\"detail\">\n <div class=\"detail__head\">\n <strong class=\"detail__title\">{{ operationName(ev) }}</strong>\n <hm-tag [name]=\"outcomeLabel(headOutcome())\" [color]=\"outcomeColor(headOutcome())\" />\n </div>\n @if (showingAction() && selectedAction()?.orphan) {\n <hm-alert class=\"detail__orphan\" variant=\"info\" size=\"small\">{{ text.orphanNote }}</hm-alert>\n }\n @if (ev.operation_label && ev.operation_identifier) {\n <p class=\"muted detail__description\">{{ ev.operation_label }}</p>\n }\n\n @if (detailLoading()) {\n <p class=\"muted\" role=\"status\">{{ text.detailLoading }}</p>\n }\n @if (detailFailed()) {\n <hm-alert variant=\"warning\" size=\"small\">{{ text.detailError }}</hm-alert>\n }\n\n <dl class=\"detail__list\">\n @for (field of detailFields(); track field.label) {\n <div class=\"detail__row\">\n <dt>{{ field.label }}</dt>\n <dd>\n @if (field.code) { <code>{{ field.value }}</code> } @else { {{ field.value }} }\n </dd>\n </div>\n }\n </dl>\n\n @if (detailsJson()) {\n <h3 class=\"audit__section-title\">{{ text.fieldDetails }}</h3>\n <pre class=\"detail__json\">{{ detailsJson() }}</pre>\n }\n\n <!-- Por a\u00E7\u00E3o, os spans sobem para logo abaixo do cabe\u00E7alho (order no CSS); por evento, ficam no fim. -->\n @if (ev.trace_id) {\n <div class=\"detail__chain\" [class.detail__chain--first]=\"showingAction()\">\n <div class=\"detail__actions\">\n @if (!trace() && !traceLoading()) {\n <hm-button emphasis=\"medium\" size=\"small\" icon=\"tree-view\" (clicked)=\"showTrace()\">{{ text.viewTrace }}</hm-button>\n }\n <hm-button emphasis=\"minimal\" size=\"small\" icon=\"filter\" (clicked)=\"filterByTrace(ev.trace_id)\">{{ text.filterByTrace }}</hm-button>\n </div>\n @if (traceLoading()) {\n <p class=\"muted\" role=\"status\">{{ text.traceLoading }}</p>\n }\n @if (traceFailed()) {\n <hm-alert variant=\"error\" size=\"small\">{{ text.traceError }}</hm-alert>\n }\n @if (trace(); as tr) {\n <section class=\"trace\" aria-labelledby=\"audit-trace-title\">\n <h3 class=\"audit__section-title\" id=\"audit-trace-title\">{{ showingAction() ? text.spansHeading : text.traceHeading }}</h3>\n @if (tr.hidden_count) {\n <hm-alert class=\"trace__hidden\" variant=\"info\" size=\"small\">{{ plural(text.traceHidden, tr.hidden_count) }}</hm-alert>\n }\n @if (!tr.items.length) {\n <p class=\"muted\">{{ text.traceEmpty }}</p>\n } @else if (spanTimeline(); as tl) {\n <p class=\"muted trace__summary\">{{ fmt(text.spansSummary, { count: number(tl.rows.length), duration: duration(tl.totalMs) }) }}</p>\n <!-- Linha do tempo: cada span \u00E9 uma linha (recuo = profundidade pela cadeia parent_span_id) com a\n barra posicionada no tempo, proporcional \u00E0 dura\u00E7\u00E3o. O texto da linha (e o aria-label) traz\n tudo o que a barra mostra. -->\n <ol class=\"spans\">\n @for (row of tl.rows; track row.event.event_id) {\n <li class=\"spans__item\" [class.spans__item--current]=\"row.event.event_id === ev.event_id\">\n <button type=\"button\" class=\"spans__link\"\n [attr.aria-current]=\"row.event.event_id === ev.event_id ? 'true' : null\"\n [attr.aria-label]=\"spanAria(row)\"\n (click)=\"openEvent(row.event, true)\">\n <span class=\"spans__head\" [style.padding-inline-start.rem]=\"row.depth * 1\">\n <span class=\"spans__app\">{{ row.event.app_name ?? '' }}</span>\n <code class=\"spans__op\">{{ operationName(row.event) }}</code>\n </span>\n <hm-tag class=\"spans__tag\" [name]=\"outcomeLabel(row.event.outcome)\" [color]=\"outcomeColor(row.event.outcome)\" />\n <span class=\"spans__dur\">{{ duration(row.event.duration_ms) }}</span>\n <span class=\"spans__track\" aria-hidden=\"true\">\n <span class=\"spans__bar spans__bar--{{ row.event.outcome }}\" [style.left.%]=\"row.left\" [style.width.%]=\"row.width\"></span>\n </span>\n </button>\n </li>\n }\n </ol>\n }\n </section>\n }\n </div>\n }\n </div>\n }\n</hm-drawer>\n", styles: [".card{border:1px solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface)}.card-block{padding:var(--hm-space-sm, 8px)}.muted{color:var(--hm-muted)}:host{display:block}.audit{display:flex;flex-direction:column;gap:var(--hm-space-md, 16px)}.audit__toolbar{display:flex;align-items:center;flex-wrap:wrap;gap:var(--hm-space-sm, 8px)}.audit__period-note{font-size:13px}.audit__toolbar-end{display:inline-flex;align-items:center;flex-wrap:wrap;gap:var(--hm-space-sm, 8px) var(--hm-space-md, 16px);margin-inline-start:auto}.audit__updated{font-size:12px;font-variant-numeric:tabular-nums}.audit__state{min-block-size:320px}.audit__section-title{margin:0 0 var(--hm-space-sm, 8px);font-size:14px;font-weight:600;color:var(--hm-text)}.audit__kpis{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:var(--hm-space-sm, 8px);align-items:stretch}.timeline{display:flex;align-items:flex-end;gap:2px;block-size:120px;margin:0;padding:0;list-style:none;border-block-end:1px solid var(--hm-border)}.timeline__bar{flex:1 1 0;min-inline-size:2px;block-size:100%;display:flex;flex-direction:column;justify-content:flex-end;border-radius:2px 2px 0 0;overflow:hidden}.timeline__bar:hover{background:color-mix(in srgb,var(--hm-text) 6%,transparent)}.timeline__seg{display:block;inline-size:100%}.timeline__seg--ok,.swatch--ok{background:color-mix(in srgb,var(--hm-action) 55%,transparent)}.timeline__seg--fail,.swatch--fail{background:var(--hm-danger)}.timeline__seg--denied,.swatch--denied{background:var(--hm-warning, #c98a00)}.timeline__axis{display:flex;justify-content:space-between;margin-block-start:4px;font-size:12px;color:var(--hm-muted)}.timeline__legend{display:flex;flex-wrap:wrap;gap:var(--hm-space-sm, 8px) var(--hm-space-md, 16px);margin:var(--hm-space-sm, 8px) 0 0;padding:0;list-style:none;font-size:12px;color:var(--hm-muted)}.timeline__legend li{display:inline-flex;align-items:center;gap:6px}.swatch{display:inline-block;inline-size:10px;block-size:10px;border-radius:2px}.audit__tops{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:var(--hm-space-sm, 8px);align-items:stretch}.audit__grid{position:relative;display:block}.audit__grid-progress{position:absolute;inset-inline:0;inset-block-start:0;z-index:2;pointer-events:none}.audit__grid--busy hm-data-grid{opacity:.6;transition:opacity .12s ease}.audit__op{font-family:var(--hm-font-mono, ui-monospace, monospace);font-size:12px}.audit__page-error{display:block}@media(prefers-reduced-motion:reduce){.audit__grid--busy hm-data-grid{transition:none}}:host-context([data-motion=reduce]) .audit__grid--busy hm-data-grid{transition:none}.detail{display:flex;flex-direction:column;gap:var(--hm-space-sm, 8px)}.detail__head{display:flex;align-items:center;gap:var(--hm-space-sm, 8px);flex-wrap:wrap}.detail__title{font-size:16px}.detail__list{display:grid;grid-template-columns:minmax(8rem,max-content) minmax(0,1fr);gap:6px var(--hm-space-md, 16px);margin:0}.detail__row{display:contents}.detail__row dt{color:var(--hm-muted);font-size:13px}.detail__row dd{margin:0;font-size:13px;overflow-wrap:anywhere}.detail__json{margin:0;padding:var(--hm-space-sm, 8px);max-block-size:280px;overflow:auto;border:1px solid var(--hm-border);border-radius:var(--hm-radius-control);background:color-mix(in srgb,var(--hm-text) 4%,transparent);font-size:12px}.detail__description{margin:0;font-size:13px}.detail__actions{display:flex;flex-wrap:wrap;gap:var(--hm-space-sm, 8px)}.trace{margin-block-start:var(--hm-space-sm, 8px)}.trace__hidden{margin-block-end:var(--hm-space-sm, 8px)}.detail__head,.detail__description,.detail__orphan{order:-3}.detail__chain{display:flex;flex-direction:column;gap:var(--hm-space-sm, 8px)}.detail__chain--first{order:-2}.trace__summary{margin:0 0 var(--hm-space-xs, 4px);font-size:13px}.spans{margin:0;padding:0;list-style:none;border-block-start:1px solid var(--hm-border)}.spans__item{border-block-end:1px solid var(--hm-border)}.spans__item--current .spans__link{background:color-mix(in srgb,var(--hm-action) 10%,transparent);box-shadow:inset 2px 0 0 var(--hm-action)}.spans__link{display:grid;grid-template-columns:minmax(0,1fr) auto auto;grid-template-areas:\"head tag dur\" \"track track track\";align-items:center;gap:4px var(--hm-space-sm, 8px);inline-size:100%;padding:6px 8px;border:0;background:transparent;color:var(--hm-text);font:inherit;font-size:13px;text-align:start;cursor:pointer}.spans__link:hover{background:color-mix(in srgb,var(--hm-action) 6%,transparent)}.spans__link:focus-visible{outline:2px solid var(--hm-action);outline-offset:-2px}.spans__head{grid-area:head;display:flex;flex-direction:column;min-inline-size:0}.spans__app{color:var(--hm-muted);font-size:12px}.spans__op{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:var(--hm-font-mono, ui-monospace, monospace);font-size:12px}.spans__tag{grid-area:tag}.spans__dur{grid-area:dur;min-inline-size:4.5rem;text-align:end;font-variant-numeric:tabular-nums;color:var(--hm-muted)}.spans__track{grid-area:track;position:relative;block-size:6px;border-radius:3px;background:color-mix(in srgb,var(--hm-text) 6%,transparent)}.spans__bar{position:absolute;inset-block:0;border-radius:3px;background:color-mix(in srgb,var(--hm-action) 70%,transparent)}.spans__bar--failure{background:var(--hm-danger)}.spans__bar--denied{background:var(--hm-warning, #c98a00)}.audit__spans{display:inline-flex;align-items:center;gap:6px;font-variant-numeric:tabular-nums}\n"], dependencies: [{ kind: "component", type: AlertComponent, selector: "hm-alert", inputs: ["variant", "appearance", "heading", "closable", "size", "showIcon", "link", "linkLabel", "linkTarget", "dismissKey", "duration"], outputs: ["closed"] }, { kind: "component", type: ButtonComponent, selector: "hm-button", inputs: ["emphasis", "intent", "size", "type", "icon", "iconDirection", "iconEnd", "disabled", "loading", "busy", "loadingState", "block", "link", "newTab", "caret", "inMenu", "toggle", "pressed"], outputs: ["loadingStateChange", "pressedChange", "clicked"] }, { kind: "directive", type: DataGridCellDirective, selector: "ng-template[hmCell]", inputs: ["hmCell"] }, { kind: "component", type: DataGridComponent, selector: "hm-data-grid", inputs: ["columns", "rows", "rowId", "caption", "rowActions", "loading", "error", "emptyMessage", "noResultsMessage", "filterable", "paginated", "pageState", "pagination", "totalItems", "sort", "selectable", "selectionMode", "selection", "rowSelectable", "rowSelectableReason", "rowDetailDisabled", "rowDetailHidden", "virtual", "rowHeight", "pinnedColumns", "columnState", "pageSizes", "fillHeight", "maxHeight", "bordered", "rowState", "rowClickable", "remoteSearch", "remoteSort", "itemsLabel", "pageSizeLabel", "exportable", "exportFileName", "groupBy", "aggregates", "aggregateFormatter", "persistKey", "persistStorage", "advancedFilter", "filterQuery", "columnManager", "columnOrder", "searchTerm"], outputs: ["pageStateChange", "sortChange", "selectionChange", "pinnedColumnsChange", "columnStateChange", "actionTriggered", "detailToggled", "rowClicked", "clearFiltersRequested", "filteredChange", "searchTermChanged", "groupByChange", "filterQueryChange", "columnOrderChange", "searchTermChange", "detailPaneToggled"] }, { kind: "component", type: DrawerComponent, selector: "hm-drawer", inputs: ["open", "heading", "size", "position", "pinned", "closeLabel", "closable", "beforeClose"], outputs: ["openChange", "closed"] }, { kind: "component", type: FilterBarComponent, selector: "hm-filter-bar", inputs: ["filters", "value", "label", "savedFilters", "storageKey", "storage", "applyDefault"], outputs: ["valueChange", "savedChange", "savedApplied"] }, { kind: "component", type: ProgressComponent, selector: "hm-progress", inputs: ["value", "max", "label", "labelHidden", "showValue", "indeterminate", "intent", "size", "hint"] }, { kind: "component", type: SegmentedControlComponent, selector: "hm-segmented-control", inputs: ["options", "value", "label", "size", "emphasis"], outputs: ["valueChange"] }, { kind: "component", type: SpineAuditTopCardComponent, selector: "spine-audit-top-card", inputs: ["heading", "rows", "total", "offset", "pageSize", "max", "loading", "failed"], outputs: ["picked", "offsetChange"] }, { kind: "component", type: StatCardComponent, selector: "hm-stat-card", inputs: ["icon", "label", "value", "intent", "fill", "description", "details", "trend", "hint", "size", "selectable", "selected", "loading"], outputs: ["selectedChange"] }, { kind: "component", type: StateScreenComponent, selector: "hm-state-screen", inputs: ["heading", "description", "situation", "action", "actionIcon"], outputs: ["actionTriggered"] }, { kind: "component", type: SwitchComponent, selector: "hm-switch", inputs: ["value", "label", "hint", "help", "labelPosition", "loading"], outputs: ["valueChange"] }, { kind: "component", type: TagComponent, selector: "hm-tag", inputs: ["name", "color", "compact"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
1336
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: SpineAuditComponent, isStandalone: true, selector: "spine-audit", inputs: { scope: { classPropertyName: "scope", publicName: "scope", isSignal: true, isRequired: true, transformFunction: null }, clientId: { classPropertyName: "clientId", publicName: "clientId", isSignal: true, isRequired: false, transformFunction: null }, adminApi: { classPropertyName: "adminApi", publicName: "adminApi", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"audit\" [attr.aria-busy]=\"loading() || pageLoading()\">\n <!-- Per\u00EDodo (escolha r\u00E1pida; as datas da faixa de filtros vencem) + atualizar. -->\n <div class=\"audit__toolbar\">\n <!-- Unidade: por a\u00E7\u00E3o (uma linha por trace, padr\u00E3o) ou por evento (cada registro). -->\n <hm-segmented-control\n [label]=\"text.unitLabel\"\n [options]=\"unitOptions\"\n [value]=\"unit()\"\n (valueChange)=\"setUnit($event)\"\n size=\"small\"\n emphasis=\"action\"\n />\n <hm-segmented-control\n [label]=\"text.period\"\n [options]=\"periodOptions\"\n [value]=\"period()\"\n (valueChange)=\"setPeriod($event)\"\n size=\"small\"\n emphasis=\"action\"\n />\n @if (customDates()) {\n <span class=\"muted audit__period-note\">{{ text.periodFromFilters }}</span>\n }\n <span class=\"audit__toolbar-end\">\n @if (lastUpdated(); as at) {\n <span class=\"muted audit__updated\" aria-live=\"polite\">{{ fmt(text.updatedAt, { time: clock(at) }) }}</span>\n }\n <hm-switch [label]=\"text.autoRefresh\" [value]=\"autoRefresh()\" (valueChange)=\"autoRefresh.set($event)\" />\n <hm-button emphasis=\"medium\" size=\"small\" icon=\"refresh\" [loading]=\"loading()\" (clicked)=\"reload()\">{{ text.refresh }}</hm-button>\n </span>\n </div>\n\n <hm-filter-bar\n class=\"audit__filters\"\n [label]=\"text.filters\"\n [filters]=\"filterFields()\"\n [value]=\"filterValue()\"\n (valueChange)=\"setFilters($event)\"\n [savedFilters]=\"true\"\n [storageKey]=\"storageKey()\"\n />\n\n @if (errorKind()) {\n <hm-state-screen\n class=\"audit__state\"\n [situation]=\"errorSituation()\"\n [heading]=\"text.errorTitle\"\n [description]=\"errorText()\"\n [action]=\"errorKind() === 'generic' ? text.retry : (hasFilters() ? text.clearFilters : '')\"\n actionIcon=\"refresh\"\n (actionTriggered)=\"errorKind() === 'generic' ? reload() : clearFilters()\"\n />\n } @else if (empty()) {\n <hm-state-screen\n class=\"audit__state\"\n situation=\"empty\"\n [heading]=\"unitText().emptyTitle\"\n [description]=\"text.emptyText\"\n [action]=\"hasFilters() ? text.clearFilters : ''\"\n (actionTriggered)=\"clearFilters()\"\n />\n } @else {\n <!-- KPIs: falhas/negados s\u00E3o filtros (altern\u00E2ncia). -->\n <section class=\"audit__kpis\" [attr.aria-label]=\"text.kpis\">\n <hm-stat-card\n icon=\"list\"\n [label]=\"unitText().kpiTotal\"\n [value]=\"number(stats()?.total ?? 0)\"\n [description]=\"unitText().kpiTotalDescription\"\n [loading]=\"loading() && !stats()\"\n />\n <hm-stat-card\n icon=\"error-standard\"\n [label]=\"text.kpiFailures\"\n [value]=\"number(stats()?.by_outcome?.failure ?? 0)\"\n [intent]=\"(stats()?.by_outcome?.failure ?? 0) > 0 ? 'danger' : 'neutral'\"\n [description]=\"fmt(text.kpiFilterHint, { share: share(stats()?.by_outcome?.failure ?? 0) })\"\n [loading]=\"loading() && !stats()\"\n selectable\n [selected]=\"selectedOutcome() === 'failure'\"\n (selectedChange)=\"toggleOutcome('failure', $event)\"\n />\n <hm-stat-card\n icon=\"ban\"\n [label]=\"text.kpiDenied\"\n [value]=\"number(stats()?.by_outcome?.denied ?? 0)\"\n [intent]=\"(stats()?.by_outcome?.denied ?? 0) > 0 ? 'warning' : 'neutral'\"\n [description]=\"fmt(text.kpiFilterHint, { share: share(stats()?.by_outcome?.denied ?? 0) })\"\n [loading]=\"loading() && !stats()\"\n selectable\n [selected]=\"selectedOutcome() === 'denied'\"\n (selectedChange)=\"toggleOutcome('denied', $event)\"\n />\n </section>\n\n <!-- Linha do tempo: barras empilhadas em CSS (sucesso embaixo, falha, negado). Cada barra tem\n o resumo em texto (title + aria-label); o gr\u00E1fico n\u00E3o \u00E9 a \u00FAnica forma de ler o n\u00FAmero. -->\n <section class=\"card card-block audit__timeline\" aria-labelledby=\"audit-timeline-title\">\n <h3 class=\"audit__section-title\" id=\"audit-timeline-title\">{{ text.timeline }}</h3>\n @if (hasTimeline()) {\n <ol class=\"timeline\" [attr.aria-label]=\"fmt(unitText().timelineSummary, { from: timelineEdges()?.from ?? '', to: timelineEdges()?.to ?? '' })\">\n @for (bar of timelineBars(); track bar.key) {\n <li class=\"timeline__bar\" [attr.title]=\"bar.summary\" [attr.aria-label]=\"bar.summary\">\n <span class=\"timeline__seg timeline__seg--denied\" [style.height.%]=\"bar.denied\"></span>\n <span class=\"timeline__seg timeline__seg--fail\" [style.height.%]=\"bar.fail\"></span>\n <span class=\"timeline__seg timeline__seg--ok\" [style.height.%]=\"bar.ok\"></span>\n </li>\n }\n </ol>\n <div class=\"timeline__axis\" aria-hidden=\"true\">\n <span>{{ timelineEdges()?.from }}</span>\n <span>{{ timelineEdges()?.to }}</span>\n </div>\n <ul class=\"timeline__legend\">\n <li><span class=\"swatch swatch--ok\"></span>{{ text.outcomeSuccess }}</li>\n <li><span class=\"swatch swatch--fail\"></span>{{ text.outcomeFailure }}</li>\n <li><span class=\"swatch swatch--denied\"></span>{{ text.outcomeDenied }}</li>\n </ul>\n } @else if (loading()) {\n <hm-skeleton shape=\"card\" [label]=\"text.loading\" />\n } @else {\n <p class=\"muted\">{{ unitText().timelineEmpty }}</p>\n }\n </section>\n\n <!-- Rankings paginados no servidor (tops/:kind). Clicar num item vira filtro. -->\n <div class=\"audit__tops\" [class.audit__tops--three]=\"multiApp()\">\n @if (multiApp()) {\n <spine-audit-top-card\n class=\"audit__top audit__top--apps\"\n [heading]=\"text.topApps\"\n [rows]=\"topAppRows()\"\n [total]=\"tops.apps().total\"\n [offset]=\"tops.apps().offset\"\n [max]=\"tops.apps().max\"\n [pageSize]=\"topPageSize\"\n [loading]=\"tops.apps().loading\"\n [failed]=\"tops.apps().failed\"\n (offsetChange)=\"loadTop('apps', $event)\"\n (picked)=\"pickTop('apps', $event)\"\n />\n }\n <spine-audit-top-card\n class=\"audit__top audit__top--operations\"\n [heading]=\"text.topOperations\"\n [rows]=\"topOperationRows()\"\n [total]=\"tops.operations().total\"\n [offset]=\"tops.operations().offset\"\n [max]=\"tops.operations().max\"\n [pageSize]=\"topPageSize\"\n [loading]=\"tops.operations().loading\"\n [failed]=\"tops.operations().failed\"\n (offsetChange)=\"loadTop('operations', $event)\"\n (picked)=\"pickTop('operations', $event)\"\n />\n <spine-audit-top-card\n class=\"audit__top audit__top--users\"\n [heading]=\"text.topUsers\"\n [rows]=\"topUserRows()\"\n [total]=\"tops.users().total\"\n [offset]=\"tops.users().offset\"\n [max]=\"tops.users().max\"\n [pageSize]=\"topPageSize\"\n [loading]=\"tops.users().loading\"\n [failed]=\"tops.users().failed\"\n (offsetChange)=\"loadTop('users', $event)\"\n (picked)=\"pickTop('users', $event)\"\n />\n </div>\n\n <!-- Tabela: pagina\u00E7\u00E3o no servidor. A grade n\u00E3o corta nada ([paginated]=false): mostra a p\u00E1gina que veio\n (limit = itens por p\u00E1gina) e o rodap\u00E9 usa o total do resumo (mesma consulta). Esqueleto s\u00F3 na 1\u00AA\n carga; nas seguintes as linhas ficam e o indicador aparece por cima (sem piscar). -->\n <div class=\"audit__grid\" [class.audit__grid--busy]=\"gridBusy()\" [attr.aria-busy]=\"gridBusy()\">\n @if (gridBusy()) {\n <hm-progress class=\"audit__grid-progress\" indeterminate [label]=\"text.updating\" labelHidden />\n }\n <hm-data-grid\n [caption]=\"unitText().caption\"\n [columns]=\"columns()\"\n [rows]=\"events()\"\n [rowId]=\"rowId\"\n [loading]=\"gridSkeleton()\"\n [filterable]=\"false\"\n [paginated]=\"false\"\n [totalItems]=\"total()\"\n [pageState]=\"pageState()\"\n (pageStateChange)=\"changePage($event)\"\n [pageSizes]=\"pageSizes\"\n [itemsLabel]=\"unitText().pageItems\"\n [pageSizeLabel]=\"text.pageSize\"\n remoteSort\n rowClickable\n (rowClicked)=\"openRow($event)\"\n >\n <ng-template hmCell=\"operation\" let-item>\n <span class=\"audit__op\" [attr.title]=\"item.operation_label || null\">{{ operationName(item) }}</span>\n </ng-template>\n <ng-template hmCell=\"spans\" let-item>\n <span class=\"audit__spans\" [attr.title]=\"spansHint(item) || null\">\n {{ number(item.span_count ?? 1) }}\n @if (item.orphan) {\n <hm-tag class=\"audit__orphan\" [name]=\"text.orphanTag\" color=\"neutral\" />\n }\n </span>\n </ng-template>\n <ng-template hmCell=\"outcome\" let-item>\n <hm-tag [name]=\"outcomeLabel(item.outcome)\" [color]=\"outcomeColor(item.outcome)\" />\n </ng-template>\n </hm-data-grid>\n </div>\n\n @if (pageFailed()) {\n <hm-alert class=\"audit__page-error\" variant=\"error\" size=\"small\">{{ unitText().pageError }}</hm-alert>\n }\n }\n</div>\n\n<!-- Detalhe do evento + a cadeia (trace). -->\n<hm-drawer [open]=\"detailOpen()\" (openChange)=\"onDetailOpenChange($event)\" [heading]=\"showingAction() ? text.actionDetailHeading : text.detailHeading\">\n @if (selected(); as ev) {\n <div class=\"detail\">\n <div class=\"detail__head\">\n <strong class=\"detail__title\">{{ operationName(ev) }}</strong>\n <hm-tag [name]=\"outcomeLabel(headOutcome())\" [color]=\"outcomeColor(headOutcome())\" />\n </div>\n @if (showingAction() && selectedAction()?.orphan) {\n <hm-alert class=\"detail__orphan\" variant=\"info\" size=\"small\">{{ text.orphanNote }}</hm-alert>\n }\n @if (ev.operation_label && ev.operation_identifier) {\n <p class=\"muted detail__description\">{{ ev.operation_label }}</p>\n }\n\n @if (detailLoading()) {\n <p class=\"muted\" role=\"status\">{{ text.detailLoading }}</p>\n }\n @if (detailFailed()) {\n <hm-alert variant=\"warning\" size=\"small\">{{ text.detailError }}</hm-alert>\n }\n\n <dl class=\"detail__list\">\n @for (field of detailFields(); track field.label) {\n <div class=\"detail__row\">\n <dt>{{ field.label }}</dt>\n <dd>\n @if (field.code) { <code>{{ field.value }}</code> } @else { {{ field.value }} }\n </dd>\n </div>\n }\n </dl>\n\n @if (detailsJson()) {\n <h3 class=\"audit__section-title\">{{ text.fieldDetails }}</h3>\n <pre class=\"detail__json\">{{ detailsJson() }}</pre>\n }\n\n <!-- Por a\u00E7\u00E3o, os spans sobem para logo abaixo do cabe\u00E7alho (order no CSS); por evento, ficam no fim. -->\n @if (ev.trace_id) {\n <div class=\"detail__chain\" [class.detail__chain--first]=\"showingAction()\">\n <div class=\"detail__actions\">\n @if (!trace() && !traceLoading()) {\n <hm-button emphasis=\"medium\" size=\"small\" icon=\"tree-view\" (clicked)=\"showTrace()\">{{ text.viewTrace }}</hm-button>\n }\n <hm-button emphasis=\"minimal\" size=\"small\" icon=\"filter\" (clicked)=\"filterByTrace(ev.trace_id)\">{{ text.filterByTrace }}</hm-button>\n </div>\n @if (traceLoading()) {\n <p class=\"muted\" role=\"status\">{{ text.traceLoading }}</p>\n }\n @if (traceFailed()) {\n <hm-alert variant=\"error\" size=\"small\">{{ text.traceError }}</hm-alert>\n }\n @if (trace(); as tr) {\n <section class=\"trace\" aria-labelledby=\"audit-trace-title\">\n <h3 class=\"audit__section-title\" id=\"audit-trace-title\">{{ showingAction() ? text.spansHeading : text.traceHeading }}</h3>\n @if (tr.hidden_count) {\n <hm-alert class=\"trace__hidden\" variant=\"info\" size=\"small\">{{ plural(text.traceHidden, tr.hidden_count) }}</hm-alert>\n }\n @if (!tr.items.length) {\n <p class=\"muted\">{{ text.traceEmpty }}</p>\n } @else if (spanTimeline(); as tl) {\n <p class=\"muted trace__summary\">{{ fmt(text.spansSummary, { count: number(tl.rows.length), duration: duration(tl.totalMs) }) }}</p>\n <!-- Linha do tempo: cada span \u00E9 uma linha (recuo = profundidade pela cadeia parent_span_id) com a\n barra posicionada no tempo, proporcional \u00E0 dura\u00E7\u00E3o. O texto da linha (e o aria-label) traz\n tudo o que a barra mostra. -->\n <ol class=\"spans\">\n @for (row of tl.rows; track row.event.event_id) {\n <li class=\"spans__item\" [class.spans__item--current]=\"row.event.event_id === ev.event_id\">\n <button type=\"button\" class=\"spans__link\"\n [attr.aria-current]=\"row.event.event_id === ev.event_id ? 'true' : null\"\n [attr.aria-label]=\"spanAria(row)\"\n (click)=\"openEvent(row.event, true)\">\n <span class=\"spans__head\" [style.padding-inline-start.rem]=\"row.depth * 1\">\n <span class=\"spans__app\">{{ row.event.app_name ?? '' }}</span>\n <code class=\"spans__op\">{{ operationName(row.event) }}</code>\n </span>\n <hm-tag class=\"spans__tag\" [name]=\"outcomeLabel(row.event.outcome)\" [color]=\"outcomeColor(row.event.outcome)\" />\n <span class=\"spans__dur\">{{ duration(row.event.duration_ms) }}</span>\n <span class=\"spans__track\" aria-hidden=\"true\">\n <span class=\"spans__bar spans__bar--{{ row.event.outcome }}\" [style.left.%]=\"row.left\" [style.width.%]=\"row.width\"></span>\n </span>\n </button>\n </li>\n }\n </ol>\n }\n </section>\n }\n </div>\n }\n </div>\n }\n</hm-drawer>\n", styles: [".card{border:1px solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface-raised)}.card-block{padding:var(--hm-space-sm, 8px)}.muted{color:var(--hm-muted)}:host{display:block}.audit{display:flex;flex-direction:column;gap:var(--hm-space-md, 16px)}.audit__toolbar{display:flex;align-items:center;flex-wrap:wrap;gap:var(--hm-space-sm, 8px)}.audit__period-note{font-size:13px}.audit__toolbar-end{display:inline-flex;align-items:center;flex-wrap:wrap;gap:var(--hm-space-sm, 8px) var(--hm-space-md, 16px);margin-inline-start:auto}.audit__updated{font-size:12px;font-variant-numeric:tabular-nums}.audit__state{min-block-size:320px}.audit__section-title{margin:0 0 var(--hm-space-sm, 8px);font-size:14px;font-weight:600;color:var(--hm-text)}.audit__kpis{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:var(--hm-space-sm, 8px);align-items:stretch}.timeline{display:flex;align-items:flex-end;gap:2px;block-size:120px;margin:0;padding:0;list-style:none;border-block-end:1px solid var(--hm-border)}.timeline__bar{flex:1 1 0;min-inline-size:2px;block-size:100%;display:flex;flex-direction:column;justify-content:flex-end;border-radius:2px 2px 0 0;overflow:hidden}.timeline__bar:hover{background:color-mix(in srgb,var(--hm-text) 6%,transparent)}.timeline__seg{display:block;inline-size:100%}.timeline__seg--ok,.swatch--ok{background:color-mix(in srgb,var(--hm-action) 55%,transparent)}.timeline__seg--fail,.swatch--fail{background:var(--hm-danger)}.timeline__seg--denied,.swatch--denied{background:var(--hm-warning, #c98a00)}.timeline__axis{display:flex;justify-content:space-between;margin-block-start:4px;font-size:12px;color:var(--hm-muted)}.timeline__legend{display:flex;flex-wrap:wrap;gap:var(--hm-space-sm, 8px) var(--hm-space-md, 16px);margin:var(--hm-space-sm, 8px) 0 0;padding:0;list-style:none;font-size:12px;color:var(--hm-muted)}.timeline__legend li{display:inline-flex;align-items:center;gap:6px}.swatch{display:inline-block;inline-size:10px;block-size:10px;border-radius:2px}.audit__tops{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:var(--hm-space-sm, 8px);align-items:stretch}.audit__grid{position:relative;display:block}.audit__grid-progress{position:absolute;inset-inline:0;inset-block-start:0;z-index:2;pointer-events:none}.audit__grid--busy hm-data-grid{opacity:.6;transition:opacity .12s ease}.audit__op{font-family:var(--hm-font-mono, ui-monospace, monospace);font-size:12px}.audit__page-error{display:block}@media(prefers-reduced-motion:reduce){.audit__grid--busy hm-data-grid{transition:none}}:host-context([data-motion=reduce]) .audit__grid--busy hm-data-grid{transition:none}.detail{display:flex;flex-direction:column;gap:var(--hm-space-sm, 8px)}.detail__head{display:flex;align-items:center;gap:var(--hm-space-sm, 8px);flex-wrap:wrap}.detail__title{font-size:16px}.detail__list{display:grid;grid-template-columns:minmax(8rem,max-content) minmax(0,1fr);gap:6px var(--hm-space-md, 16px);margin:0}.detail__row{display:contents}.detail__row dt{color:var(--hm-muted);font-size:13px}.detail__row dd{margin:0;font-size:13px;overflow-wrap:anywhere}.detail__json{margin:0;padding:var(--hm-space-sm, 8px);max-block-size:280px;overflow:auto;border:1px solid var(--hm-border);border-radius:var(--hm-radius-control);background:color-mix(in srgb,var(--hm-text) 4%,transparent);font-size:12px}.detail__description{margin:0;font-size:13px}.detail__actions{display:flex;flex-wrap:wrap;gap:var(--hm-space-sm, 8px)}.trace{margin-block-start:var(--hm-space-sm, 8px)}.trace__hidden{margin-block-end:var(--hm-space-sm, 8px)}.detail__head,.detail__description,.detail__orphan{order:-3}.detail__chain{display:flex;flex-direction:column;gap:var(--hm-space-sm, 8px)}.detail__chain--first{order:-2}.trace__summary{margin:0 0 var(--hm-space-xs, 4px);font-size:13px}.spans{margin:0;padding:0;list-style:none;border-block-start:1px solid var(--hm-border)}.spans__item{border-block-end:1px solid var(--hm-border)}.spans__item--current .spans__link{background:color-mix(in srgb,var(--hm-action) 10%,transparent);box-shadow:inset 2px 0 0 var(--hm-action)}.spans__link{display:grid;grid-template-columns:minmax(0,1fr) auto auto;grid-template-areas:\"head tag dur\" \"track track track\";align-items:center;gap:4px var(--hm-space-sm, 8px);inline-size:100%;padding:6px 8px;border:0;background:transparent;color:var(--hm-text);font:inherit;font-size:13px;text-align:start;cursor:pointer}.spans__link:hover{background:color-mix(in srgb,var(--hm-action) 6%,transparent)}.spans__link:focus-visible{outline:2px solid var(--hm-action);outline-offset:-2px}.spans__head{grid-area:head;display:flex;flex-direction:column;min-inline-size:0}.spans__app{color:var(--hm-muted);font-size:12px}.spans__op{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:var(--hm-font-mono, ui-monospace, monospace);font-size:12px}.spans__tag{grid-area:tag}.spans__dur{grid-area:dur;min-inline-size:4.5rem;text-align:end;font-variant-numeric:tabular-nums;color:var(--hm-muted)}.spans__track{grid-area:track;position:relative;block-size:6px;border-radius:3px;background:color-mix(in srgb,var(--hm-text) 6%,transparent)}.spans__bar{position:absolute;inset-block:0;border-radius:3px;background:color-mix(in srgb,var(--hm-action) 70%,transparent)}.spans__bar--failure{background:var(--hm-danger)}.spans__bar--denied{background:var(--hm-warning, #c98a00)}.audit__spans{display:inline-flex;align-items:center;gap:6px;font-variant-numeric:tabular-nums}\n"], dependencies: [{ kind: "component", type: AlertComponent, selector: "hm-alert", inputs: ["variant", "appearance", "heading", "closable", "size", "showIcon", "link", "linkLabel", "linkTarget", "dismissKey", "duration"], outputs: ["closed"] }, { kind: "component", type: ButtonComponent, selector: "hm-button", inputs: ["emphasis", "intent", "size", "type", "icon", "iconDirection", "iconEnd", "disabled", "loading", "loadingState", "block", "link", "newTab", "caret", "inMenu", "toggle", "pressed"], outputs: ["loadingStateChange", "pressedChange", "clicked"] }, { kind: "directive", type: DataGridCellDirective, selector: "ng-template[hmCell]", inputs: ["hmCell"] }, { kind: "component", type: DataGridComponent, selector: "hm-data-grid", inputs: ["columns", "rows", "rowId", "caption", "rowActions", "loading", "error", "emptyMessage", "noResultsMessage", "filterable", "paginated", "pageState", "pagination", "totalItems", "sort", "selectable", "selectionMode", "selection", "rowSelectable", "rowSelectableReason", "rowDetailDisabled", "rowDetailHidden", "virtual", "rowHeight", "pinnedColumns", "columnState", "pageSizes", "fillHeight", "maxHeight", "bordered", "rowState", "rowClickable", "remoteSearch", "remoteSort", "itemsLabel", "pageSizeLabel", "exportable", "exportFileName", "groupBy", "aggregates", "aggregateFormatter", "persistKey", "persistStorage", "advancedFilter", "filterQuery", "columnManager", "columnOrder", "searchTerm"], outputs: ["pageStateChange", "sortChange", "selectionChange", "pinnedColumnsChange", "columnStateChange", "actionTriggered", "detailToggled", "rowClicked", "clearFiltersRequested", "filteredChange", "searchTermChanged", "groupByChange", "filterQueryChange", "columnOrderChange", "searchTermChange", "detailPaneToggled"] }, { kind: "component", type: DrawerComponent, selector: "hm-drawer", inputs: ["open", "heading", "size", "position", "pinned", "closeLabel", "closable", "beforeClose"], outputs: ["openChange", "closed"] }, { kind: "component", type: FilterBarComponent, selector: "hm-filter-bar", inputs: ["filters", "value", "label", "savedFilters", "storageKey", "storage", "applyDefault"], outputs: ["valueChange", "savedChange", "savedApplied"] }, { kind: "component", type: ProgressComponent, selector: "hm-progress", inputs: ["value", "max", "label", "labelHidden", "showValue", "indeterminate", "intent", "size", "hint"] }, { kind: "component", type: SegmentedControlComponent, selector: "hm-segmented-control", inputs: ["options", "value", "label", "size", "emphasis"], outputs: ["valueChange"] }, { kind: "component", type: SkeletonComponent, selector: "hm-skeleton", inputs: ["shape", "count", "label"] }, { kind: "component", type: SpineAuditTopCardComponent, selector: "spine-audit-top-card", inputs: ["heading", "rows", "total", "offset", "pageSize", "max", "loading", "failed"], outputs: ["picked", "offsetChange"] }, { kind: "component", type: StatCardComponent, selector: "hm-stat-card", inputs: ["icon", "label", "value", "intent", "fill", "description", "details", "trend", "hint", "size", "selectable", "selected", "loading"], outputs: ["selectedChange"] }, { kind: "component", type: StateScreenComponent, selector: "hm-state-screen", inputs: ["heading", "description", "situation", "action", "actionIcon"], outputs: ["actionTriggered"] }, { kind: "component", type: SwitchComponent, selector: "hm-switch", inputs: ["value", "label", "hint", "labelPosition", "loading"], outputs: ["valueChange"] }, { kind: "component", type: TagComponent, selector: "hm-tag", inputs: ["name", "color", "compact"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
1333
1337
  }
1334
1338
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SpineAuditComponent, decorators: [{
1335
1339
  type: Component,
1336
1340
  args: [{ selector: 'spine-audit', imports: [
1337
1341
  AlertComponent, ButtonComponent, DataGridCellDirective, DataGridComponent, DrawerComponent, FilterBarComponent,
1338
- ProgressComponent, SegmentedControlComponent, SpineAuditTopCardComponent, StatCardComponent, StateScreenComponent,
1342
+ ProgressComponent, SegmentedControlComponent, SkeletonComponent, SpineAuditTopCardComponent, StatCardComponent, StateScreenComponent,
1339
1343
  SwitchComponent, TagComponent,
1340
- ], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"audit\" [attr.aria-busy]=\"loading() || pageLoading()\">\n <!-- Per\u00EDodo (escolha r\u00E1pida; as datas da faixa de filtros vencem) + atualizar. -->\n <div class=\"audit__toolbar\">\n <!-- Unidade: por a\u00E7\u00E3o (uma linha por trace, padr\u00E3o) ou por evento (cada registro). -->\n <hm-segmented-control\n [label]=\"text.unitLabel\"\n [options]=\"unitOptions\"\n [value]=\"unit()\"\n (valueChange)=\"setUnit($event)\"\n size=\"small\"\n emphasis=\"action\"\n />\n <hm-segmented-control\n [label]=\"text.period\"\n [options]=\"periodOptions\"\n [value]=\"period()\"\n (valueChange)=\"setPeriod($event)\"\n size=\"small\"\n emphasis=\"action\"\n />\n @if (customDates()) {\n <span class=\"muted audit__period-note\">{{ text.periodFromFilters }}</span>\n }\n <span class=\"audit__toolbar-end\">\n @if (lastUpdated(); as at) {\n <span class=\"muted audit__updated\" aria-live=\"polite\">{{ fmt(text.updatedAt, { time: clock(at) }) }}</span>\n }\n <hm-switch [label]=\"text.autoRefresh\" [value]=\"autoRefresh()\" (valueChange)=\"autoRefresh.set($event)\" />\n <hm-button emphasis=\"medium\" size=\"small\" icon=\"refresh\" [loading]=\"loading()\" (clicked)=\"reload()\">{{ text.refresh }}</hm-button>\n </span>\n </div>\n\n <hm-filter-bar\n class=\"audit__filters\"\n [label]=\"text.filters\"\n [filters]=\"filterFields()\"\n [value]=\"filterValue()\"\n (valueChange)=\"setFilters($event)\"\n [savedFilters]=\"true\"\n [storageKey]=\"storageKey()\"\n />\n\n @if (errorKind()) {\n <hm-state-screen\n class=\"audit__state\"\n [situation]=\"errorSituation()\"\n [heading]=\"text.errorTitle\"\n [description]=\"errorText()\"\n [action]=\"errorKind() === 'generic' ? text.retry : (hasFilters() ? text.clearFilters : '')\"\n actionIcon=\"refresh\"\n (actionTriggered)=\"errorKind() === 'generic' ? reload() : clearFilters()\"\n />\n } @else if (empty()) {\n <hm-state-screen\n class=\"audit__state\"\n situation=\"empty\"\n [heading]=\"unitText().emptyTitle\"\n [description]=\"text.emptyText\"\n [action]=\"hasFilters() ? text.clearFilters : ''\"\n (actionTriggered)=\"clearFilters()\"\n />\n } @else {\n <!-- KPIs: falhas/negados s\u00E3o filtros (altern\u00E2ncia). -->\n <section class=\"audit__kpis\" [attr.aria-label]=\"text.kpis\">\n <hm-stat-card\n icon=\"list\"\n [label]=\"unitText().kpiTotal\"\n [value]=\"number(stats()?.total ?? 0)\"\n [description]=\"unitText().kpiTotalDescription\"\n [loading]=\"loading() && !stats()\"\n />\n <hm-stat-card\n icon=\"error-standard\"\n [label]=\"text.kpiFailures\"\n [value]=\"number(stats()?.by_outcome?.failure ?? 0)\"\n [intent]=\"(stats()?.by_outcome?.failure ?? 0) > 0 ? 'danger' : 'neutral'\"\n [description]=\"fmt(text.kpiFilterHint, { share: share(stats()?.by_outcome?.failure ?? 0) })\"\n [loading]=\"loading() && !stats()\"\n selectable\n [selected]=\"selectedOutcome() === 'failure'\"\n (selectedChange)=\"toggleOutcome('failure', $event)\"\n />\n <hm-stat-card\n icon=\"ban\"\n [label]=\"text.kpiDenied\"\n [value]=\"number(stats()?.by_outcome?.denied ?? 0)\"\n [intent]=\"(stats()?.by_outcome?.denied ?? 0) > 0 ? 'warning' : 'neutral'\"\n [description]=\"fmt(text.kpiFilterHint, { share: share(stats()?.by_outcome?.denied ?? 0) })\"\n [loading]=\"loading() && !stats()\"\n selectable\n [selected]=\"selectedOutcome() === 'denied'\"\n (selectedChange)=\"toggleOutcome('denied', $event)\"\n />\n </section>\n\n <!-- Linha do tempo: barras empilhadas em CSS (sucesso embaixo, falha, negado). Cada barra tem\n o resumo em texto (title + aria-label); o gr\u00E1fico n\u00E3o \u00E9 a \u00FAnica forma de ler o n\u00FAmero. -->\n <section class=\"card card-block audit__timeline\" aria-labelledby=\"audit-timeline-title\">\n <h3 class=\"audit__section-title\" id=\"audit-timeline-title\">{{ text.timeline }}</h3>\n @if (hasTimeline()) {\n <ol class=\"timeline\" [attr.aria-label]=\"fmt(unitText().timelineSummary, { from: timelineEdges()?.from ?? '', to: timelineEdges()?.to ?? '' })\">\n @for (bar of timelineBars(); track bar.key) {\n <li class=\"timeline__bar\" [attr.title]=\"bar.summary\" [attr.aria-label]=\"bar.summary\">\n <span class=\"timeline__seg timeline__seg--denied\" [style.height.%]=\"bar.denied\"></span>\n <span class=\"timeline__seg timeline__seg--fail\" [style.height.%]=\"bar.fail\"></span>\n <span class=\"timeline__seg timeline__seg--ok\" [style.height.%]=\"bar.ok\"></span>\n </li>\n }\n </ol>\n <div class=\"timeline__axis\" aria-hidden=\"true\">\n <span>{{ timelineEdges()?.from }}</span>\n <span>{{ timelineEdges()?.to }}</span>\n </div>\n <ul class=\"timeline__legend\">\n <li><span class=\"swatch swatch--ok\"></span>{{ text.outcomeSuccess }}</li>\n <li><span class=\"swatch swatch--fail\"></span>{{ text.outcomeFailure }}</li>\n <li><span class=\"swatch swatch--denied\"></span>{{ text.outcomeDenied }}</li>\n </ul>\n } @else {\n <p class=\"muted\">{{ loading() ? text.loading : unitText().timelineEmpty }}</p>\n }\n </section>\n\n <!-- Rankings paginados no servidor (tops/:kind). Clicar num item vira filtro. -->\n <div class=\"audit__tops\" [class.audit__tops--three]=\"multiApp()\">\n @if (multiApp()) {\n <spine-audit-top-card\n class=\"audit__top audit__top--apps\"\n [heading]=\"text.topApps\"\n [rows]=\"topAppRows()\"\n [total]=\"tops.apps().total\"\n [offset]=\"tops.apps().offset\"\n [max]=\"tops.apps().max\"\n [pageSize]=\"topPageSize\"\n [loading]=\"tops.apps().loading\"\n [failed]=\"tops.apps().failed\"\n (offsetChange)=\"loadTop('apps', $event)\"\n (picked)=\"pickTop('apps', $event)\"\n />\n }\n <spine-audit-top-card\n class=\"audit__top audit__top--operations\"\n [heading]=\"text.topOperations\"\n [rows]=\"topOperationRows()\"\n [total]=\"tops.operations().total\"\n [offset]=\"tops.operations().offset\"\n [max]=\"tops.operations().max\"\n [pageSize]=\"topPageSize\"\n [loading]=\"tops.operations().loading\"\n [failed]=\"tops.operations().failed\"\n (offsetChange)=\"loadTop('operations', $event)\"\n (picked)=\"pickTop('operations', $event)\"\n />\n <spine-audit-top-card\n class=\"audit__top audit__top--users\"\n [heading]=\"text.topUsers\"\n [rows]=\"topUserRows()\"\n [total]=\"tops.users().total\"\n [offset]=\"tops.users().offset\"\n [max]=\"tops.users().max\"\n [pageSize]=\"topPageSize\"\n [loading]=\"tops.users().loading\"\n [failed]=\"tops.users().failed\"\n (offsetChange)=\"loadTop('users', $event)\"\n (picked)=\"pickTop('users', $event)\"\n />\n </div>\n\n <!-- Tabela: pagina\u00E7\u00E3o no servidor. A grade n\u00E3o corta nada ([paginated]=false): mostra a p\u00E1gina que veio\n (limit = itens por p\u00E1gina) e o rodap\u00E9 usa o total do resumo (mesma consulta). Esqueleto s\u00F3 na 1\u00AA\n carga; nas seguintes as linhas ficam e o indicador aparece por cima (sem piscar). -->\n <div class=\"audit__grid\" [class.audit__grid--busy]=\"gridBusy()\" [attr.aria-busy]=\"gridBusy()\">\n @if (gridBusy()) {\n <hm-progress class=\"audit__grid-progress\" indeterminate [label]=\"text.updating\" labelHidden />\n }\n <hm-data-grid\n [caption]=\"unitText().caption\"\n [columns]=\"columns()\"\n [rows]=\"events()\"\n [rowId]=\"rowId\"\n [loading]=\"gridSkeleton()\"\n [filterable]=\"false\"\n [paginated]=\"false\"\n [totalItems]=\"total()\"\n [pageState]=\"pageState()\"\n (pageStateChange)=\"changePage($event)\"\n [pageSizes]=\"pageSizes\"\n [itemsLabel]=\"unitText().pageItems\"\n [pageSizeLabel]=\"text.pageSize\"\n remoteSort\n rowClickable\n (rowClicked)=\"openRow($event)\"\n >\n <ng-template hmCell=\"operation\" let-item>\n <span class=\"audit__op\" [attr.title]=\"item.operation_label || null\">{{ operationName(item) }}</span>\n </ng-template>\n <ng-template hmCell=\"spans\" let-item>\n <span class=\"audit__spans\" [attr.title]=\"spansHint(item) || null\">\n {{ number(item.span_count ?? 1) }}\n @if (item.orphan) {\n <hm-tag class=\"audit__orphan\" [name]=\"text.orphanTag\" color=\"neutral\" />\n }\n </span>\n </ng-template>\n <ng-template hmCell=\"outcome\" let-item>\n <hm-tag [name]=\"outcomeLabel(item.outcome)\" [color]=\"outcomeColor(item.outcome)\" />\n </ng-template>\n </hm-data-grid>\n </div>\n\n @if (pageFailed()) {\n <hm-alert class=\"audit__page-error\" variant=\"error\" size=\"small\">{{ unitText().pageError }}</hm-alert>\n }\n }\n</div>\n\n<!-- Detalhe do evento + a cadeia (trace). -->\n<hm-drawer [open]=\"detailOpen()\" (openChange)=\"onDetailOpenChange($event)\" [heading]=\"showingAction() ? text.actionDetailHeading : text.detailHeading\">\n @if (selected(); as ev) {\n <div class=\"detail\">\n <div class=\"detail__head\">\n <strong class=\"detail__title\">{{ operationName(ev) }}</strong>\n <hm-tag [name]=\"outcomeLabel(headOutcome())\" [color]=\"outcomeColor(headOutcome())\" />\n </div>\n @if (showingAction() && selectedAction()?.orphan) {\n <hm-alert class=\"detail__orphan\" variant=\"info\" size=\"small\">{{ text.orphanNote }}</hm-alert>\n }\n @if (ev.operation_label && ev.operation_identifier) {\n <p class=\"muted detail__description\">{{ ev.operation_label }}</p>\n }\n\n @if (detailLoading()) {\n <p class=\"muted\" role=\"status\">{{ text.detailLoading }}</p>\n }\n @if (detailFailed()) {\n <hm-alert variant=\"warning\" size=\"small\">{{ text.detailError }}</hm-alert>\n }\n\n <dl class=\"detail__list\">\n @for (field of detailFields(); track field.label) {\n <div class=\"detail__row\">\n <dt>{{ field.label }}</dt>\n <dd>\n @if (field.code) { <code>{{ field.value }}</code> } @else { {{ field.value }} }\n </dd>\n </div>\n }\n </dl>\n\n @if (detailsJson()) {\n <h3 class=\"audit__section-title\">{{ text.fieldDetails }}</h3>\n <pre class=\"detail__json\">{{ detailsJson() }}</pre>\n }\n\n <!-- Por a\u00E7\u00E3o, os spans sobem para logo abaixo do cabe\u00E7alho (order no CSS); por evento, ficam no fim. -->\n @if (ev.trace_id) {\n <div class=\"detail__chain\" [class.detail__chain--first]=\"showingAction()\">\n <div class=\"detail__actions\">\n @if (!trace() && !traceLoading()) {\n <hm-button emphasis=\"medium\" size=\"small\" icon=\"tree-view\" (clicked)=\"showTrace()\">{{ text.viewTrace }}</hm-button>\n }\n <hm-button emphasis=\"minimal\" size=\"small\" icon=\"filter\" (clicked)=\"filterByTrace(ev.trace_id)\">{{ text.filterByTrace }}</hm-button>\n </div>\n @if (traceLoading()) {\n <p class=\"muted\" role=\"status\">{{ text.traceLoading }}</p>\n }\n @if (traceFailed()) {\n <hm-alert variant=\"error\" size=\"small\">{{ text.traceError }}</hm-alert>\n }\n @if (trace(); as tr) {\n <section class=\"trace\" aria-labelledby=\"audit-trace-title\">\n <h3 class=\"audit__section-title\" id=\"audit-trace-title\">{{ showingAction() ? text.spansHeading : text.traceHeading }}</h3>\n @if (tr.hidden_count) {\n <hm-alert class=\"trace__hidden\" variant=\"info\" size=\"small\">{{ plural(text.traceHidden, tr.hidden_count) }}</hm-alert>\n }\n @if (!tr.items.length) {\n <p class=\"muted\">{{ text.traceEmpty }}</p>\n } @else if (spanTimeline(); as tl) {\n <p class=\"muted trace__summary\">{{ fmt(text.spansSummary, { count: number(tl.rows.length), duration: duration(tl.totalMs) }) }}</p>\n <!-- Linha do tempo: cada span \u00E9 uma linha (recuo = profundidade pela cadeia parent_span_id) com a\n barra posicionada no tempo, proporcional \u00E0 dura\u00E7\u00E3o. O texto da linha (e o aria-label) traz\n tudo o que a barra mostra. -->\n <ol class=\"spans\">\n @for (row of tl.rows; track row.event.event_id) {\n <li class=\"spans__item\" [class.spans__item--current]=\"row.event.event_id === ev.event_id\">\n <button type=\"button\" class=\"spans__link\"\n [attr.aria-current]=\"row.event.event_id === ev.event_id ? 'true' : null\"\n [attr.aria-label]=\"spanAria(row)\"\n (click)=\"openEvent(row.event, true)\">\n <span class=\"spans__head\" [style.padding-inline-start.rem]=\"row.depth * 1\">\n <span class=\"spans__app\">{{ row.event.app_name ?? '' }}</span>\n <code class=\"spans__op\">{{ operationName(row.event) }}</code>\n </span>\n <hm-tag class=\"spans__tag\" [name]=\"outcomeLabel(row.event.outcome)\" [color]=\"outcomeColor(row.event.outcome)\" />\n <span class=\"spans__dur\">{{ duration(row.event.duration_ms) }}</span>\n <span class=\"spans__track\" aria-hidden=\"true\">\n <span class=\"spans__bar spans__bar--{{ row.event.outcome }}\" [style.left.%]=\"row.left\" [style.width.%]=\"row.width\"></span>\n </span>\n </button>\n </li>\n }\n </ol>\n }\n </section>\n }\n </div>\n }\n </div>\n }\n</hm-drawer>\n", styles: [".card{border:1px solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface)}.card-block{padding:var(--hm-space-sm, 8px)}.muted{color:var(--hm-muted)}:host{display:block}.audit{display:flex;flex-direction:column;gap:var(--hm-space-md, 16px)}.audit__toolbar{display:flex;align-items:center;flex-wrap:wrap;gap:var(--hm-space-sm, 8px)}.audit__period-note{font-size:13px}.audit__toolbar-end{display:inline-flex;align-items:center;flex-wrap:wrap;gap:var(--hm-space-sm, 8px) var(--hm-space-md, 16px);margin-inline-start:auto}.audit__updated{font-size:12px;font-variant-numeric:tabular-nums}.audit__state{min-block-size:320px}.audit__section-title{margin:0 0 var(--hm-space-sm, 8px);font-size:14px;font-weight:600;color:var(--hm-text)}.audit__kpis{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:var(--hm-space-sm, 8px);align-items:stretch}.timeline{display:flex;align-items:flex-end;gap:2px;block-size:120px;margin:0;padding:0;list-style:none;border-block-end:1px solid var(--hm-border)}.timeline__bar{flex:1 1 0;min-inline-size:2px;block-size:100%;display:flex;flex-direction:column;justify-content:flex-end;border-radius:2px 2px 0 0;overflow:hidden}.timeline__bar:hover{background:color-mix(in srgb,var(--hm-text) 6%,transparent)}.timeline__seg{display:block;inline-size:100%}.timeline__seg--ok,.swatch--ok{background:color-mix(in srgb,var(--hm-action) 55%,transparent)}.timeline__seg--fail,.swatch--fail{background:var(--hm-danger)}.timeline__seg--denied,.swatch--denied{background:var(--hm-warning, #c98a00)}.timeline__axis{display:flex;justify-content:space-between;margin-block-start:4px;font-size:12px;color:var(--hm-muted)}.timeline__legend{display:flex;flex-wrap:wrap;gap:var(--hm-space-sm, 8px) var(--hm-space-md, 16px);margin:var(--hm-space-sm, 8px) 0 0;padding:0;list-style:none;font-size:12px;color:var(--hm-muted)}.timeline__legend li{display:inline-flex;align-items:center;gap:6px}.swatch{display:inline-block;inline-size:10px;block-size:10px;border-radius:2px}.audit__tops{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:var(--hm-space-sm, 8px);align-items:stretch}.audit__grid{position:relative;display:block}.audit__grid-progress{position:absolute;inset-inline:0;inset-block-start:0;z-index:2;pointer-events:none}.audit__grid--busy hm-data-grid{opacity:.6;transition:opacity .12s ease}.audit__op{font-family:var(--hm-font-mono, ui-monospace, monospace);font-size:12px}.audit__page-error{display:block}@media(prefers-reduced-motion:reduce){.audit__grid--busy hm-data-grid{transition:none}}:host-context([data-motion=reduce]) .audit__grid--busy hm-data-grid{transition:none}.detail{display:flex;flex-direction:column;gap:var(--hm-space-sm, 8px)}.detail__head{display:flex;align-items:center;gap:var(--hm-space-sm, 8px);flex-wrap:wrap}.detail__title{font-size:16px}.detail__list{display:grid;grid-template-columns:minmax(8rem,max-content) minmax(0,1fr);gap:6px var(--hm-space-md, 16px);margin:0}.detail__row{display:contents}.detail__row dt{color:var(--hm-muted);font-size:13px}.detail__row dd{margin:0;font-size:13px;overflow-wrap:anywhere}.detail__json{margin:0;padding:var(--hm-space-sm, 8px);max-block-size:280px;overflow:auto;border:1px solid var(--hm-border);border-radius:var(--hm-radius-control);background:color-mix(in srgb,var(--hm-text) 4%,transparent);font-size:12px}.detail__description{margin:0;font-size:13px}.detail__actions{display:flex;flex-wrap:wrap;gap:var(--hm-space-sm, 8px)}.trace{margin-block-start:var(--hm-space-sm, 8px)}.trace__hidden{margin-block-end:var(--hm-space-sm, 8px)}.detail__head,.detail__description,.detail__orphan{order:-3}.detail__chain{display:flex;flex-direction:column;gap:var(--hm-space-sm, 8px)}.detail__chain--first{order:-2}.trace__summary{margin:0 0 var(--hm-space-xs, 4px);font-size:13px}.spans{margin:0;padding:0;list-style:none;border-block-start:1px solid var(--hm-border)}.spans__item{border-block-end:1px solid var(--hm-border)}.spans__item--current .spans__link{background:color-mix(in srgb,var(--hm-action) 10%,transparent);box-shadow:inset 2px 0 0 var(--hm-action)}.spans__link{display:grid;grid-template-columns:minmax(0,1fr) auto auto;grid-template-areas:\"head tag dur\" \"track track track\";align-items:center;gap:4px var(--hm-space-sm, 8px);inline-size:100%;padding:6px 8px;border:0;background:transparent;color:var(--hm-text);font:inherit;font-size:13px;text-align:start;cursor:pointer}.spans__link:hover{background:color-mix(in srgb,var(--hm-action) 6%,transparent)}.spans__link:focus-visible{outline:2px solid var(--hm-action);outline-offset:-2px}.spans__head{grid-area:head;display:flex;flex-direction:column;min-inline-size:0}.spans__app{color:var(--hm-muted);font-size:12px}.spans__op{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:var(--hm-font-mono, ui-monospace, monospace);font-size:12px}.spans__tag{grid-area:tag}.spans__dur{grid-area:dur;min-inline-size:4.5rem;text-align:end;font-variant-numeric:tabular-nums;color:var(--hm-muted)}.spans__track{grid-area:track;position:relative;block-size:6px;border-radius:3px;background:color-mix(in srgb,var(--hm-text) 6%,transparent)}.spans__bar{position:absolute;inset-block:0;border-radius:3px;background:color-mix(in srgb,var(--hm-action) 70%,transparent)}.spans__bar--failure{background:var(--hm-danger)}.spans__bar--denied{background:var(--hm-warning, #c98a00)}.audit__spans{display:inline-flex;align-items:center;gap:6px;font-variant-numeric:tabular-nums}\n"] }]
1344
+ ], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"audit\" [attr.aria-busy]=\"loading() || pageLoading()\">\n <!-- Per\u00EDodo (escolha r\u00E1pida; as datas da faixa de filtros vencem) + atualizar. -->\n <div class=\"audit__toolbar\">\n <!-- Unidade: por a\u00E7\u00E3o (uma linha por trace, padr\u00E3o) ou por evento (cada registro). -->\n <hm-segmented-control\n [label]=\"text.unitLabel\"\n [options]=\"unitOptions\"\n [value]=\"unit()\"\n (valueChange)=\"setUnit($event)\"\n size=\"small\"\n emphasis=\"action\"\n />\n <hm-segmented-control\n [label]=\"text.period\"\n [options]=\"periodOptions\"\n [value]=\"period()\"\n (valueChange)=\"setPeriod($event)\"\n size=\"small\"\n emphasis=\"action\"\n />\n @if (customDates()) {\n <span class=\"muted audit__period-note\">{{ text.periodFromFilters }}</span>\n }\n <span class=\"audit__toolbar-end\">\n @if (lastUpdated(); as at) {\n <span class=\"muted audit__updated\" aria-live=\"polite\">{{ fmt(text.updatedAt, { time: clock(at) }) }}</span>\n }\n <hm-switch [label]=\"text.autoRefresh\" [value]=\"autoRefresh()\" (valueChange)=\"autoRefresh.set($event)\" />\n <hm-button emphasis=\"medium\" size=\"small\" icon=\"refresh\" [loading]=\"loading()\" (clicked)=\"reload()\">{{ text.refresh }}</hm-button>\n </span>\n </div>\n\n <hm-filter-bar\n class=\"audit__filters\"\n [label]=\"text.filters\"\n [filters]=\"filterFields()\"\n [value]=\"filterValue()\"\n (valueChange)=\"setFilters($event)\"\n [savedFilters]=\"true\"\n [storageKey]=\"storageKey()\"\n />\n\n @if (errorKind()) {\n <hm-state-screen\n class=\"audit__state\"\n [situation]=\"errorSituation()\"\n [heading]=\"text.errorTitle\"\n [description]=\"errorText()\"\n [action]=\"errorKind() === 'generic' ? text.retry : (hasFilters() ? text.clearFilters : '')\"\n actionIcon=\"refresh\"\n (actionTriggered)=\"errorKind() === 'generic' ? reload() : clearFilters()\"\n />\n } @else if (empty()) {\n <hm-state-screen\n class=\"audit__state\"\n situation=\"empty\"\n [heading]=\"unitText().emptyTitle\"\n [description]=\"text.emptyText\"\n [action]=\"hasFilters() ? text.clearFilters : ''\"\n (actionTriggered)=\"clearFilters()\"\n />\n } @else {\n <!-- KPIs: falhas/negados s\u00E3o filtros (altern\u00E2ncia). -->\n <section class=\"audit__kpis\" [attr.aria-label]=\"text.kpis\">\n <hm-stat-card\n icon=\"list\"\n [label]=\"unitText().kpiTotal\"\n [value]=\"number(stats()?.total ?? 0)\"\n [description]=\"unitText().kpiTotalDescription\"\n [loading]=\"loading() && !stats()\"\n />\n <hm-stat-card\n icon=\"error-standard\"\n [label]=\"text.kpiFailures\"\n [value]=\"number(stats()?.by_outcome?.failure ?? 0)\"\n [intent]=\"(stats()?.by_outcome?.failure ?? 0) > 0 ? 'danger' : 'neutral'\"\n [description]=\"fmt(text.kpiFilterHint, { share: share(stats()?.by_outcome?.failure ?? 0) })\"\n [loading]=\"loading() && !stats()\"\n selectable\n [selected]=\"selectedOutcome() === 'failure'\"\n (selectedChange)=\"toggleOutcome('failure', $event)\"\n />\n <hm-stat-card\n icon=\"ban\"\n [label]=\"text.kpiDenied\"\n [value]=\"number(stats()?.by_outcome?.denied ?? 0)\"\n [intent]=\"(stats()?.by_outcome?.denied ?? 0) > 0 ? 'warning' : 'neutral'\"\n [description]=\"fmt(text.kpiFilterHint, { share: share(stats()?.by_outcome?.denied ?? 0) })\"\n [loading]=\"loading() && !stats()\"\n selectable\n [selected]=\"selectedOutcome() === 'denied'\"\n (selectedChange)=\"toggleOutcome('denied', $event)\"\n />\n </section>\n\n <!-- Linha do tempo: barras empilhadas em CSS (sucesso embaixo, falha, negado). Cada barra tem\n o resumo em texto (title + aria-label); o gr\u00E1fico n\u00E3o \u00E9 a \u00FAnica forma de ler o n\u00FAmero. -->\n <section class=\"card card-block audit__timeline\" aria-labelledby=\"audit-timeline-title\">\n <h3 class=\"audit__section-title\" id=\"audit-timeline-title\">{{ text.timeline }}</h3>\n @if (hasTimeline()) {\n <ol class=\"timeline\" [attr.aria-label]=\"fmt(unitText().timelineSummary, { from: timelineEdges()?.from ?? '', to: timelineEdges()?.to ?? '' })\">\n @for (bar of timelineBars(); track bar.key) {\n <li class=\"timeline__bar\" [attr.title]=\"bar.summary\" [attr.aria-label]=\"bar.summary\">\n <span class=\"timeline__seg timeline__seg--denied\" [style.height.%]=\"bar.denied\"></span>\n <span class=\"timeline__seg timeline__seg--fail\" [style.height.%]=\"bar.fail\"></span>\n <span class=\"timeline__seg timeline__seg--ok\" [style.height.%]=\"bar.ok\"></span>\n </li>\n }\n </ol>\n <div class=\"timeline__axis\" aria-hidden=\"true\">\n <span>{{ timelineEdges()?.from }}</span>\n <span>{{ timelineEdges()?.to }}</span>\n </div>\n <ul class=\"timeline__legend\">\n <li><span class=\"swatch swatch--ok\"></span>{{ text.outcomeSuccess }}</li>\n <li><span class=\"swatch swatch--fail\"></span>{{ text.outcomeFailure }}</li>\n <li><span class=\"swatch swatch--denied\"></span>{{ text.outcomeDenied }}</li>\n </ul>\n } @else if (loading()) {\n <hm-skeleton shape=\"card\" [label]=\"text.loading\" />\n } @else {\n <p class=\"muted\">{{ unitText().timelineEmpty }}</p>\n }\n </section>\n\n <!-- Rankings paginados no servidor (tops/:kind). Clicar num item vira filtro. -->\n <div class=\"audit__tops\" [class.audit__tops--three]=\"multiApp()\">\n @if (multiApp()) {\n <spine-audit-top-card\n class=\"audit__top audit__top--apps\"\n [heading]=\"text.topApps\"\n [rows]=\"topAppRows()\"\n [total]=\"tops.apps().total\"\n [offset]=\"tops.apps().offset\"\n [max]=\"tops.apps().max\"\n [pageSize]=\"topPageSize\"\n [loading]=\"tops.apps().loading\"\n [failed]=\"tops.apps().failed\"\n (offsetChange)=\"loadTop('apps', $event)\"\n (picked)=\"pickTop('apps', $event)\"\n />\n }\n <spine-audit-top-card\n class=\"audit__top audit__top--operations\"\n [heading]=\"text.topOperations\"\n [rows]=\"topOperationRows()\"\n [total]=\"tops.operations().total\"\n [offset]=\"tops.operations().offset\"\n [max]=\"tops.operations().max\"\n [pageSize]=\"topPageSize\"\n [loading]=\"tops.operations().loading\"\n [failed]=\"tops.operations().failed\"\n (offsetChange)=\"loadTop('operations', $event)\"\n (picked)=\"pickTop('operations', $event)\"\n />\n <spine-audit-top-card\n class=\"audit__top audit__top--users\"\n [heading]=\"text.topUsers\"\n [rows]=\"topUserRows()\"\n [total]=\"tops.users().total\"\n [offset]=\"tops.users().offset\"\n [max]=\"tops.users().max\"\n [pageSize]=\"topPageSize\"\n [loading]=\"tops.users().loading\"\n [failed]=\"tops.users().failed\"\n (offsetChange)=\"loadTop('users', $event)\"\n (picked)=\"pickTop('users', $event)\"\n />\n </div>\n\n <!-- Tabela: pagina\u00E7\u00E3o no servidor. A grade n\u00E3o corta nada ([paginated]=false): mostra a p\u00E1gina que veio\n (limit = itens por p\u00E1gina) e o rodap\u00E9 usa o total do resumo (mesma consulta). Esqueleto s\u00F3 na 1\u00AA\n carga; nas seguintes as linhas ficam e o indicador aparece por cima (sem piscar). -->\n <div class=\"audit__grid\" [class.audit__grid--busy]=\"gridBusy()\" [attr.aria-busy]=\"gridBusy()\">\n @if (gridBusy()) {\n <hm-progress class=\"audit__grid-progress\" indeterminate [label]=\"text.updating\" labelHidden />\n }\n <hm-data-grid\n [caption]=\"unitText().caption\"\n [columns]=\"columns()\"\n [rows]=\"events()\"\n [rowId]=\"rowId\"\n [loading]=\"gridSkeleton()\"\n [filterable]=\"false\"\n [paginated]=\"false\"\n [totalItems]=\"total()\"\n [pageState]=\"pageState()\"\n (pageStateChange)=\"changePage($event)\"\n [pageSizes]=\"pageSizes\"\n [itemsLabel]=\"unitText().pageItems\"\n [pageSizeLabel]=\"text.pageSize\"\n remoteSort\n rowClickable\n (rowClicked)=\"openRow($event)\"\n >\n <ng-template hmCell=\"operation\" let-item>\n <span class=\"audit__op\" [attr.title]=\"item.operation_label || null\">{{ operationName(item) }}</span>\n </ng-template>\n <ng-template hmCell=\"spans\" let-item>\n <span class=\"audit__spans\" [attr.title]=\"spansHint(item) || null\">\n {{ number(item.span_count ?? 1) }}\n @if (item.orphan) {\n <hm-tag class=\"audit__orphan\" [name]=\"text.orphanTag\" color=\"neutral\" />\n }\n </span>\n </ng-template>\n <ng-template hmCell=\"outcome\" let-item>\n <hm-tag [name]=\"outcomeLabel(item.outcome)\" [color]=\"outcomeColor(item.outcome)\" />\n </ng-template>\n </hm-data-grid>\n </div>\n\n @if (pageFailed()) {\n <hm-alert class=\"audit__page-error\" variant=\"error\" size=\"small\">{{ unitText().pageError }}</hm-alert>\n }\n }\n</div>\n\n<!-- Detalhe do evento + a cadeia (trace). -->\n<hm-drawer [open]=\"detailOpen()\" (openChange)=\"onDetailOpenChange($event)\" [heading]=\"showingAction() ? text.actionDetailHeading : text.detailHeading\">\n @if (selected(); as ev) {\n <div class=\"detail\">\n <div class=\"detail__head\">\n <strong class=\"detail__title\">{{ operationName(ev) }}</strong>\n <hm-tag [name]=\"outcomeLabel(headOutcome())\" [color]=\"outcomeColor(headOutcome())\" />\n </div>\n @if (showingAction() && selectedAction()?.orphan) {\n <hm-alert class=\"detail__orphan\" variant=\"info\" size=\"small\">{{ text.orphanNote }}</hm-alert>\n }\n @if (ev.operation_label && ev.operation_identifier) {\n <p class=\"muted detail__description\">{{ ev.operation_label }}</p>\n }\n\n @if (detailLoading()) {\n <p class=\"muted\" role=\"status\">{{ text.detailLoading }}</p>\n }\n @if (detailFailed()) {\n <hm-alert variant=\"warning\" size=\"small\">{{ text.detailError }}</hm-alert>\n }\n\n <dl class=\"detail__list\">\n @for (field of detailFields(); track field.label) {\n <div class=\"detail__row\">\n <dt>{{ field.label }}</dt>\n <dd>\n @if (field.code) { <code>{{ field.value }}</code> } @else { {{ field.value }} }\n </dd>\n </div>\n }\n </dl>\n\n @if (detailsJson()) {\n <h3 class=\"audit__section-title\">{{ text.fieldDetails }}</h3>\n <pre class=\"detail__json\">{{ detailsJson() }}</pre>\n }\n\n <!-- Por a\u00E7\u00E3o, os spans sobem para logo abaixo do cabe\u00E7alho (order no CSS); por evento, ficam no fim. -->\n @if (ev.trace_id) {\n <div class=\"detail__chain\" [class.detail__chain--first]=\"showingAction()\">\n <div class=\"detail__actions\">\n @if (!trace() && !traceLoading()) {\n <hm-button emphasis=\"medium\" size=\"small\" icon=\"tree-view\" (clicked)=\"showTrace()\">{{ text.viewTrace }}</hm-button>\n }\n <hm-button emphasis=\"minimal\" size=\"small\" icon=\"filter\" (clicked)=\"filterByTrace(ev.trace_id)\">{{ text.filterByTrace }}</hm-button>\n </div>\n @if (traceLoading()) {\n <p class=\"muted\" role=\"status\">{{ text.traceLoading }}</p>\n }\n @if (traceFailed()) {\n <hm-alert variant=\"error\" size=\"small\">{{ text.traceError }}</hm-alert>\n }\n @if (trace(); as tr) {\n <section class=\"trace\" aria-labelledby=\"audit-trace-title\">\n <h3 class=\"audit__section-title\" id=\"audit-trace-title\">{{ showingAction() ? text.spansHeading : text.traceHeading }}</h3>\n @if (tr.hidden_count) {\n <hm-alert class=\"trace__hidden\" variant=\"info\" size=\"small\">{{ plural(text.traceHidden, tr.hidden_count) }}</hm-alert>\n }\n @if (!tr.items.length) {\n <p class=\"muted\">{{ text.traceEmpty }}</p>\n } @else if (spanTimeline(); as tl) {\n <p class=\"muted trace__summary\">{{ fmt(text.spansSummary, { count: number(tl.rows.length), duration: duration(tl.totalMs) }) }}</p>\n <!-- Linha do tempo: cada span \u00E9 uma linha (recuo = profundidade pela cadeia parent_span_id) com a\n barra posicionada no tempo, proporcional \u00E0 dura\u00E7\u00E3o. O texto da linha (e o aria-label) traz\n tudo o que a barra mostra. -->\n <ol class=\"spans\">\n @for (row of tl.rows; track row.event.event_id) {\n <li class=\"spans__item\" [class.spans__item--current]=\"row.event.event_id === ev.event_id\">\n <button type=\"button\" class=\"spans__link\"\n [attr.aria-current]=\"row.event.event_id === ev.event_id ? 'true' : null\"\n [attr.aria-label]=\"spanAria(row)\"\n (click)=\"openEvent(row.event, true)\">\n <span class=\"spans__head\" [style.padding-inline-start.rem]=\"row.depth * 1\">\n <span class=\"spans__app\">{{ row.event.app_name ?? '' }}</span>\n <code class=\"spans__op\">{{ operationName(row.event) }}</code>\n </span>\n <hm-tag class=\"spans__tag\" [name]=\"outcomeLabel(row.event.outcome)\" [color]=\"outcomeColor(row.event.outcome)\" />\n <span class=\"spans__dur\">{{ duration(row.event.duration_ms) }}</span>\n <span class=\"spans__track\" aria-hidden=\"true\">\n <span class=\"spans__bar spans__bar--{{ row.event.outcome }}\" [style.left.%]=\"row.left\" [style.width.%]=\"row.width\"></span>\n </span>\n </button>\n </li>\n }\n </ol>\n }\n </section>\n }\n </div>\n }\n </div>\n }\n</hm-drawer>\n", styles: [".card{border:1px solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface-raised)}.card-block{padding:var(--hm-space-sm, 8px)}.muted{color:var(--hm-muted)}:host{display:block}.audit{display:flex;flex-direction:column;gap:var(--hm-space-md, 16px)}.audit__toolbar{display:flex;align-items:center;flex-wrap:wrap;gap:var(--hm-space-sm, 8px)}.audit__period-note{font-size:13px}.audit__toolbar-end{display:inline-flex;align-items:center;flex-wrap:wrap;gap:var(--hm-space-sm, 8px) var(--hm-space-md, 16px);margin-inline-start:auto}.audit__updated{font-size:12px;font-variant-numeric:tabular-nums}.audit__state{min-block-size:320px}.audit__section-title{margin:0 0 var(--hm-space-sm, 8px);font-size:14px;font-weight:600;color:var(--hm-text)}.audit__kpis{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:var(--hm-space-sm, 8px);align-items:stretch}.timeline{display:flex;align-items:flex-end;gap:2px;block-size:120px;margin:0;padding:0;list-style:none;border-block-end:1px solid var(--hm-border)}.timeline__bar{flex:1 1 0;min-inline-size:2px;block-size:100%;display:flex;flex-direction:column;justify-content:flex-end;border-radius:2px 2px 0 0;overflow:hidden}.timeline__bar:hover{background:color-mix(in srgb,var(--hm-text) 6%,transparent)}.timeline__seg{display:block;inline-size:100%}.timeline__seg--ok,.swatch--ok{background:color-mix(in srgb,var(--hm-action) 55%,transparent)}.timeline__seg--fail,.swatch--fail{background:var(--hm-danger)}.timeline__seg--denied,.swatch--denied{background:var(--hm-warning, #c98a00)}.timeline__axis{display:flex;justify-content:space-between;margin-block-start:4px;font-size:12px;color:var(--hm-muted)}.timeline__legend{display:flex;flex-wrap:wrap;gap:var(--hm-space-sm, 8px) var(--hm-space-md, 16px);margin:var(--hm-space-sm, 8px) 0 0;padding:0;list-style:none;font-size:12px;color:var(--hm-muted)}.timeline__legend li{display:inline-flex;align-items:center;gap:6px}.swatch{display:inline-block;inline-size:10px;block-size:10px;border-radius:2px}.audit__tops{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:var(--hm-space-sm, 8px);align-items:stretch}.audit__grid{position:relative;display:block}.audit__grid-progress{position:absolute;inset-inline:0;inset-block-start:0;z-index:2;pointer-events:none}.audit__grid--busy hm-data-grid{opacity:.6;transition:opacity .12s ease}.audit__op{font-family:var(--hm-font-mono, ui-monospace, monospace);font-size:12px}.audit__page-error{display:block}@media(prefers-reduced-motion:reduce){.audit__grid--busy hm-data-grid{transition:none}}:host-context([data-motion=reduce]) .audit__grid--busy hm-data-grid{transition:none}.detail{display:flex;flex-direction:column;gap:var(--hm-space-sm, 8px)}.detail__head{display:flex;align-items:center;gap:var(--hm-space-sm, 8px);flex-wrap:wrap}.detail__title{font-size:16px}.detail__list{display:grid;grid-template-columns:minmax(8rem,max-content) minmax(0,1fr);gap:6px var(--hm-space-md, 16px);margin:0}.detail__row{display:contents}.detail__row dt{color:var(--hm-muted);font-size:13px}.detail__row dd{margin:0;font-size:13px;overflow-wrap:anywhere}.detail__json{margin:0;padding:var(--hm-space-sm, 8px);max-block-size:280px;overflow:auto;border:1px solid var(--hm-border);border-radius:var(--hm-radius-control);background:color-mix(in srgb,var(--hm-text) 4%,transparent);font-size:12px}.detail__description{margin:0;font-size:13px}.detail__actions{display:flex;flex-wrap:wrap;gap:var(--hm-space-sm, 8px)}.trace{margin-block-start:var(--hm-space-sm, 8px)}.trace__hidden{margin-block-end:var(--hm-space-sm, 8px)}.detail__head,.detail__description,.detail__orphan{order:-3}.detail__chain{display:flex;flex-direction:column;gap:var(--hm-space-sm, 8px)}.detail__chain--first{order:-2}.trace__summary{margin:0 0 var(--hm-space-xs, 4px);font-size:13px}.spans{margin:0;padding:0;list-style:none;border-block-start:1px solid var(--hm-border)}.spans__item{border-block-end:1px solid var(--hm-border)}.spans__item--current .spans__link{background:color-mix(in srgb,var(--hm-action) 10%,transparent);box-shadow:inset 2px 0 0 var(--hm-action)}.spans__link{display:grid;grid-template-columns:minmax(0,1fr) auto auto;grid-template-areas:\"head tag dur\" \"track track track\";align-items:center;gap:4px var(--hm-space-sm, 8px);inline-size:100%;padding:6px 8px;border:0;background:transparent;color:var(--hm-text);font:inherit;font-size:13px;text-align:start;cursor:pointer}.spans__link:hover{background:color-mix(in srgb,var(--hm-action) 6%,transparent)}.spans__link:focus-visible{outline:2px solid var(--hm-action);outline-offset:-2px}.spans__head{grid-area:head;display:flex;flex-direction:column;min-inline-size:0}.spans__app{color:var(--hm-muted);font-size:12px}.spans__op{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:var(--hm-font-mono, ui-monospace, monospace);font-size:12px}.spans__tag{grid-area:tag}.spans__dur{grid-area:dur;min-inline-size:4.5rem;text-align:end;font-variant-numeric:tabular-nums;color:var(--hm-muted)}.spans__track{grid-area:track;position:relative;block-size:6px;border-radius:3px;background:color-mix(in srgb,var(--hm-text) 6%,transparent)}.spans__bar{position:absolute;inset-block:0;border-radius:3px;background:color-mix(in srgb,var(--hm-action) 70%,transparent)}.spans__bar--failure{background:var(--hm-danger)}.spans__bar--denied{background:var(--hm-warning, #c98a00)}.audit__spans{display:inline-flex;align-items:center;gap:6px;font-variant-numeric:tabular-nums}\n"] }]
1341
1345
  }], ctorParameters: () => [], propDecorators: { scope: [{ type: i0.Input, args: [{ isSignal: true, alias: "scope", required: true }] }], clientId: [{ type: i0.Input, args: [{ isSignal: true, alias: "clientId", required: false }] }], adminApi: [{ type: i0.Input, args: [{ isSignal: true, alias: "adminApi", required: false }] }] } });
1342
1346
 
1343
1347
  /** Tentativas por página, de saída (o servidor aceita até 200). */
@@ -1524,14 +1528,14 @@ class SpineLoginAttemptsComponent {
1524
1528
  return why ? formatMessage(this.text.loadErrorDetail, { detail: why }) : this.text.loadError;
1525
1529
  }
1526
1530
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SpineLoginAttemptsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1527
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: SpineLoginAttemptsComponent, isStandalone: true, selector: "spine-login-attempts", inputs: { scope: { classPropertyName: "scope", publicName: "scope", isSignal: true, isRequired: true, transformFunction: null }, clientId: { classPropertyName: "clientId", publicName: "clientId", isSignal: true, isRequired: false, transformFunction: null }, realmId: { classPropertyName: "realmId", publicName: "realmId", isSignal: true, isRequired: false, transformFunction: null }, adminApi: { classPropertyName: "adminApi", publicName: "adminApi", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"attempts\">\n <div class=\"attempts__actions\">\n <hm-button emphasis=\"medium\" icon=\"refresh\" [disabled]=\"loading()\" (clicked)=\"load(pageState().page)\">{{ text.refresh }}</hm-button>\n </div>\n\n @if (loadError(); as message) {\n <hm-alert variant=\"error\" [closable]=\"true\" (closed)=\"loadError.set(null)\">{{ message }}</hm-alert>\n }\n\n <hm-card>\n <div class=\"attempts__filters\" [formGroup]=\"filtersForm\">\n <div class=\"attempts__row\">\n <hm-input class=\"attempts__search\" [label]=\"text.user\" [placeholder]=\"text.userPlaceholder\" formControlName=\"user\" (keyup.enter)=\"applyFilters()\" />\n <hm-date-range-picker class=\"attempts__period\" [label]=\"text.period\" formControlName=\"period\" [presets]=\"periodPresets\" />\n <hm-combobox class=\"attempts__field\" [label]=\"text.result\" formControlName=\"result\" [options]=\"resultOptions\" />\n <hm-combobox class=\"attempts__field\" [label]=\"text.method\" formControlName=\"method\" [options]=\"methodOptions\" />\n <div class=\"attempts__buttons\">\n <hm-button emphasis=\"medium\" (clicked)=\"clearFilters()\">{{ text.clear }}</hm-button>\n <hm-button emphasis=\"minimal\" [iconEnd]=\"showAdvancedFilters() ? 'chevron-up' : 'chevron-down'\" (clicked)=\"showAdvancedFilters.update(v => !v)\">\n {{ text.advancedFilters }}\n </hm-button>\n </div>\n </div>\n @if (showAdvancedFilters()) {\n <div class=\"attempts__row\">\n <hm-input class=\"attempts__field\" [label]=\"text.ip\" formControlName=\"ip\" [placeholder]=\"text.ipPlaceholder\" [hint]=\"text.ipHint\" />\n <hm-combobox class=\"attempts__field attempts__field--wide\" [label]=\"text.reason\" formControlName=\"reason\" [options]=\"reasonOptions\" />\n </div>\n }\n </div>\n </hm-card>\n\n <hm-card>\n <hm-data-grid\n [caption]=\"text.caption\"\n [columns]=\"columns()\"\n [rows]=\"attempts()\"\n [rowId]=\"rowId\"\n [rowActions]=\"rowActions\"\n [loading]=\"loading()\"\n [filterable]=\"false\"\n [paginated]=\"false\"\n [totalItems]=\"total()\"\n [pageState]=\"pageState()\"\n (pageStateChange)=\"onPageStateChange($event)\"\n [pageSizes]=\"pageSizes\"\n [itemsLabel]=\"text.pageItems\"\n [pageSizeLabel]=\"text.pageSize\"\n (actionTriggered)=\"onRowAction($event)\"\n >\n <!-- Data/hora no fuso do navegador (a API manda ISO em UTC). -->\n <ng-template hmCell=\"created_at\" let-a>{{ formatDate(a.created_at) }}</ng-template>\n <ng-template hmCell=\"result\" let-a>\n <hm-tag [name]=\"resultLabel(a.result)\" [color]=\"resultColor(a.result)\" />\n </ng-template>\n <ng-template hmCell=\"user\" let-a>\n @if (a.user_id) {\n <span class=\"attempts__user\">\n <span class=\"attempts__user-name\">{{ a.user_name || a.user_username }}</span>\n <span class=\"attempts__user-detail\">{{ a.user_username }}@if (a.user_email) { \u00B7 {{ a.user_email }} }</span>\n </span>\n } @else if (a.username) {\n <span class=\"attempts__user\">\n <span class=\"attempts__user-name\">{{ a.username }}</span>\n <span class=\"attempts__user-detail\">{{ text.noMatchingUser }}</span>\n </span>\n } @else {\n <span class=\"attempts__muted\">\u2014</span>\n }\n </ng-template>\n <ng-template hmCell=\"reason\" let-a>\n @if (a.reason) { {{ reasonLabel(a.reason) }} } @else { <span class=\"attempts__muted\">\u2014</span> }\n </ng-template>\n <ng-template hmCell=\"ip\" let-a>\n @if (a.ip) { <code class=\"attempts__ip\">{{ a.ip }}</code> } @else { <span class=\"attempts__muted\">\u2014</span> }\n </ng-template>\n <ng-template hmCell=\"client\" let-a>\n @if (a.client_name) { {{ a.client_name }} } @else { <span class=\"attempts__muted\">\u2014</span> }\n </ng-template>\n <ng-template hmCell=\"user_agent\" let-a>\n @if (a.user_agent) { <span class=\"attempts__ua\" [attr.title]=\"a.user_agent\">{{ a.user_agent }}</span> } @else { <span class=\"attempts__muted\">\u2014</span> }\n </ng-template>\n </hm-data-grid>\n </hm-card>\n</div>\n", styles: [":host{display:block}.attempts{display:flex;flex-direction:column;gap:var(--hm-space-md, 16px)}.attempts__actions{display:flex;justify-content:flex-end}.attempts__filters{display:flex;flex-direction:column;gap:var(--hm-space-md, 16px)}.attempts__row{display:flex;align-items:flex-end;flex-wrap:wrap;gap:var(--hm-space-sm, 12px)}.attempts__row+.attempts__row{padding-top:var(--hm-space-md, 16px);border-top:1px solid var(--hm-divider)}.attempts__search{display:block;flex:1 1 16rem;min-width:0}.attempts__period{display:block;flex:0 0 auto}.attempts__field{display:block;flex:0 1 12rem;min-width:0}.attempts__field--wide{flex-basis:18rem}.attempts__buttons{display:inline-flex;flex-wrap:wrap;gap:var(--hm-space-sm, 8px)}.attempts__user{display:inline-flex;flex-direction:column;line-height:1.25}.attempts__user-name{font-weight:600}.attempts__user-detail{color:var(--hm-muted);font-size:.8rem}.attempts__ip{font-family:var(--hm-font-mono, monospace);font-size:.85rem}.attempts__ua{display:inline-block;max-width:22rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;vertical-align:bottom}.attempts__muted{color:var(--hm-muted)}\n"], dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "component", type: AlertComponent, selector: "hm-alert", inputs: ["variant", "appearance", "heading", "closable", "size", "showIcon", "link", "linkLabel", "linkTarget", "dismissKey", "duration"], outputs: ["closed"] }, { kind: "component", type: ButtonComponent, selector: "hm-button", inputs: ["emphasis", "intent", "size", "type", "icon", "iconDirection", "iconEnd", "disabled", "loading", "busy", "loadingState", "block", "link", "newTab", "caret", "inMenu", "toggle", "pressed"], outputs: ["loadingStateChange", "pressedChange", "clicked"] }, { kind: "component", type: CardComponent, selector: "hm-card", inputs: ["heading", "icon", "selected", "fill", "media", "mediaAlt", "mediaDecorative", "mediaPosition"] }, { kind: "component", type: ComboboxComponent, selector: "hm-combobox", inputs: ["label", "options", "value", "placeholder", "icon", "hint", "help", "size", "labelHidden", "clearable", "requiredManual", "loading", "allowCustomValue"], outputs: ["valueChange"] }, { kind: "directive", type: DataGridCellDirective, selector: "ng-template[hmCell]", inputs: ["hmCell"] }, { kind: "component", type: DataGridComponent, selector: "hm-data-grid", inputs: ["columns", "rows", "rowId", "caption", "rowActions", "loading", "error", "emptyMessage", "noResultsMessage", "filterable", "paginated", "pageState", "pagination", "totalItems", "sort", "selectable", "selectionMode", "selection", "rowSelectable", "rowSelectableReason", "rowDetailDisabled", "rowDetailHidden", "virtual", "rowHeight", "pinnedColumns", "columnState", "pageSizes", "fillHeight", "maxHeight", "bordered", "rowState", "rowClickable", "remoteSearch", "remoteSort", "itemsLabel", "pageSizeLabel", "exportable", "exportFileName", "groupBy", "aggregates", "aggregateFormatter", "persistKey", "persistStorage", "advancedFilter", "filterQuery", "columnManager", "columnOrder", "searchTerm"], outputs: ["pageStateChange", "sortChange", "selectionChange", "pinnedColumnsChange", "columnStateChange", "actionTriggered", "detailToggled", "rowClicked", "clearFiltersRequested", "filteredChange", "searchTermChanged", "groupByChange", "filterQueryChange", "columnOrderChange", "searchTermChange", "detailPaneToggled"] }, { kind: "component", type: DateRangePickerComponent, selector: "hm-date-range-picker", inputs: ["value", "startControl", "endControl", "start", "end", "label", "labelHidden", "hint", "min", "max", "valueFormat", "presets"], outputs: ["valueChange", "startChange", "endChange"] }, { kind: "component", type: InputComponent, selector: "hm-input", inputs: ["value", "label", "type", "placeholder", "icon", "labelHidden", "hint", "prefix", "suffix", "prefixIcon", "mask", "maskValue", "min", "max", "step", "stepButtons", "maxLength", "autocomplete", "inputMode"], outputs: ["valueChange"] }, { kind: "component", type: TagComponent, selector: "hm-tag", inputs: ["name", "color", "compact"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
1531
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: SpineLoginAttemptsComponent, isStandalone: true, selector: "spine-login-attempts", inputs: { scope: { classPropertyName: "scope", publicName: "scope", isSignal: true, isRequired: true, transformFunction: null }, clientId: { classPropertyName: "clientId", publicName: "clientId", isSignal: true, isRequired: false, transformFunction: null }, realmId: { classPropertyName: "realmId", publicName: "realmId", isSignal: true, isRequired: false, transformFunction: null }, adminApi: { classPropertyName: "adminApi", publicName: "adminApi", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"attempts\">\n <div class=\"attempts__actions\">\n <hm-button emphasis=\"medium\" icon=\"refresh\" [disabled]=\"loading()\" (clicked)=\"load(pageState().page)\">{{ text.refresh }}</hm-button>\n </div>\n\n @if (loadError(); as message) {\n <hm-alert variant=\"error\" [closable]=\"true\" (closed)=\"loadError.set(null)\">{{ message }}</hm-alert>\n }\n\n <hm-card>\n <div class=\"attempts__filters\" [formGroup]=\"filtersForm\">\n <div class=\"attempts__row\">\n <hm-input class=\"attempts__search\" [label]=\"text.user\" [placeholder]=\"text.userPlaceholder\" formControlName=\"user\" (keyup.enter)=\"applyFilters()\" />\n <hm-date-range-picker class=\"attempts__period\" [label]=\"text.period\" formControlName=\"period\" [presets]=\"periodPresets\" />\n <hm-combobox class=\"attempts__field\" [label]=\"text.result\" formControlName=\"result\" [options]=\"resultOptions\" />\n <hm-combobox class=\"attempts__field\" [label]=\"text.method\" formControlName=\"method\" [options]=\"methodOptions\" />\n <div class=\"attempts__buttons\">\n <hm-button emphasis=\"medium\" (clicked)=\"clearFilters()\">{{ text.clear }}</hm-button>\n <hm-button emphasis=\"minimal\" [iconEnd]=\"showAdvancedFilters() ? 'chevron-up' : 'chevron-down'\" (clicked)=\"showAdvancedFilters.update(v => !v)\">\n {{ text.advancedFilters }}\n </hm-button>\n </div>\n </div>\n @if (showAdvancedFilters()) {\n <div class=\"attempts__row\">\n <hm-input class=\"attempts__field\" [label]=\"text.ip\" formControlName=\"ip\" [placeholder]=\"text.ipPlaceholder\" [hint]=\"text.ipHint\" />\n <hm-combobox class=\"attempts__field attempts__field--wide\" [label]=\"text.reason\" formControlName=\"reason\" [options]=\"reasonOptions\" />\n </div>\n }\n </div>\n </hm-card>\n\n <hm-card>\n <hm-data-grid\n [caption]=\"text.caption\"\n [columns]=\"columns()\"\n [rows]=\"attempts()\"\n [rowId]=\"rowId\"\n [rowActions]=\"rowActions\"\n [loading]=\"loading()\"\n [filterable]=\"false\"\n [paginated]=\"false\"\n [totalItems]=\"total()\"\n [pageState]=\"pageState()\"\n (pageStateChange)=\"onPageStateChange($event)\"\n [pageSizes]=\"pageSizes\"\n [itemsLabel]=\"text.pageItems\"\n [pageSizeLabel]=\"text.pageSize\"\n (actionTriggered)=\"onRowAction($event)\"\n >\n <!-- Data/hora no fuso do navegador (a API manda ISO em UTC). -->\n <ng-template hmCell=\"created_at\" let-a>{{ formatDate(a.created_at) }}</ng-template>\n <ng-template hmCell=\"result\" let-a>\n <hm-tag [name]=\"resultLabel(a.result)\" [color]=\"resultColor(a.result)\" />\n </ng-template>\n <ng-template hmCell=\"user\" let-a>\n @if (a.user_id) {\n <span class=\"attempts__person\">\n <hm-account-avatar size=\"small\" [name]=\"a.user_name || a.user_username || ''\" [imageUrl]=\"a.user_picture\" />\n <span class=\"attempts__user\">\n <span class=\"attempts__user-name\">{{ a.user_name || a.user_username }}</span>\n <span class=\"attempts__user-detail\">{{ a.user_username }}@if (a.user_email) { \u00B7 {{ a.user_email }} }</span>\n </span>\n </span>\n } @else if (a.username) {\n <span class=\"attempts__user\">\n <span class=\"attempts__user-name\">{{ a.username }}</span>\n <span class=\"attempts__user-detail\">{{ text.noMatchingUser }}</span>\n </span>\n } @else {\n <span class=\"attempts__muted\">\u2014</span>\n }\n </ng-template>\n <ng-template hmCell=\"reason\" let-a>\n @if (a.reason) { {{ reasonLabel(a.reason) }} } @else { <span class=\"attempts__muted\">\u2014</span> }\n </ng-template>\n <ng-template hmCell=\"ip\" let-a>\n @if (a.ip) { <code class=\"attempts__ip\">{{ a.ip }}</code> } @else { <span class=\"attempts__muted\">\u2014</span> }\n </ng-template>\n <ng-template hmCell=\"client\" let-a>\n @if (a.client_name) { {{ a.client_name }} } @else { <span class=\"attempts__muted\">\u2014</span> }\n </ng-template>\n <ng-template hmCell=\"user_agent\" let-a>\n @if (a.user_agent) { <span class=\"attempts__ua\" [attr.title]=\"a.user_agent\">{{ a.user_agent }}</span> } @else { <span class=\"attempts__muted\">\u2014</span> }\n </ng-template>\n </hm-data-grid>\n </hm-card>\n</div>\n", styles: [":host{display:block}.attempts{display:flex;flex-direction:column;gap:var(--hm-space-md, 16px)}.attempts__actions{display:flex;justify-content:flex-end}.attempts__filters{display:flex;flex-direction:column;gap:var(--hm-space-md, 16px)}.attempts__row{display:flex;align-items:flex-end;flex-wrap:wrap;gap:var(--hm-space-sm, 12px)}.attempts__row+.attempts__row{padding-top:var(--hm-space-md, 16px);border-top:1px solid var(--hm-divider)}.attempts__search{display:block;flex:1 1 16rem;min-width:0}.attempts__period{display:block;flex:0 0 auto}.attempts__field{display:block;flex:0 1 12rem;min-width:0}.attempts__field--wide{flex-basis:18rem}.attempts__buttons{display:inline-flex;flex-wrap:wrap;gap:var(--hm-space-sm, 8px)}.attempts__person{display:inline-flex;align-items:center;gap:var(--hm-space-sm);min-width:0}.attempts__person hm-account-avatar{flex:none}.attempts__user{display:inline-flex;flex-direction:column;min-width:0;line-height:1.25}.attempts__user-name{font-weight:600}.attempts__user-detail{color:var(--hm-muted);font-size:.8rem}.attempts__ip{font-family:var(--hm-font-mono, monospace);font-size:.85rem}.attempts__ua{display:inline-block;max-width:22rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;vertical-align:bottom}.attempts__muted{color:var(--hm-muted)}\n"], dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "component", type: AccountAvatarComponent, selector: "hm-account-avatar", inputs: ["name", "imageUrl", "size", "loading"] }, { kind: "component", type: AlertComponent, selector: "hm-alert", inputs: ["variant", "appearance", "heading", "closable", "size", "showIcon", "link", "linkLabel", "linkTarget", "dismissKey", "duration"], outputs: ["closed"] }, { kind: "component", type: ButtonComponent, selector: "hm-button", inputs: ["emphasis", "intent", "size", "type", "icon", "iconDirection", "iconEnd", "disabled", "loading", "loadingState", "block", "link", "newTab", "caret", "inMenu", "toggle", "pressed"], outputs: ["loadingStateChange", "pressedChange", "clicked"] }, { kind: "component", type: CardComponent, selector: "hm-card", inputs: ["heading", "icon", "selected", "fill", "media", "mediaAlt", "mediaDecorative", "mediaPosition"] }, { kind: "component", type: ComboboxComponent, selector: "hm-combobox", inputs: ["label", "options", "value", "placeholder", "icon", "hint", "size", "labelHidden", "clearable", "loading", "allowCustomValue"], outputs: ["valueChange"] }, { kind: "directive", type: DataGridCellDirective, selector: "ng-template[hmCell]", inputs: ["hmCell"] }, { kind: "component", type: DataGridComponent, selector: "hm-data-grid", inputs: ["columns", "rows", "rowId", "caption", "rowActions", "loading", "error", "emptyMessage", "noResultsMessage", "filterable", "paginated", "pageState", "pagination", "totalItems", "sort", "selectable", "selectionMode", "selection", "rowSelectable", "rowSelectableReason", "rowDetailDisabled", "rowDetailHidden", "virtual", "rowHeight", "pinnedColumns", "columnState", "pageSizes", "fillHeight", "maxHeight", "bordered", "rowState", "rowClickable", "remoteSearch", "remoteSort", "itemsLabel", "pageSizeLabel", "exportable", "exportFileName", "groupBy", "aggregates", "aggregateFormatter", "persistKey", "persistStorage", "advancedFilter", "filterQuery", "columnManager", "columnOrder", "searchTerm"], outputs: ["pageStateChange", "sortChange", "selectionChange", "pinnedColumnsChange", "columnStateChange", "actionTriggered", "detailToggled", "rowClicked", "clearFiltersRequested", "filteredChange", "searchTermChanged", "groupByChange", "filterQueryChange", "columnOrderChange", "searchTermChange", "detailPaneToggled"] }, { kind: "component", type: DateRangePickerComponent, selector: "hm-date-range-picker", inputs: ["value", "label", "labelHidden", "hint", "min", "max", "valueFormat", "presets"], outputs: ["valueChange"] }, { kind: "component", type: InputComponent, selector: "hm-input", inputs: ["value", "label", "type", "placeholder", "icon", "labelHidden", "hint", "prefix", "suffix", "prefixIcon", "mask", "maskValue", "min", "max", "step", "stepButtons", "maxLength", "autocomplete", "inputMode"], outputs: ["valueChange"] }, { kind: "component", type: TagComponent, selector: "hm-tag", inputs: ["name", "color", "compact"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
1528
1532
  }
1529
1533
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SpineLoginAttemptsComponent, decorators: [{
1530
1534
  type: Component,
1531
1535
  args: [{ selector: 'spine-login-attempts', imports: [
1532
- ReactiveFormsModule, AlertComponent, ButtonComponent, CardComponent, ComboboxComponent, DataGridCellDirective, DataGridComponent,
1536
+ ReactiveFormsModule, AccountAvatarComponent, AlertComponent, ButtonComponent, CardComponent, ComboboxComponent, DataGridCellDirective, DataGridComponent,
1533
1537
  DateRangePickerComponent, InputComponent, TagComponent,
1534
- ], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"attempts\">\n <div class=\"attempts__actions\">\n <hm-button emphasis=\"medium\" icon=\"refresh\" [disabled]=\"loading()\" (clicked)=\"load(pageState().page)\">{{ text.refresh }}</hm-button>\n </div>\n\n @if (loadError(); as message) {\n <hm-alert variant=\"error\" [closable]=\"true\" (closed)=\"loadError.set(null)\">{{ message }}</hm-alert>\n }\n\n <hm-card>\n <div class=\"attempts__filters\" [formGroup]=\"filtersForm\">\n <div class=\"attempts__row\">\n <hm-input class=\"attempts__search\" [label]=\"text.user\" [placeholder]=\"text.userPlaceholder\" formControlName=\"user\" (keyup.enter)=\"applyFilters()\" />\n <hm-date-range-picker class=\"attempts__period\" [label]=\"text.period\" formControlName=\"period\" [presets]=\"periodPresets\" />\n <hm-combobox class=\"attempts__field\" [label]=\"text.result\" formControlName=\"result\" [options]=\"resultOptions\" />\n <hm-combobox class=\"attempts__field\" [label]=\"text.method\" formControlName=\"method\" [options]=\"methodOptions\" />\n <div class=\"attempts__buttons\">\n <hm-button emphasis=\"medium\" (clicked)=\"clearFilters()\">{{ text.clear }}</hm-button>\n <hm-button emphasis=\"minimal\" [iconEnd]=\"showAdvancedFilters() ? 'chevron-up' : 'chevron-down'\" (clicked)=\"showAdvancedFilters.update(v => !v)\">\n {{ text.advancedFilters }}\n </hm-button>\n </div>\n </div>\n @if (showAdvancedFilters()) {\n <div class=\"attempts__row\">\n <hm-input class=\"attempts__field\" [label]=\"text.ip\" formControlName=\"ip\" [placeholder]=\"text.ipPlaceholder\" [hint]=\"text.ipHint\" />\n <hm-combobox class=\"attempts__field attempts__field--wide\" [label]=\"text.reason\" formControlName=\"reason\" [options]=\"reasonOptions\" />\n </div>\n }\n </div>\n </hm-card>\n\n <hm-card>\n <hm-data-grid\n [caption]=\"text.caption\"\n [columns]=\"columns()\"\n [rows]=\"attempts()\"\n [rowId]=\"rowId\"\n [rowActions]=\"rowActions\"\n [loading]=\"loading()\"\n [filterable]=\"false\"\n [paginated]=\"false\"\n [totalItems]=\"total()\"\n [pageState]=\"pageState()\"\n (pageStateChange)=\"onPageStateChange($event)\"\n [pageSizes]=\"pageSizes\"\n [itemsLabel]=\"text.pageItems\"\n [pageSizeLabel]=\"text.pageSize\"\n (actionTriggered)=\"onRowAction($event)\"\n >\n <!-- Data/hora no fuso do navegador (a API manda ISO em UTC). -->\n <ng-template hmCell=\"created_at\" let-a>{{ formatDate(a.created_at) }}</ng-template>\n <ng-template hmCell=\"result\" let-a>\n <hm-tag [name]=\"resultLabel(a.result)\" [color]=\"resultColor(a.result)\" />\n </ng-template>\n <ng-template hmCell=\"user\" let-a>\n @if (a.user_id) {\n <span class=\"attempts__user\">\n <span class=\"attempts__user-name\">{{ a.user_name || a.user_username }}</span>\n <span class=\"attempts__user-detail\">{{ a.user_username }}@if (a.user_email) { \u00B7 {{ a.user_email }} }</span>\n </span>\n } @else if (a.username) {\n <span class=\"attempts__user\">\n <span class=\"attempts__user-name\">{{ a.username }}</span>\n <span class=\"attempts__user-detail\">{{ text.noMatchingUser }}</span>\n </span>\n } @else {\n <span class=\"attempts__muted\">\u2014</span>\n }\n </ng-template>\n <ng-template hmCell=\"reason\" let-a>\n @if (a.reason) { {{ reasonLabel(a.reason) }} } @else { <span class=\"attempts__muted\">\u2014</span> }\n </ng-template>\n <ng-template hmCell=\"ip\" let-a>\n @if (a.ip) { <code class=\"attempts__ip\">{{ a.ip }}</code> } @else { <span class=\"attempts__muted\">\u2014</span> }\n </ng-template>\n <ng-template hmCell=\"client\" let-a>\n @if (a.client_name) { {{ a.client_name }} } @else { <span class=\"attempts__muted\">\u2014</span> }\n </ng-template>\n <ng-template hmCell=\"user_agent\" let-a>\n @if (a.user_agent) { <span class=\"attempts__ua\" [attr.title]=\"a.user_agent\">{{ a.user_agent }}</span> } @else { <span class=\"attempts__muted\">\u2014</span> }\n </ng-template>\n </hm-data-grid>\n </hm-card>\n</div>\n", styles: [":host{display:block}.attempts{display:flex;flex-direction:column;gap:var(--hm-space-md, 16px)}.attempts__actions{display:flex;justify-content:flex-end}.attempts__filters{display:flex;flex-direction:column;gap:var(--hm-space-md, 16px)}.attempts__row{display:flex;align-items:flex-end;flex-wrap:wrap;gap:var(--hm-space-sm, 12px)}.attempts__row+.attempts__row{padding-top:var(--hm-space-md, 16px);border-top:1px solid var(--hm-divider)}.attempts__search{display:block;flex:1 1 16rem;min-width:0}.attempts__period{display:block;flex:0 0 auto}.attempts__field{display:block;flex:0 1 12rem;min-width:0}.attempts__field--wide{flex-basis:18rem}.attempts__buttons{display:inline-flex;flex-wrap:wrap;gap:var(--hm-space-sm, 8px)}.attempts__user{display:inline-flex;flex-direction:column;line-height:1.25}.attempts__user-name{font-weight:600}.attempts__user-detail{color:var(--hm-muted);font-size:.8rem}.attempts__ip{font-family:var(--hm-font-mono, monospace);font-size:.85rem}.attempts__ua{display:inline-block;max-width:22rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;vertical-align:bottom}.attempts__muted{color:var(--hm-muted)}\n"] }]
1538
+ ], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"attempts\">\n <div class=\"attempts__actions\">\n <hm-button emphasis=\"medium\" icon=\"refresh\" [disabled]=\"loading()\" (clicked)=\"load(pageState().page)\">{{ text.refresh }}</hm-button>\n </div>\n\n @if (loadError(); as message) {\n <hm-alert variant=\"error\" [closable]=\"true\" (closed)=\"loadError.set(null)\">{{ message }}</hm-alert>\n }\n\n <hm-card>\n <div class=\"attempts__filters\" [formGroup]=\"filtersForm\">\n <div class=\"attempts__row\">\n <hm-input class=\"attempts__search\" [label]=\"text.user\" [placeholder]=\"text.userPlaceholder\" formControlName=\"user\" (keyup.enter)=\"applyFilters()\" />\n <hm-date-range-picker class=\"attempts__period\" [label]=\"text.period\" formControlName=\"period\" [presets]=\"periodPresets\" />\n <hm-combobox class=\"attempts__field\" [label]=\"text.result\" formControlName=\"result\" [options]=\"resultOptions\" />\n <hm-combobox class=\"attempts__field\" [label]=\"text.method\" formControlName=\"method\" [options]=\"methodOptions\" />\n <div class=\"attempts__buttons\">\n <hm-button emphasis=\"medium\" (clicked)=\"clearFilters()\">{{ text.clear }}</hm-button>\n <hm-button emphasis=\"minimal\" [iconEnd]=\"showAdvancedFilters() ? 'chevron-up' : 'chevron-down'\" (clicked)=\"showAdvancedFilters.update(v => !v)\">\n {{ text.advancedFilters }}\n </hm-button>\n </div>\n </div>\n @if (showAdvancedFilters()) {\n <div class=\"attempts__row\">\n <hm-input class=\"attempts__field\" [label]=\"text.ip\" formControlName=\"ip\" [placeholder]=\"text.ipPlaceholder\" [hint]=\"text.ipHint\" />\n <hm-combobox class=\"attempts__field attempts__field--wide\" [label]=\"text.reason\" formControlName=\"reason\" [options]=\"reasonOptions\" />\n </div>\n }\n </div>\n </hm-card>\n\n <hm-card>\n <hm-data-grid\n [caption]=\"text.caption\"\n [columns]=\"columns()\"\n [rows]=\"attempts()\"\n [rowId]=\"rowId\"\n [rowActions]=\"rowActions\"\n [loading]=\"loading()\"\n [filterable]=\"false\"\n [paginated]=\"false\"\n [totalItems]=\"total()\"\n [pageState]=\"pageState()\"\n (pageStateChange)=\"onPageStateChange($event)\"\n [pageSizes]=\"pageSizes\"\n [itemsLabel]=\"text.pageItems\"\n [pageSizeLabel]=\"text.pageSize\"\n (actionTriggered)=\"onRowAction($event)\"\n >\n <!-- Data/hora no fuso do navegador (a API manda ISO em UTC). -->\n <ng-template hmCell=\"created_at\" let-a>{{ formatDate(a.created_at) }}</ng-template>\n <ng-template hmCell=\"result\" let-a>\n <hm-tag [name]=\"resultLabel(a.result)\" [color]=\"resultColor(a.result)\" />\n </ng-template>\n <ng-template hmCell=\"user\" let-a>\n @if (a.user_id) {\n <span class=\"attempts__person\">\n <hm-account-avatar size=\"small\" [name]=\"a.user_name || a.user_username || ''\" [imageUrl]=\"a.user_picture\" />\n <span class=\"attempts__user\">\n <span class=\"attempts__user-name\">{{ a.user_name || a.user_username }}</span>\n <span class=\"attempts__user-detail\">{{ a.user_username }}@if (a.user_email) { \u00B7 {{ a.user_email }} }</span>\n </span>\n </span>\n } @else if (a.username) {\n <span class=\"attempts__user\">\n <span class=\"attempts__user-name\">{{ a.username }}</span>\n <span class=\"attempts__user-detail\">{{ text.noMatchingUser }}</span>\n </span>\n } @else {\n <span class=\"attempts__muted\">\u2014</span>\n }\n </ng-template>\n <ng-template hmCell=\"reason\" let-a>\n @if (a.reason) { {{ reasonLabel(a.reason) }} } @else { <span class=\"attempts__muted\">\u2014</span> }\n </ng-template>\n <ng-template hmCell=\"ip\" let-a>\n @if (a.ip) { <code class=\"attempts__ip\">{{ a.ip }}</code> } @else { <span class=\"attempts__muted\">\u2014</span> }\n </ng-template>\n <ng-template hmCell=\"client\" let-a>\n @if (a.client_name) { {{ a.client_name }} } @else { <span class=\"attempts__muted\">\u2014</span> }\n </ng-template>\n <ng-template hmCell=\"user_agent\" let-a>\n @if (a.user_agent) { <span class=\"attempts__ua\" [attr.title]=\"a.user_agent\">{{ a.user_agent }}</span> } @else { <span class=\"attempts__muted\">\u2014</span> }\n </ng-template>\n </hm-data-grid>\n </hm-card>\n</div>\n", styles: [":host{display:block}.attempts{display:flex;flex-direction:column;gap:var(--hm-space-md, 16px)}.attempts__actions{display:flex;justify-content:flex-end}.attempts__filters{display:flex;flex-direction:column;gap:var(--hm-space-md, 16px)}.attempts__row{display:flex;align-items:flex-end;flex-wrap:wrap;gap:var(--hm-space-sm, 12px)}.attempts__row+.attempts__row{padding-top:var(--hm-space-md, 16px);border-top:1px solid var(--hm-divider)}.attempts__search{display:block;flex:1 1 16rem;min-width:0}.attempts__period{display:block;flex:0 0 auto}.attempts__field{display:block;flex:0 1 12rem;min-width:0}.attempts__field--wide{flex-basis:18rem}.attempts__buttons{display:inline-flex;flex-wrap:wrap;gap:var(--hm-space-sm, 8px)}.attempts__person{display:inline-flex;align-items:center;gap:var(--hm-space-sm);min-width:0}.attempts__person hm-account-avatar{flex:none}.attempts__user{display:inline-flex;flex-direction:column;min-width:0;line-height:1.25}.attempts__user-name{font-weight:600}.attempts__user-detail{color:var(--hm-muted);font-size:.8rem}.attempts__ip{font-family:var(--hm-font-mono, monospace);font-size:.85rem}.attempts__ua{display:inline-block;max-width:22rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;vertical-align:bottom}.attempts__muted{color:var(--hm-muted)}\n"] }]
1535
1539
  }], ctorParameters: () => [], propDecorators: { scope: [{ type: i0.Input, args: [{ isSignal: true, alias: "scope", required: true }] }], clientId: [{ type: i0.Input, args: [{ isSignal: true, alias: "clientId", required: false }] }], realmId: [{ type: i0.Input, args: [{ isSignal: true, alias: "realmId", required: false }] }], adminApi: [{ type: i0.Input, args: [{ isSignal: true, alias: "adminApi", required: false }] }] } });
1536
1540
 
1537
1541
  /*