rl-core-front 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/package.json +1 -1
  2. package/src/_services/api/app-error-code.enum.ts +23 -0
  3. package/src/_services/api/schema.d.ts +86 -132
  4. package/src/_services/auth/index.ts +2 -0
  5. package/src/_utils/filter.ts +15 -34
  6. package/src/_utils/password.ts +40 -0
  7. package/src/_utils/permission.ts +14 -0
  8. package/src/_utils/storage.ts +64 -0
  9. package/src/_utils/user.ts +19 -0
  10. package/src/components/app-shell.tsx +85 -11
  11. package/src/components/brand-header.tsx +23 -8
  12. package/src/components/brand-panel.tsx +41 -0
  13. package/src/components/index.ts +1 -0
  14. package/src/components/password-field.tsx +49 -22
  15. package/src/components/ui/alert.tsx +34 -5
  16. package/src/components/ui/badge.tsx +2 -2
  17. package/src/components/ui/button.tsx +5 -0
  18. package/src/components/ui/confirm-dialog.tsx +3 -1
  19. package/src/components/ui/data-table.tsx +13 -4
  20. package/src/components/ui/dialog.tsx +120 -21
  21. package/src/components/ui/filter-sheet.tsx +47 -25
  22. package/src/components/ui/index.ts +3 -1
  23. package/src/components/ui/password-requirements.tsx +105 -0
  24. package/src/components/ui/segmented-control.tsx +75 -0
  25. package/src/components/ui/simple-select.tsx +74 -0
  26. package/src/components/ui/switch.tsx +3 -1
  27. package/src/components/ui/tabs.tsx +4 -8
  28. package/src/components/ui/track.styles.ts +31 -0
  29. package/src/contexts/brand-context.tsx +15 -0
  30. package/src/contexts/color-mode-context.tsx +4 -2
  31. package/src/contexts/i18n-context.tsx +72 -6
  32. package/src/contexts/socket-context.tsx +27 -1
  33. package/src/features/audit/audit-screen.tsx +13 -140
  34. package/src/features/audit/components/audit-diff.tsx +156 -0
  35. package/src/features/audit/components/index.ts +3 -0
  36. package/src/features/audit/components/record-audit-button.tsx +176 -0
  37. package/src/features/audit/enums/audit-data-action.enum.ts +20 -0
  38. package/src/features/audit/hooks/use-audit-trail.ts +9 -2
  39. package/src/features/audit/services/audit.service.ts +43 -1
  40. package/src/features/login/components/backup-codes-dialog.tsx +4 -4
  41. package/src/features/login/components/login-form.tsx +18 -3
  42. package/src/features/login/components/two-factor-setup.tsx +32 -2
  43. package/src/features/login/hooks/use-login-flow.ts +60 -7
  44. package/src/features/login/login-screen.tsx +99 -40
  45. package/src/features/logs/enums/error-type.enum.ts +21 -0
  46. package/src/features/logs/enums/request-outcome.enum.ts +14 -0
  47. package/src/features/logs/hooks/use-request-logs.ts +26 -3
  48. package/src/features/logs/logs-screen.tsx +2 -14
  49. package/src/features/logs/panels/index.ts +0 -1
  50. package/src/features/logs/panels/request-logs-panel.tsx +177 -25
  51. package/src/features/logs/services/logs.service.ts +13 -51
  52. package/src/features/notifications/components/notification-item.tsx +10 -8
  53. package/src/features/notifications/enums/notification-type.enum.ts +8 -0
  54. package/src/features/profile/force-password-change.tsx +18 -3
  55. package/src/features/profile/profile-screen.tsx +10 -2
  56. package/src/features/rbac/panels/groups-panel.tsx +78 -30
  57. package/src/features/rbac/panels/roles-panel.tsx +2 -2
  58. package/src/features/rbac/services/rbac.service.ts +23 -27
  59. package/src/features/recovery/reset-password-screen.tsx +16 -2
  60. package/src/features/users/components/user-form-dialog.tsx +65 -9
  61. package/src/features/users/hooks/use-users.ts +24 -3
  62. package/src/features/users/services/users.service.ts +16 -0
  63. package/src/features/users/users-screen.tsx +17 -12
  64. package/src/hooks/use-column-visibility.ts +13 -12
  65. package/src/hooks/use-countdown.ts +70 -0
  66. package/src/hooks/use-filters.ts +25 -2
  67. package/src/hooks/use-list-query.ts +14 -1
  68. package/src/hooks/use-request.ts +21 -2
  69. package/src/i18n/messages/en.ts +37 -6
  70. package/src/i18n/messages/pt.ts +41 -6
  71. package/src/index.ts +15 -1
  72. package/src/styles/core.css +99 -2
  73. package/src/styles/fonts/poppins-latin-400.woff2 +0 -0
  74. package/src/styles/fonts/poppins-latin-500.woff2 +0 -0
  75. package/src/styles/fonts/poppins-latin-600.woff2 +0 -0
  76. package/src/styles/fonts/poppins-latin-700.woff2 +0 -0
  77. package/tailwind-preset.ts +17 -0
  78. package/src/components/ui/filter-chips.tsx +0 -88
  79. package/src/features/logs/hooks/use-error-logs.ts +0 -65
  80. package/src/features/logs/panels/error-logs-panel.tsx +0 -183
