ltcai 4.7.0 → 4.7.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.
- package/README.md +49 -38
- package/docs/CHANGELOG.md +80 -0
- package/docs/V4_7_1_ADMIN_OPERATIONS_REPORT.md +49 -0
- package/docs/V4_7_2_INTUITIVE_BRAIN_UX_REPORT.md +62 -0
- package/docs/V4_DIGITAL_BRAIN_RECOVERY.md +16 -14
- package/frontend/src/App.tsx +191 -15
- package/frontend/src/api/client.ts +9 -1
- package/frontend/src/components/ProductFlow.tsx +89 -57
- package/frontend/src/pages/System.tsx +1 -1
- package/frontend/src/styles.css +205 -0
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/runtime/multi_agent.py +1 -1
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/admin.py +112 -3
- package/latticeai/api/chat.py +11 -3
- package/latticeai/app_factory.py +0 -2
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/workspace_os.py +1 -1
- package/package.json +2 -1
- package/src-tauri/Cargo.lock +1 -1
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/tauri.conf.json +1 -1
- package/static/app/asset-manifest.json +5 -5
- package/static/app/assets/index-DdAB4yfa.js +16 -0
- package/static/app/assets/index-DdAB4yfa.js.map +1 -0
- package/static/app/assets/{index-DFmuiJ6t.css → index-KlQ04wVv.css} +1 -1
- package/static/app/index.html +2 -2
- package/scripts/launch-pts-grok.sh +0 -56
- package/static/app/assets/index-DwX3rNfA.js +0 -16
- package/static/app/assets/index-DwX3rNfA.js.map +0 -1
package/frontend/src/App.tsx
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
Download,
|
|
10
10
|
Eye,
|
|
11
11
|
ImagePlus,
|
|
12
|
+
ListFilter,
|
|
12
13
|
RotateCcw,
|
|
13
14
|
Search,
|
|
14
15
|
Send,
|
|
@@ -16,7 +17,7 @@ import {
|
|
|
16
17
|
ShieldCheck,
|
|
17
18
|
Users,
|
|
18
19
|
} from "lucide-react";
|
|
19
|
-
import { latticeApi, type ApiResult } from "@/api/client";
|
|
20
|
+
import { latticeApi, type AdminAuditFilters, type ApiResult } from "@/api/client";
|
|
20
21
|
import { Button } from "@/components/ui/button";
|
|
21
22
|
import { type BrainState, LivingBrain, triggerBrainRecall } from "@/components/LivingBrain";
|
|
22
23
|
import { ProductFlow, readProductFlowComplete } from "@/components/ProductFlow";
|
|
@@ -25,6 +26,7 @@ import { asArray } from "@/lib/utils";
|
|
|
25
26
|
|
|
26
27
|
type ApiRecord = Record<string, unknown>;
|
|
27
28
|
type BrainDepth = 1 | 2 | 3 | 4 | 5;
|
|
29
|
+
type AdminFilterState = Required<Pick<AdminAuditFilters, "q" | "actor" | "action" | "severity">> & { limit: number };
|
|
28
30
|
|
|
29
31
|
type Message = {
|
|
30
32
|
role: "user" | "assistant";
|
|
@@ -159,6 +161,7 @@ function BrainHome({
|
|
|
159
161
|
const [explorationDepth, setExplorationDepth] = React.useState<BrainDepth>(1);
|
|
160
162
|
const [graphSearch, setGraphSearch] = React.useState("");
|
|
161
163
|
const [selectedGraphId, setSelectedGraphId] = React.useState<string | null>(null);
|
|
164
|
+
const [memoryFeedback, setMemoryFeedback] = React.useState<string | null>(null);
|
|
162
165
|
const streamRef = React.useRef<HTMLDivElement>(null);
|
|
163
166
|
const recallTimerRef = React.useRef<number | null>(null);
|
|
164
167
|
|
|
@@ -210,6 +213,7 @@ function BrainHome({
|
|
|
210
213
|
setDraft("");
|
|
211
214
|
setImageData(null);
|
|
212
215
|
setStreaming(true);
|
|
216
|
+
setMemoryFeedback(null);
|
|
213
217
|
onBrainChange("thinking", 0.96);
|
|
214
218
|
|
|
215
219
|
try {
|
|
@@ -238,6 +242,8 @@ function BrainHome({
|
|
|
238
242
|
next[next.length - 1] = { role: "assistant", content: `Unavailable: ${result.error}` };
|
|
239
243
|
return next;
|
|
240
244
|
});
|
|
245
|
+
} else {
|
|
246
|
+
setMemoryFeedback(`기억에 저장됨 · 연결된 주제 ${knowledgeConcepts.length}개 · 관련 기억 ${memoryFragments.length}개`);
|
|
241
247
|
}
|
|
242
248
|
} finally {
|
|
243
249
|
setStreaming(false);
|
|
@@ -257,6 +263,13 @@ function BrainHome({
|
|
|
257
263
|
});
|
|
258
264
|
}
|
|
259
265
|
|
|
266
|
+
function jumpToDepth(next: BrainDepth) {
|
|
267
|
+
setExplorationDepth(next);
|
|
268
|
+
const nextDepth = DEPTHS[next - 1];
|
|
269
|
+
onBrainChange(nextDepth.state, next === 1 ? 0.58 : 0.66 + next * 0.06);
|
|
270
|
+
if (next >= 2) triggerBrainRecall();
|
|
271
|
+
}
|
|
272
|
+
|
|
260
273
|
function surface() {
|
|
261
274
|
setExplorationDepth(1);
|
|
262
275
|
setSelectedGraphId(null);
|
|
@@ -269,7 +282,7 @@ function BrainHome({
|
|
|
269
282
|
setExplorationDepth((depth) => Math.max(depth, 2) as BrainDepth);
|
|
270
283
|
setMessages((items) => [
|
|
271
284
|
...items,
|
|
272
|
-
{ role: "assistant", content:
|
|
285
|
+
{ role: "assistant", content: `기억을 다시 꺼냈습니다: ${fragment.title}` },
|
|
273
286
|
]);
|
|
274
287
|
}
|
|
275
288
|
|
|
@@ -291,6 +304,13 @@ function BrainHome({
|
|
|
291
304
|
<strong>{currentDepth.label}</strong>
|
|
292
305
|
</div>
|
|
293
306
|
|
|
307
|
+
<div className="brain-depth-actions" aria-label="Brain quick views">
|
|
308
|
+
<button type="button" className={explorationDepth === 2 ? "is-active" : ""} onClick={() => jumpToDepth(2)}>기억 보기</button>
|
|
309
|
+
<button type="button" className={explorationDepth === 3 ? "is-active" : ""} onClick={() => jumpToDepth(3)}>주제 보기</button>
|
|
310
|
+
<button type="button" className={explorationDepth === 4 ? "is-active" : ""} onClick={() => jumpToDepth(4)}>관계 보기</button>
|
|
311
|
+
<button type="button" className={explorationDepth === 5 ? "is-active" : ""} onClick={() => jumpToDepth(5)}>그래프로 보기</button>
|
|
312
|
+
</div>
|
|
313
|
+
|
|
294
314
|
<div className="brain-field-layer" aria-hidden={explorationDepth < 2}>
|
|
295
315
|
<DepthEmergence
|
|
296
316
|
depth={explorationDepth}
|
|
@@ -333,12 +353,17 @@ function BrainHome({
|
|
|
333
353
|
</div>
|
|
334
354
|
|
|
335
355
|
<div ref={streamRef} className="brain-stream">
|
|
356
|
+
<BrainOverviewPanel
|
|
357
|
+
memories={memoryFragments}
|
|
358
|
+
concepts={knowledgeConcepts}
|
|
359
|
+
onOpenDepth={jumpToDepth}
|
|
360
|
+
/>
|
|
336
361
|
{messages.length === 0 ? (
|
|
337
362
|
<div className="mind-empty">
|
|
338
|
-
<div className="mind-empty-kicker"
|
|
339
|
-
<div className="mind-empty-title"
|
|
363
|
+
<div className="mind-empty-kicker">내 오래가는 기억</div>
|
|
364
|
+
<div className="mind-empty-title">잊으면 안 되는 일부터 말해 주세요.</div>
|
|
340
365
|
<p>
|
|
341
|
-
|
|
366
|
+
문서, 대화, 프로젝트, 결정이 Brain에 쌓이고 나중에 주제와 관계로 다시 보입니다.
|
|
342
367
|
</p>
|
|
343
368
|
<div className="mind-empty-prompts" aria-label="Starter prompts">
|
|
344
369
|
{STARTER_PROMPTS.map((prompt) => (
|
|
@@ -357,6 +382,8 @@ function BrainHome({
|
|
|
357
382
|
)}
|
|
358
383
|
</div>
|
|
359
384
|
|
|
385
|
+
{memoryFeedback ? <div className="brain-save-feedback" role="status">{memoryFeedback}</div> : null}
|
|
386
|
+
|
|
360
387
|
<BrainCarePanel />
|
|
361
388
|
|
|
362
389
|
<div className="brain-composer">
|
|
@@ -398,14 +425,8 @@ function BrainHome({
|
|
|
398
425
|
|
|
399
426
|
function AdminConsole({ onBack }: { onBack: () => void }) {
|
|
400
427
|
const qc = useQueryClient();
|
|
401
|
-
const
|
|
402
|
-
const statsQ
|
|
403
|
-
const usersQ = useQuery({ queryKey: ["adminUsers"], queryFn: latticeApi.adminUsers });
|
|
404
|
-
const auditQ = useQuery({ queryKey: ["adminAudit"], queryFn: latticeApi.adminAudit });
|
|
405
|
-
const securityQ = useQuery({ queryKey: ["adminSecurity"], queryFn: latticeApi.adminSecurity });
|
|
406
|
-
const securityEventsQ = useQuery({ queryKey: ["adminSecurityEvents"], queryFn: () => latticeApi.adminSecurityEvents(50) });
|
|
407
|
-
const policiesQ = useQuery({ queryKey: ["adminPolicies"], queryFn: latticeApi.adminPolicies });
|
|
408
|
-
const indexQ = useQuery({ queryKey: ["indexStatus"], queryFn: latticeApi.indexStatus });
|
|
428
|
+
const [filters, setFilters] = React.useState<AdminFilterState>({ q: "", actor: "", action: "", severity: "", limit: 50 });
|
|
429
|
+
const { summaryQ, statsQ, usersQ, auditQ, securityQ, securityEventsQ, policiesQ, rolesQ, retentionQ, indexQ } = useAdminConsoleData(filters);
|
|
409
430
|
const rebuildIndex = useMutation({
|
|
410
431
|
mutationFn: latticeApi.rebuildIndex,
|
|
411
432
|
onSuccess: () => void qc.invalidateQueries({ queryKey: ["indexStatus"] }),
|
|
@@ -415,6 +436,8 @@ function AdminConsole({ onBack }: { onBack: () => void }) {
|
|
|
415
436
|
const auditEvents = asArray((auditQ.data?.data as ApiRecord | undefined)?.recent_events);
|
|
416
437
|
const securityEvents = asArray((securityEventsQ.data?.data as ApiRecord | undefined)?.events);
|
|
417
438
|
const policies = asArray((policiesQ.data?.data as ApiRecord | undefined)?.policies);
|
|
439
|
+
const roles = asArray((rolesQ.data?.data as ApiRecord | undefined)?.roles);
|
|
440
|
+
const retention = (retentionQ.data?.data || {}) as ApiRecord;
|
|
418
441
|
|
|
419
442
|
return (
|
|
420
443
|
<main className="admin-console" aria-label="Lattice Admin">
|
|
@@ -469,7 +492,24 @@ function AdminConsole({ onBack }: { onBack: () => void }) {
|
|
|
469
492
|
/>
|
|
470
493
|
</AdminPanel>
|
|
471
494
|
|
|
495
|
+
<AdminPanel title="Role Permissions" eyebrow="Access">
|
|
496
|
+
<AdminList
|
|
497
|
+
items={roles.slice(0, 6)}
|
|
498
|
+
empty="No role matrix reported."
|
|
499
|
+
render={(item) => {
|
|
500
|
+
const role = item as ApiRecord;
|
|
501
|
+
return (
|
|
502
|
+
<>
|
|
503
|
+
<strong>{stringValue(role.role, "role")} · {stringValue(role.members, "0")} users</strong>
|
|
504
|
+
<span>{asArray(role.caps).slice(0, 4).map((cap) => stringValue(cap, "")).filter(Boolean).join(", ") || "No caps"}</span>
|
|
505
|
+
</>
|
|
506
|
+
);
|
|
507
|
+
}}
|
|
508
|
+
/>
|
|
509
|
+
</AdminPanel>
|
|
510
|
+
|
|
472
511
|
<AdminPanel title="Activity Logs" eyebrow="Audit">
|
|
512
|
+
<AdminLogFilters filters={filters} onChange={setFilters} matched={(auditQ.data?.data as ApiRecord | undefined)?.filters as ApiRecord | undefined} />
|
|
473
513
|
<AdminList
|
|
474
514
|
items={auditEvents.slice(0, 8)}
|
|
475
515
|
empty="No recent audit events."
|
|
@@ -499,16 +539,82 @@ function AdminConsole({ onBack }: { onBack: () => void }) {
|
|
|
499
539
|
<div className="admin-policy-strip">
|
|
500
540
|
{policies.slice(0, 5).map((item, index) => {
|
|
501
541
|
const policy = item as ApiRecord;
|
|
502
|
-
return <span key={`${stringValue(policy.id || policy.name, "policy")}-${index}`}>{stringValue(policy.name || policy.id, "Policy")}</span>;
|
|
542
|
+
return <span key={`${stringValue(policy.id || policy.name, "policy")}-${index}`}>{stringValue(policy.label || policy.name || policy.id, "Policy")}</span>;
|
|
503
543
|
})}
|
|
504
544
|
{!policies.length ? <span>Policy API quiet</span> : null}
|
|
505
545
|
</div>
|
|
546
|
+
<div className="admin-retention">
|
|
547
|
+
<strong>{stringValue(retention.retention_days, "90")} day retention</strong>
|
|
548
|
+
<span>{stringValue(retention.retained_events, "0")} retained · {stringValue(retention.prune_candidates, "0")} ready for export/prune review</span>
|
|
549
|
+
</div>
|
|
506
550
|
</AdminPanel>
|
|
507
551
|
</section>
|
|
508
552
|
</main>
|
|
509
553
|
);
|
|
510
554
|
}
|
|
511
555
|
|
|
556
|
+
function useAdminConsoleData(filters: AdminFilterState) {
|
|
557
|
+
const auditFilters = React.useMemo<AdminAuditFilters>(() => ({
|
|
558
|
+
q: filters.q || undefined,
|
|
559
|
+
actor: filters.actor || undefined,
|
|
560
|
+
action: filters.action || undefined,
|
|
561
|
+
severity: filters.severity || undefined,
|
|
562
|
+
limit: filters.limit,
|
|
563
|
+
}), [filters]);
|
|
564
|
+
|
|
565
|
+
return {
|
|
566
|
+
summaryQ: useQuery({ queryKey: ["adminSummary"], queryFn: latticeApi.adminSummary }),
|
|
567
|
+
statsQ: useQuery({ queryKey: ["adminStats"], queryFn: latticeApi.adminStats }),
|
|
568
|
+
usersQ: useQuery({ queryKey: ["adminUsers"], queryFn: latticeApi.adminUsers }),
|
|
569
|
+
auditQ: useQuery({ queryKey: ["adminAudit", auditFilters], queryFn: () => latticeApi.adminAudit(auditFilters) }),
|
|
570
|
+
securityQ: useQuery({ queryKey: ["adminSecurity"], queryFn: latticeApi.adminSecurity }),
|
|
571
|
+
securityEventsQ: useQuery({ queryKey: ["adminSecurityEvents"], queryFn: () => latticeApi.adminSecurityEvents(50) }),
|
|
572
|
+
policiesQ: useQuery({ queryKey: ["adminPolicies"], queryFn: latticeApi.adminPolicies }),
|
|
573
|
+
rolesQ: useQuery({ queryKey: ["adminRoles"], queryFn: latticeApi.adminRoles }),
|
|
574
|
+
retentionQ: useQuery({ queryKey: ["adminLogRetention"], queryFn: latticeApi.adminLogRetention }),
|
|
575
|
+
indexQ: useQuery({ queryKey: ["indexStatus"], queryFn: latticeApi.indexStatus }),
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function AdminLogFilters({
|
|
580
|
+
filters,
|
|
581
|
+
matched,
|
|
582
|
+
onChange,
|
|
583
|
+
}: {
|
|
584
|
+
filters: AdminFilterState;
|
|
585
|
+
matched?: ApiRecord;
|
|
586
|
+
onChange: React.Dispatch<React.SetStateAction<AdminFilterState>>;
|
|
587
|
+
}) {
|
|
588
|
+
return (
|
|
589
|
+
<div className="admin-log-filters" aria-label="Audit log filters">
|
|
590
|
+
<label>
|
|
591
|
+
<Search className="h-3.5 w-3.5" />
|
|
592
|
+
<input
|
|
593
|
+
value={filters.q}
|
|
594
|
+
onChange={(event) => onChange((current) => ({ ...current, q: event.target.value }))}
|
|
595
|
+
placeholder="Search logs"
|
|
596
|
+
aria-label="Search audit logs"
|
|
597
|
+
/>
|
|
598
|
+
</label>
|
|
599
|
+
<label>
|
|
600
|
+
<ListFilter className="h-3.5 w-3.5" />
|
|
601
|
+
<select
|
|
602
|
+
value={filters.severity}
|
|
603
|
+
onChange={(event) => onChange((current) => ({ ...current, severity: event.target.value }))}
|
|
604
|
+
aria-label="Filter by severity"
|
|
605
|
+
>
|
|
606
|
+
<option value="">All severities</option>
|
|
607
|
+
<option value="informational">Informational</option>
|
|
608
|
+
<option value="notice">Notice</option>
|
|
609
|
+
<option value="warning">Warning</option>
|
|
610
|
+
<option value="high">High</option>
|
|
611
|
+
</select>
|
|
612
|
+
</label>
|
|
613
|
+
<span>{stringValue(matched?.matched_events, "0")} matched</span>
|
|
614
|
+
</div>
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
|
|
512
618
|
function AdminMetric({ icon, label, value, detail }: { icon: React.ReactNode; label: string; value: string; detail: string }) {
|
|
513
619
|
return (
|
|
514
620
|
<div className="admin-metric">
|
|
@@ -757,6 +863,75 @@ function DepthEmergence({
|
|
|
757
863
|
);
|
|
758
864
|
}
|
|
759
865
|
|
|
866
|
+
function BrainOverviewPanel({
|
|
867
|
+
memories,
|
|
868
|
+
concepts,
|
|
869
|
+
onOpenDepth,
|
|
870
|
+
}: {
|
|
871
|
+
memories: MemoryFragment[];
|
|
872
|
+
concepts: KnowledgeConcept[];
|
|
873
|
+
onOpenDepth: (depth: BrainDepth) => void;
|
|
874
|
+
}) {
|
|
875
|
+
const recent = memories.slice(0, 3);
|
|
876
|
+
const older = memories.slice(3, 6);
|
|
877
|
+
const topics = concepts.slice(0, 4);
|
|
878
|
+
|
|
879
|
+
return (
|
|
880
|
+
<section className="brain-overview-panel" aria-label="Brain overview">
|
|
881
|
+
<div className="brain-overview-head">
|
|
882
|
+
<div>
|
|
883
|
+
<span>Brain 한눈에 보기</span>
|
|
884
|
+
<strong>기억과 주제를 바로 확인하세요.</strong>
|
|
885
|
+
</div>
|
|
886
|
+
<button type="button" onClick={() => onOpenDepth(5)}>전체 그래프</button>
|
|
887
|
+
</div>
|
|
888
|
+
<div className="brain-overview-grid">
|
|
889
|
+
<BrainOverviewColumn
|
|
890
|
+
title="최근 기억"
|
|
891
|
+
empty="아직 최근 기억이 없습니다."
|
|
892
|
+
items={recent.map((memory) => memory.title)}
|
|
893
|
+
onOpen={() => onOpenDepth(2)}
|
|
894
|
+
/>
|
|
895
|
+
<BrainOverviewColumn
|
|
896
|
+
title="이전 기억"
|
|
897
|
+
empty="대화가 쌓이면 과거 기억이 보입니다."
|
|
898
|
+
items={older.map((memory) => memory.title)}
|
|
899
|
+
onOpen={() => onOpenDepth(2)}
|
|
900
|
+
/>
|
|
901
|
+
<BrainOverviewColumn
|
|
902
|
+
title="주요 주제"
|
|
903
|
+
empty="주제가 형성되는 중입니다."
|
|
904
|
+
items={topics.map((concept) => concept.label)}
|
|
905
|
+
onOpen={() => onOpenDepth(3)}
|
|
906
|
+
/>
|
|
907
|
+
</div>
|
|
908
|
+
</section>
|
|
909
|
+
);
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
function BrainOverviewColumn({
|
|
913
|
+
title,
|
|
914
|
+
empty,
|
|
915
|
+
items,
|
|
916
|
+
onOpen,
|
|
917
|
+
}: {
|
|
918
|
+
title: string;
|
|
919
|
+
empty: string;
|
|
920
|
+
items: string[];
|
|
921
|
+
onOpen: () => void;
|
|
922
|
+
}) {
|
|
923
|
+
return (
|
|
924
|
+
<button type="button" className="brain-overview-column" onClick={onOpen}>
|
|
925
|
+
<span>{title}</span>
|
|
926
|
+
{items.length ? (
|
|
927
|
+
items.slice(0, 3).map((item) => <strong key={item}>{item}</strong>)
|
|
928
|
+
) : (
|
|
929
|
+
<em>{empty}</em>
|
|
930
|
+
)}
|
|
931
|
+
</button>
|
|
932
|
+
);
|
|
933
|
+
}
|
|
934
|
+
|
|
760
935
|
function MemoryLayer({
|
|
761
936
|
memories,
|
|
762
937
|
depth,
|
|
@@ -945,9 +1120,10 @@ function EmergentKnowledgeGraph({
|
|
|
945
1120
|
<span>{selected.type}</span>
|
|
946
1121
|
<strong>{selected.label}</strong>
|
|
947
1122
|
<p>{selected.summary || "This concept is part of the deepest knowledge layer."}</p>
|
|
1123
|
+
<p>대화와 문서에서 함께 나온 내용이 선으로 이어집니다.</p>
|
|
948
1124
|
</>
|
|
949
1125
|
) : (
|
|
950
|
-
<p
|
|
1126
|
+
<p>대화, 문서, 프로젝트를 쌓으면 Brain 그래프가 자랍니다.</p>
|
|
951
1127
|
)}
|
|
952
1128
|
</div>
|
|
953
1129
|
</section>
|
|
@@ -12,6 +12,13 @@ export type ApiResult<T = unknown> = {
|
|
|
12
12
|
|
|
13
13
|
type HttpMethod = "GET" | "POST" | "PATCH" | "DELETE";
|
|
14
14
|
type Query = Record<string, string | number | boolean | null | undefined>;
|
|
15
|
+
export type AdminAuditFilters = {
|
|
16
|
+
q?: string;
|
|
17
|
+
actor?: string;
|
|
18
|
+
action?: string;
|
|
19
|
+
severity?: string;
|
|
20
|
+
limit?: number;
|
|
21
|
+
};
|
|
15
22
|
|
|
16
23
|
const TIMEOUT_MS = 10_000;
|
|
17
24
|
const clients = new Map<string, ReturnType<typeof createClient<paths>>>();
|
|
@@ -419,9 +426,10 @@ export const latticeApi = {
|
|
|
419
426
|
adminSummary: () => get("/admin/summary", {}),
|
|
420
427
|
adminStats: () => get("/admin/stats", {}),
|
|
421
428
|
adminUsers: () => get("/admin/users", []),
|
|
422
|
-
adminAudit: () => get("/admin/audit", { recent_events: [] }),
|
|
429
|
+
adminAudit: (filters?: AdminAuditFilters) => get("/admin/audit", { recent_events: [], filters: {} }, filters),
|
|
423
430
|
adminRoles: () => get("/admin/roles", { roles: [] }),
|
|
424
431
|
adminPolicies: () => get("/admin/policies", { policies: [] }),
|
|
432
|
+
adminLogRetention: () => get("/admin/log-retention", {}),
|
|
425
433
|
adminProductHardening: () => get("/admin/product-hardening", {}),
|
|
426
434
|
adminSecurity: () => get("/admin/security/overview", {}),
|
|
427
435
|
adminSecurityEvents: (limit = 50) => get("/admin/security/events", { events: [] }, { limit }),
|