@@ -4,6 +4,7 @@ import { AlertTriangle } from "lucide-react";
4
4
  import type { JSX } from "react";
5
5
 
6
6
  import {
7
+ Alert,
7
8
  Button,
8
9
  Dialog,
9
10
  DialogContent,
@@ -43,10 +44,9 @@ export function BackupCodesDialog({ open, codes, onClose }: Props): JSX.Element
43
44
  <DialogTitle>{t("auth.backupTitle")}</DialogTitle>
44
45
  </DialogHeader>
45
46
 
46
- <div className="flex items-start gap-2 rounded-md border border-warning/30 bg-warning/10 px-3 py-2 text-sm text-warning">
47
- <AlertTriangle className="mt-0.5 h-4 w-4 shrink-0" />
48
- <span>{t("auth.backupWarning")}</span>
49
- </div>
47
+ <Alert variant="warning" icon={<AlertTriangle />}>
48
+ {t("auth.backupWarning")}
49
+ </Alert>
50
50
 
51
51
  <div className="grid grid-cols-2 gap-2 rounded-lg bg-muted p-4 font-mono text-sm text-foreground">
52
52
  {codes.map((c) => (
@@ -17,16 +17,29 @@ import {
17
17
  interface Props {
18
18
  loading: boolean;
19
19
  onSubmit: (email: string, password: string) => void;
20
+ /**
21
+ * Email já preenchido. Serve a quem foi devolvido para cá porque a
22
+ * verificação de 2FA venceu: a senha o sistema não pode guardar, mas fazer a
23
+ * pessoa redigitar o email também seria gratuito.
24
+ */
25
+ defaultEmail?: string;
20
26
  }
21
27
 
22
- export function LoginForm({ loading, onSubmit }: Props): JSX.Element {
28
+ export function LoginForm({
29
+ loading,
30
+ onSubmit,
31
+ defaultEmail,
32
+ }: Props): JSX.Element {
23
33
  const { t } = useI18n();
24
34
  const schema = useMemo(() => makeLoginSchema(t), [t]);
25
35
  const {
26
36
  register,
27
37
  handleSubmit,
28
38
  formState: { errors },
29
- } = useForm<LoginFormData>({ resolver: yupResolver(schema) });
39
+ } = useForm<LoginFormData>({
40
+ resolver: yupResolver(schema),
41
+ defaultValues: { email: defaultEmail ?? "", password: "" },
42
+ });
30
43
 
31
44
  return (
32
45
  <form
@@ -37,13 +50,15 @@ export function LoginForm({ loading, onSubmit }: Props): JSX.Element {
37
50
  <Input
38
51
  type="email"
39
52
  autoComplete="email"
40
- autoFocus
53
+ // Com o email já preenchido, o cursor vai direto para o que falta.
54
+ autoFocus={!defaultEmail}
41
55
  {...register("email")}
42
56
  />
43
57
  </Field>
44
58
  <Field label={t("auth.password")} error={errors.password?.message}>
45
59
  <PasswordField
46
60
  autoComplete="current-password"
61
+ autoFocus={!!defaultEmail}
47
62
  error={!!errors.password}
48
63
  {...register("password")}
49
64
  />
@@ -13,15 +13,25 @@ import {
13
13
  CodeFormData,
14
14
  makeCodeSchema,
15
15
  } from "#core/features/login/validation/schemas";
16
+ import { useCountdown } from "#core/hooks/use-countdown";
17
+ import { cn } from "#core/lib/utils";
16
18
 
17
19
  interface Props {
18
20
  setup: TotpSetup;
19
21
  loading: boolean;
20
22
  onConfirm: (code: string) => void;
23
+ /** Prazo para confirmar, em segundos. `null` esconde o contador. */
24
+ expiresInSeconds?: number | null;
21
25
  }
22
26
 
23
- export function TwoFactorSetup({ setup, loading, onConfirm }: Props): JSX.Element {
27
+ export function TwoFactorSetup({
28
+ setup,
29
+ loading,
30
+ onConfirm,
31
+ expiresInSeconds = null,
32
+ }: Props): JSX.Element {
24
33
  const { t } = useI18n();
34
+ const { formatted, remaining, expired } = useCountdown(expiresInSeconds);
25
35
  const schema = useMemo(() => makeCodeSchema(t), [t]);
26
36
  const {
27
37
  register,
@@ -62,7 +72,27 @@ export function TwoFactorSetup({ setup, loading, onConfirm }: Props): JSX.Elemen
62
72
  {...register("code")}
63
73
  />
64
74
  </Field>
65
- <Button type="submit" size="lg" disabled={loading}>
75
+ {/*
76
+ O prazo fica à vista porque é justamente aqui que a pessoa demora —
77
+ instalar o app, escanear, digitar. Antes ele vencia em silêncio e o
78
+ código certo voltava como inválido.
79
+ */}
80
+ {expiresInSeconds !== null && (
81
+ <p
82
+ className={cn(
83
+ "text-center text-xs",
84
+ // Último minuto: deixa de ser informação e passa a ser aviso.
85
+ remaining <= 60 ? "text-warning-strong" : "text-muted-foreground",
86
+ )}
87
+ aria-live="polite"
88
+ >
89
+ {expired
90
+ ? t("auth.expiredHint")
91
+ : t("auth.expiresIn", { time: formatted })}
92
+ </p>
93
+ )}
94
+
95
+ <Button type="submit" size="lg" disabled={loading || expired}>
66
96
  {loading ? t("auth.confirming") : t("auth.confirmActivate")}
67
97
  </Button>
68
98
  </form>
@@ -8,6 +8,7 @@ import {
8
8
  useState,
9
9
  } from "react";
10
10
 
11
+ import { AppErrorCode } from "#core/_services/api/app-error-code.enum";
11
12
  import { authService, TotpSetup } from "#core/_services/auth";
12
13
  import { useAuth, useI18n, useToast } from "#core/contexts";
13
14
  import { useRequest } from "#core/hooks/use-request";
@@ -26,6 +27,15 @@ export interface UseLoginFlowResult {
26
27
  loading: boolean;
27
28
  error: string | null;
28
29
  setError: Dispatch<SetStateAction<string | null>>;
30
+ /**
31
+ * Aviso que explica por que a tela voltou ao início. Diferente de `error`,
32
+ * atravessa a troca de passo.
33
+ */
34
+ notice: string | null;
35
+ /** Último email digitado — repõe o campo quando a verificação vence. */
36
+ email: string;
37
+ /** Prazo da etapa do código, em segundos. `null` fora dessa etapa. */
38
+ expiresInSeconds: number | null;
29
39
  submitCredentials: (email: string, password: string) => Promise<void>;
30
40
  submitCode: (
31
41
  code: string,
@@ -46,15 +56,52 @@ export function useLoginFlow(): UseLoginFlowResult {
46
56
  const [pendingToken, setPendingToken] = useState("");
47
57
  const [setup, setSetup] = useState<TotpSetup | null>(null);
48
58
  const [backupCodes, setBackupCodes] = useState<string[]>([]);
59
+ const [email, setEmail] = useState("");
60
+ const [notice, setNotice] = useState<string | null>(null);
61
+ const [expiresInSeconds, setExpiresInSeconds] = useState<number | null>(null);
62
+
63
+ /**
64
+ * A janela entre a senha e o código venceu.
65
+ *
66
+ * Devolve ao primeiro passo em vez de deixar a pessoa presa numa tela cujo
67
+ * botão só responde erro — que era o que acontecia no primeiro acesso: o
68
+ * usuário instalava o autenticador, digitava o código certo e recebia
69
+ * "inválido", sem caminho nenhum. O email volta preenchido, e o QR do próximo
70
+ * login é o mesmo (o backend reaproveita o segredo pendente), então quem já
71
+ * escaneou não precisa escanear de novo.
72
+ */
73
+ const onExpired = useCallback(
74
+ ({ code }: { code: string | null }) => {
75
+ if (code !== AppErrorCode.PENDING_TOKEN_EXPIRED) {
76
+ return;
77
+ }
78
+ setStep("credentials");
79
+ setSetup(null);
80
+ setPendingToken("");
81
+ setExpiresInSeconds(null);
82
+ /*
83
+ * Vai em `notice`, e não em `error`: a tela limpa o erro a cada troca de
84
+ * passo, e é justamente um passo que estamos trocando aqui — o aviso
85
+ * sumiria no mesmo render em que aparece.
86
+ */
87
+ setNotice(t("auth.verificationExpired"));
88
+ },
89
+ [t],
90
+ );
49
91
 
50
92
  const submitCredentials = useCallback(
51
93
  async (email: string, password: string) => {
94
+ setEmail(email);
95
+ setNotice(null);
52
96
  const res = await run(() => authService.login(email, password));
53
97
  if (!res) {return;}
54
98
  setPendingToken(res.pendingToken);
99
+ setExpiresInSeconds(res.expiresInSeconds);
55
100
 
56
101
  if (res.status === "SETUP_REQUIRED") {
57
- const s = await run(() => authService.setupTotp(res.pendingToken));
102
+ const s = await run(() => authService.setupTotp(res.pendingToken), {
103
+ onError: onExpired,
104
+ });
58
105
  if (!s) {return;}
59
106
  setSetup(s);
60
107
  setStep("setup");
@@ -70,13 +117,14 @@ export function useLoginFlow(): UseLoginFlowResult {
70
117
  .catch(() => {});
71
118
  }
72
119
  },
73
- [run, notify, t],
120
+ [run, notify, t, onExpired],
74
121
  );
75
122
 
76
123
  const submitCode = useCallback(
77
124
  async (code: string, method?: "totp" | "email" | "backup") => {
78
- const res = await run(() =>
79
- authService.verify(pendingToken, code, method),
125
+ const res = await run(
126
+ () => authService.verify(pendingToken, code, method),
127
+ { onError: onExpired },
80
128
  );
81
129
  if (!res) {return;}
82
130
 
@@ -89,13 +137,15 @@ export function useLoginFlow(): UseLoginFlowResult {
89
137
  router.replace("/dashboard");
90
138
  }
91
139
  },
92
- [run, pendingToken, notify, router, t, refreshProfile],
140
+ [run, pendingToken, notify, router, t, refreshProfile, onExpired],
93
141
  );
94
142
 
95
143
  const requestEmailCode = useCallback(async () => {
96
- const res = await run(() => authService.requestEmailCode(pendingToken));
144
+ const res = await run(() => authService.requestEmailCode(pendingToken), {
145
+ onError: onExpired,
146
+ });
97
147
  if (res?.sent) {notify(t("auth.codeSent"), "success");}
98
- }, [run, pendingToken, notify, t]);
148
+ }, [run, pendingToken, notify, t, onExpired]);
99
149
 
100
150
  const finishBackup = useCallback(async () => {
101
151
  await refreshProfile();
@@ -111,6 +161,9 @@ export function useLoginFlow(): UseLoginFlowResult {
111
161
  loading,
112
162
  error,
113
163
  setError,
164
+ notice,
165
+ email,
166
+ expiresInSeconds,
114
167
  submitCredentials,
115
168
  submitCode,
116
169
  requestEmailCode,
@@ -1,11 +1,17 @@
1
1
  "use client";
2
2
 
3
+ import { Clock } from "lucide-react";
3
4
  import type { JSX } from "react";
4
5
  import { useEffect } from "react";
5
6
 
6
- import { BrandHeader, LanguageSelector, ThemeToggle } from "#core/components";
7
- import { Card } from "#core/components/ui";
8
- import { useI18n } from "#core/contexts";
7
+ import {
8
+ BrandHeader,
9
+ BrandPanel,
10
+ LanguageSelector,
11
+ ThemeToggle,
12
+ } from "#core/components";
13
+ import { Alert, Card, Separator } from "#core/components/ui";
14
+ import { useBrand, useI18n } from "#core/contexts";
9
15
  import {
10
16
  BackupCodesDialog,
11
17
  LoginForm,
@@ -13,6 +19,7 @@ import {
13
19
  TwoFactorVerify,
14
20
  } from "#core/features/login/components";
15
21
  import { useLoginFlow } from "#core/features/login/hooks/use-login-flow";
22
+ import { cn } from "#core/lib/utils";
16
23
 
17
24
  export function LoginScreen(): JSX.Element {
18
25
  const flow = useLoginFlow();
@@ -30,50 +37,102 @@ export function LoginScreen(): JSX.Element {
30
37
  // eslint-disable-next-line react-hooks/exhaustive-deps
31
38
  }, [flow.step]);
32
39
 
33
- return (
34
- <div className="relative flex min-h-screen items-center justify-center bg-background p-4">
35
- <div className="absolute right-3 top-3 flex gap-1">
36
- <LanguageSelector />
37
- <ThemeToggle />
38
- </div>
40
+ const { loginLayout } = useBrand();
41
+ const aoLado = loginLayout !== "top";
39
42
 
40
- <Card className="w-full max-w-md p-8">
41
- {/* A marca vem antes do título do passo: é a primeira tela do sistema,
42
- e quem chega aqui precisa reconhecer onde está antes de logar. */}
43
- <BrandHeader title={titles[flow.step]} />
43
+ const conteudo = (
44
+ <>
45
+ {/* A marca vem antes do título do passo: é a primeira tela do sistema, e
46
+ quem chega aqui precisa reconhecer onde está antes de logar. Com a
47
+ marca ao lado, só o título — repeti-la aqui seria dizer duas vezes. */}
48
+ <BrandHeader title={titles[flow.step]} showBrand={!aoLado} />
44
49
 
45
- {flow.error && (
46
- <div className="mb-4 rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm text-destructive">
47
- {flow.error}
48
- </div>
49
- )}
50
+ {/* Explica por que a tela voltou ao início; não é falha de credencial. */}
51
+ {flow.notice && (
52
+ <Alert variant="warning" icon={<Clock />} className="mb-4">
53
+ {flow.notice}
54
+ </Alert>
55
+ )}
50
56
 
51
- {flow.step === "credentials" && (
52
- <LoginForm loading={flow.loading} onSubmit={flow.submitCredentials} />
53
- )}
57
+ {flow.error && (
58
+ <Alert variant="error" className="mb-4">
59
+ {flow.error}
60
+ </Alert>
61
+ )}
54
62
 
55
- {flow.step === "setup" && flow.setup && (
56
- <TwoFactorSetup
57
- setup={flow.setup}
58
- loading={flow.loading}
59
- onConfirm={(code) => flow.submitCode(code, "totp")}
60
- />
61
- )}
63
+ {flow.step === "credentials" && (
64
+ <LoginForm
65
+ loading={flow.loading}
66
+ onSubmit={flow.submitCredentials}
67
+ defaultEmail={flow.email}
68
+ />
69
+ )}
62
70
 
63
- {flow.step === "verify" && (
64
- <TwoFactorVerify
65
- loading={flow.loading}
66
- onVerify={flow.submitCode}
67
- onRequestEmail={flow.requestEmailCode}
68
- />
69
- )}
71
+ {flow.step === "setup" && flow.setup && (
72
+ <TwoFactorSetup
73
+ setup={flow.setup}
74
+ loading={flow.loading}
75
+ expiresInSeconds={flow.expiresInSeconds}
76
+ onConfirm={(code) => flow.submitCode(code, "totp")}
77
+ />
78
+ )}
70
79
 
71
- <BackupCodesDialog
72
- open={flow.step === "backup"}
73
- codes={flow.backupCodes}
74
- onClose={flow.finishBackup}
80
+ {flow.step === "verify" && (
81
+ <TwoFactorVerify
82
+ loading={flow.loading}
83
+ onVerify={flow.submitCode}
84
+ onRequestEmail={flow.requestEmailCode}
75
85
  />
76
- </Card>
86
+ )}
87
+
88
+ <BackupCodesDialog
89
+ open={flow.step === "backup"}
90
+ codes={flow.backupCodes}
91
+ onClose={flow.finishBackup}
92
+ />
93
+ </>
94
+ );
95
+
96
+ const seletores = (
97
+ <div className="absolute right-3 top-3 z-10 flex gap-1">
98
+ <LanguageSelector />
99
+ <ThemeToggle />
100
+ </div>
101
+ );
102
+
103
+ if (aoLado) {
104
+ return (
105
+ <div className="relative min-h-screen bg-background">
106
+ {seletores}
107
+ {/* Empilhado em tela estreita, lado a lado a partir do `lg`. No DOM a
108
+ marca vem primeiro — no celular é ela que deve abrir a tela, nos
109
+ dois casos. Quem troca os lados no desktop é o `order`, e o divisor
110
+ precisa do dele para não sobrar na ponta. */}
111
+ <div className="flex min-h-screen flex-col lg:flex-row">
112
+ <BrandPanel
113
+ className={loginLayout === "right" ? "lg:order-3" : "lg:order-1"}
114
+ />
115
+ <Separator
116
+ orientation="horizontal"
117
+ className="lg:order-2 lg:h-auto lg:w-px lg:self-stretch"
118
+ />
119
+ <div
120
+ className={cn(
121
+ "flex flex-1 items-center justify-center p-4",
122
+ loginLayout === "right" ? "lg:order-1" : "lg:order-3",
123
+ )}
124
+ >
125
+ <Card className="w-full max-w-md p-8">{conteudo}</Card>
126
+ </div>
127
+ </div>
128
+ </div>
129
+ );
130
+ }
131
+
132
+ return (
133
+ <div className="relative flex min-h-screen items-center justify-center bg-background p-4">
134
+ {seletores}
135
+ <Card className="w-full max-w-md p-8">{conteudo}</Card>
77
136
  </div>
78
137
  );
79
138
  }
@@ -0,0 +1,21 @@
1
+ // Categoria do erro (espelha o ErrorType do backend: request_logs.error_type).
2
+ export enum ErrorType {
3
+ VALIDATION = 0,
4
+ AUTH = 1,
5
+ NOT_FOUND = 2,
6
+ RATE_LIMIT = 3,
7
+ DATABASE = 4,
8
+ INTEGRATION = 5,
9
+ INTERNAL = 6,
10
+ }
11
+
12
+ /** Chave de i18n de cada valor — o texto mora em `i18n/messages` (§6). */
13
+ export const ErrorTypeLabelKey: Record<ErrorType, string> = {
14
+ [ErrorType.VALIDATION]: "logs.errorValidation",
15
+ [ErrorType.AUTH]: "logs.errorAuth",
16
+ [ErrorType.NOT_FOUND]: "logs.errorNotFound",
17
+ [ErrorType.RATE_LIMIT]: "logs.errorRateLimit",
18
+ [ErrorType.DATABASE]: "logs.errorDatabase",
19
+ [ErrorType.INTEGRATION]: "logs.errorIntegration",
20
+ [ErrorType.INTERNAL]: "logs.errorInternal",
21
+ };
@@ -0,0 +1,14 @@
1
+ // Desfecho da requisição (espelha o RequestOutcome do backend:
2
+ // request_logs.outcome).
3
+ export enum RequestOutcome {
4
+ SUCCESS = 0,
5
+ ERROR = 1,
6
+ ABORTED = 2,
7
+ }
8
+
9
+ /** Chave de i18n de cada valor — o texto mora em `i18n/messages` (§6). */
10
+ export const RequestOutcomeLabelKey: Record<RequestOutcome, string> = {
11
+ [RequestOutcome.SUCCESS]: "logs.outcomeSuccess",
12
+ [RequestOutcome.ERROR]: "logs.outcomeError",
13
+ [RequestOutcome.ABORTED]: "logs.outcomeAborted",
14
+ };
@@ -9,11 +9,23 @@ import {
9
9
  useState,
10
10
  } from "react";
11
11
 
12
+ import type { FilterValues } from "#core/_utils/filter";
13
+ import { RequestOutcome } from "#core/features/logs/enums/request-outcome.enum";
12
14
  import { logsService, RequestLog } from "#core/features/logs/services/logs.service";
13
15
  import type { FiltersState } from "#core/hooks/use-filters";
14
16
  import { useListQuery } from "#core/hooks/use-list-query";
15
17
  import { useRequest } from "#core/hooks/use-request";
16
18
 
19
+ /**
20
+ * A tela abre no sucesso, não em tudo.
21
+ *
22
+ * Fora do módulo para não recriar o objeto a cada render: ele é dependência do
23
+ * efeito de hidratação, e um literal novo por render o refaria sem parar.
24
+ */
25
+ const DEFAULT_FILTER: FilterValues = {
26
+ outcome: { values: [String(RequestOutcome.SUCCESS)] },
27
+ };
28
+
17
29
  export interface UseRequestLogsResult {
18
30
  rows: RequestLog[];
19
31
  total: number;
@@ -32,7 +44,11 @@ export interface UseRequestLogsResult {
32
44
  export function useRequestLogs(): UseRequestLogsResult {
33
45
  const { run, loading } = useRequest();
34
46
 
35
- const list = useListQuery("/audit/changes/filter-schema", 20);
47
+ const list = useListQuery(
48
+ "/audit/requests/filter-schema",
49
+ 20,
50
+ DEFAULT_FILTER,
51
+ );
36
52
 
37
53
  const [rows, setRows] = useState<RequestLog[]>([]);
38
54
  const [total, setTotal] = useState(0);
@@ -45,9 +61,14 @@ export function useRequestLogs(): UseRequestLogsResult {
45
61
  }
46
62
  }, [run, list.query]);
47
63
 
64
+ // Não busca antes da URL ser lida: a query sairia sem o filtro, e essa
65
+ // resposta pode chegar depois da certa e pintar a tela com tudo.
48
66
  useEffect(() => {
67
+ if (!list.ready) {
68
+ return;
69
+ }
49
70
  void fetch();
50
- }, [fetch]);
71
+ }, [fetch, list.ready]);
51
72
 
52
73
  return {
53
74
  rows,
@@ -55,7 +76,9 @@ export function useRequestLogs(): UseRequestLogsResult {
55
76
  page: list.page,
56
77
  pageSize: list.pageSize,
57
78
  sorting: list.sorting,
58
- loading,
79
+ // Esperar o catálogo também é carregar: sem isso a tabela pisca
80
+ // "Nenhum registro" antes da primeira busca sair.
81
+ loading: loading || !list.ready,
59
82
  setPage: list.setPage,
60
83
  setPageSize: list.setPageSize,
61
84
  setSorting: list.setSorting,
@@ -2,9 +2,8 @@
2
2
 
3
3
  import type { JSX } from "react";
4
4
 
5
- import { Tabs, TabsContent, TabsList, TabsTrigger } from "#core/components/ui";
6
5
  import { useI18n } from "#core/contexts";
7
- import { ErrorLogsPanel, RequestLogsPanel } from "#core/features/logs/panels";
6
+ import { RequestLogsPanel } from "#core/features/logs/panels";
8
7
 
9
8
  export function LogsScreen(): JSX.Element {
10
9
  const { t } = useI18n();
@@ -14,18 +13,7 @@ export function LogsScreen(): JSX.Element {
14
13
  <h1 className="mb-1 text-3xl font-bold">{t("logs.title")}</h1>
15
14
  <p className="mb-4 text-sm text-muted-foreground">{t("logs.subtitle")}</p>
16
15
 
17
- <Tabs defaultValue="requests">
18
- <TabsList>
19
- <TabsTrigger value="requests">{t("logs.tabRequests")}</TabsTrigger>
20
- <TabsTrigger value="errors">{t("logs.tabErrors")}</TabsTrigger>
21
- </TabsList>
22
- <TabsContent value="requests">
23
- <RequestLogsPanel />
24
- </TabsContent>
25
- <TabsContent value="errors">
26
- <ErrorLogsPanel />
27
- </TabsContent>
28
- </Tabs>
16
+ <RequestLogsPanel />
29
17
  </div>
30
18
  );
31
19
  }
@@ -1,3 +1,2 @@
1
1
  // Barrel da pasta — importe daqui de fora; dentro da pasta use o caminho direto.
2
- export * from "./error-logs-panel";
3
2
  export * from "./request-logs-panel";