ltcai 1.5.0 → 1.7.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.
- package/README.md +63 -33
- package/docs/CHANGELOG.md +89 -0
- package/docs/EDITION_STRATEGY.md +4 -0
- package/docs/ENTERPRISE.md +8 -2
- package/docs/images/enterprise.png +0 -0
- package/docs/images/graph.png +0 -0
- package/docs/images/hero.gif +0 -0
- package/docs/images/model-recommendation.png +0 -0
- package/docs/images/onboarding.png +0 -0
- package/docs/images/organization.png +0 -0
- package/docs/images/skills.png +0 -0
- package/docs/images/tmp_frames/hero_00.png +0 -0
- package/docs/images/tmp_frames/hero_01.png +0 -0
- package/docs/images/tmp_frames/hero_02.png +0 -0
- package/docs/images/tmp_frames/hero_03.png +0 -0
- package/docs/images/workspace.png +0 -0
- package/latticeai/__init__.py +1 -1
- package/latticeai/core/workspace_os.py +11 -1
- package/package.json +10 -1
- package/static/admin.html +62 -0
- package/static/graph.html +7 -1
- package/static/lattice-reference.css +184 -0
- package/static/scripts/admin.js +121 -1
- package/static/scripts/chat.js +28 -7
- package/static/scripts/graph.js +296 -14
- package/static/scripts/workspace.js +363 -24
- package/static/workspace.css +140 -0
- package/static/workspace.html +95 -2
|
@@ -1555,6 +1555,91 @@
|
|
|
1555
1555
|
line-height: 1.5;
|
|
1556
1556
|
}
|
|
1557
1557
|
|
|
1558
|
+
.lattice-ref-admin .enterprise-grid {
|
|
1559
|
+
display: grid;
|
|
1560
|
+
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
|
1561
|
+
gap: 10px;
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
.lattice-ref-admin .enterprise-cap-card {
|
|
1565
|
+
display: flex;
|
|
1566
|
+
align-items: center;
|
|
1567
|
+
gap: 10px;
|
|
1568
|
+
min-width: 0;
|
|
1569
|
+
border: 1px solid rgba(111,66,232,0.12);
|
|
1570
|
+
border-radius: 8px;
|
|
1571
|
+
background: rgba(255,255,255,0.70);
|
|
1572
|
+
padding: 11px 12px;
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
.lattice-ref-admin .enterprise-cap-card i {
|
|
1576
|
+
color: #7a74a0;
|
|
1577
|
+
font-size: 18px;
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
.lattice-ref-admin .enterprise-cap-card.on i {
|
|
1581
|
+
color: #0d8f72;
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
.lattice-ref-admin .enterprise-cap-card span {
|
|
1585
|
+
flex: 1;
|
|
1586
|
+
min-width: 0;
|
|
1587
|
+
color: #14162c;
|
|
1588
|
+
font-size: 13px;
|
|
1589
|
+
font-weight: 800;
|
|
1590
|
+
overflow: hidden;
|
|
1591
|
+
text-overflow: ellipsis;
|
|
1592
|
+
white-space: nowrap;
|
|
1593
|
+
text-transform: capitalize;
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
.lattice-ref-admin .enterprise-cap-card strong {
|
|
1597
|
+
color: #4a4668;
|
|
1598
|
+
font-size: 11px;
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
.lattice-ref-admin .enterprise-kv {
|
|
1602
|
+
display: grid;
|
|
1603
|
+
gap: 8px;
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
.lattice-ref-admin .enterprise-kv div {
|
|
1607
|
+
display: grid;
|
|
1608
|
+
grid-template-columns: 150px minmax(0, 1fr);
|
|
1609
|
+
gap: 10px;
|
|
1610
|
+
align-items: start;
|
|
1611
|
+
border: 1px solid rgba(111,66,232,0.10);
|
|
1612
|
+
border-radius: 8px;
|
|
1613
|
+
background: rgba(255,255,255,0.62);
|
|
1614
|
+
padding: 9px 10px;
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
.lattice-ref-admin .enterprise-kv span {
|
|
1618
|
+
color: #4a4668;
|
|
1619
|
+
font-size: 12px;
|
|
1620
|
+
font-weight: 800;
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
.lattice-ref-admin .enterprise-kv strong {
|
|
1624
|
+
color: #14162c;
|
|
1625
|
+
font-size: 12px;
|
|
1626
|
+
line-height: 1.45;
|
|
1627
|
+
overflow-wrap: anywhere;
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
.lattice-ref-admin .enterprise-json {
|
|
1631
|
+
max-height: 280px;
|
|
1632
|
+
overflow: auto;
|
|
1633
|
+
margin: 12px 0 0;
|
|
1634
|
+
border: 1px solid rgba(111,66,232,0.10);
|
|
1635
|
+
border-radius: 8px;
|
|
1636
|
+
background: rgba(20,22,44,0.05);
|
|
1637
|
+
color: #14162c;
|
|
1638
|
+
padding: 12px;
|
|
1639
|
+
font-size: 12px;
|
|
1640
|
+
white-space: pre-wrap;
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1558
1643
|
@media (max-width: 980px) {
|
|
1559
1644
|
.lattice-ref-chat .reference-card-grid,
|
|
1560
1645
|
.reference-lists {
|
|
@@ -2784,9 +2869,11 @@ body.lattice-ref-graph {
|
|
|
2784
2869
|
top: 16px;
|
|
2785
2870
|
right: 16px;
|
|
2786
2871
|
display: flex;
|
|
2872
|
+
flex-wrap: wrap;
|
|
2787
2873
|
gap: 8px;
|
|
2788
2874
|
padding: 8px;
|
|
2789
2875
|
border-radius: 10px;
|
|
2876
|
+
max-width: min(760px, calc(100% - 32px));
|
|
2790
2877
|
}
|
|
2791
2878
|
|
|
2792
2879
|
.tb-btn {
|
|
@@ -3301,6 +3388,103 @@ body.lattice-ref-graph {
|
|
|
3301
3388
|
|
|
3302
3389
|
.jump-btn:hover { filter: brightness(1.04); }
|
|
3303
3390
|
|
|
3391
|
+
.jump-btn.secondary {
|
|
3392
|
+
border: 1px solid rgba(111,66,232,0.20);
|
|
3393
|
+
color: var(--text);
|
|
3394
|
+
background: rgba(255,255,255,0.82);
|
|
3395
|
+
box-shadow: none;
|
|
3396
|
+
cursor: pointer;
|
|
3397
|
+
font: inherit;
|
|
3398
|
+
font-size: 12px;
|
|
3399
|
+
font-weight: 700;
|
|
3400
|
+
}
|
|
3401
|
+
|
|
3402
|
+
.detail-actions {
|
|
3403
|
+
display: flex;
|
|
3404
|
+
flex-wrap: wrap;
|
|
3405
|
+
gap: 8px;
|
|
3406
|
+
margin-bottom: 14px;
|
|
3407
|
+
}
|
|
3408
|
+
|
|
3409
|
+
.related-node-list {
|
|
3410
|
+
display: grid;
|
|
3411
|
+
gap: 7px;
|
|
3412
|
+
margin-bottom: 14px;
|
|
3413
|
+
}
|
|
3414
|
+
|
|
3415
|
+
.related-node-btn {
|
|
3416
|
+
width: 100%;
|
|
3417
|
+
min-width: 0;
|
|
3418
|
+
display: grid;
|
|
3419
|
+
grid-template-columns: 18px minmax(0, 1fr) auto;
|
|
3420
|
+
align-items: center;
|
|
3421
|
+
gap: 8px;
|
|
3422
|
+
border: 1px solid rgba(111,66,232,0.13);
|
|
3423
|
+
border-radius: 8px;
|
|
3424
|
+
background: rgba(255,255,255,0.76);
|
|
3425
|
+
color: var(--text);
|
|
3426
|
+
padding: 8px 10px;
|
|
3427
|
+
text-align: left;
|
|
3428
|
+
cursor: pointer;
|
|
3429
|
+
}
|
|
3430
|
+
|
|
3431
|
+
.related-node-btn:hover {
|
|
3432
|
+
border-color: rgba(111,66,232,0.34);
|
|
3433
|
+
background: rgba(111,66,232,0.07);
|
|
3434
|
+
}
|
|
3435
|
+
|
|
3436
|
+
.related-node-btn strong {
|
|
3437
|
+
min-width: 0;
|
|
3438
|
+
overflow: hidden;
|
|
3439
|
+
text-overflow: ellipsis;
|
|
3440
|
+
white-space: nowrap;
|
|
3441
|
+
font-size: 12px;
|
|
3442
|
+
}
|
|
3443
|
+
|
|
3444
|
+
.related-node-btn em {
|
|
3445
|
+
color: var(--faint);
|
|
3446
|
+
font-size: 11px;
|
|
3447
|
+
font-style: normal;
|
|
3448
|
+
white-space: nowrap;
|
|
3449
|
+
}
|
|
3450
|
+
|
|
3451
|
+
.focus-chip {
|
|
3452
|
+
position: absolute;
|
|
3453
|
+
z-index: 21;
|
|
3454
|
+
left: 16px;
|
|
3455
|
+
bottom: 16px;
|
|
3456
|
+
max-width: min(560px, calc(100% - 32px));
|
|
3457
|
+
display: flex;
|
|
3458
|
+
flex-wrap: wrap;
|
|
3459
|
+
gap: 8px;
|
|
3460
|
+
padding: 8px;
|
|
3461
|
+
border: 1px solid var(--line);
|
|
3462
|
+
border-radius: 10px;
|
|
3463
|
+
background: rgba(255,255,255,0.90);
|
|
3464
|
+
box-shadow: var(--shadow);
|
|
3465
|
+
backdrop-filter: blur(18px);
|
|
3466
|
+
}
|
|
3467
|
+
|
|
3468
|
+
.focus-chip span {
|
|
3469
|
+
display: inline-flex;
|
|
3470
|
+
align-items: center;
|
|
3471
|
+
gap: 6px;
|
|
3472
|
+
border-radius: 999px;
|
|
3473
|
+
background: rgba(111,66,232,0.08);
|
|
3474
|
+
color: var(--text);
|
|
3475
|
+
padding: 5px 10px;
|
|
3476
|
+
font-size: 11px;
|
|
3477
|
+
font-weight: 800;
|
|
3478
|
+
max-width: 260px;
|
|
3479
|
+
overflow: hidden;
|
|
3480
|
+
text-overflow: ellipsis;
|
|
3481
|
+
white-space: nowrap;
|
|
3482
|
+
}
|
|
3483
|
+
|
|
3484
|
+
.search-shell.search-open .search-results {
|
|
3485
|
+
display: block;
|
|
3486
|
+
}
|
|
3487
|
+
|
|
3304
3488
|
.empty-hint {
|
|
3305
3489
|
margin: 0;
|
|
3306
3490
|
color: var(--muted);
|
package/static/scripts/admin.js
CHANGED
|
@@ -49,6 +49,7 @@ const A18N = {
|
|
|
49
49
|
nav_users: '사용자 관리',
|
|
50
50
|
nav_permissions: '권한 관리',
|
|
51
51
|
nav_sso: 'SSO 관리',
|
|
52
|
+
nav_enterprise: 'Enterprise',
|
|
52
53
|
nav_security: '보안 모니터링',
|
|
53
54
|
nav_audit: '감사 로그',
|
|
54
55
|
nav_chat: '채팅으로',
|
|
@@ -170,6 +171,16 @@ const A18N = {
|
|
|
170
171
|
section_sensitivity: '보안 모니터링',
|
|
171
172
|
section_audit: '감사 로그',
|
|
172
173
|
section_sso: 'SSO 관리',
|
|
174
|
+
enterprise_title: 'Enterprise 관리자',
|
|
175
|
+
enterprise_desc: '관리자 정책, 감사 추출, SIEM 추출, 조직 설정, 기능 상태를 확인합니다.',
|
|
176
|
+
enterprise_policies: '관리자 정책',
|
|
177
|
+
enterprise_policies_desc: 'Community 유효 정책과 Enterprise 정책 팩 상태입니다.',
|
|
178
|
+
enterprise_org: '조직 설정',
|
|
179
|
+
enterprise_org_desc: '워크스페이스 거버넌스와 조직 기능 상태입니다.',
|
|
180
|
+
enterprise_audit_export: '감사 추출',
|
|
181
|
+
enterprise_audit_export_desc: 'Community에서는 로컬 추출이 가능하며 보존 정책은 Enterprise 확장 지점입니다.',
|
|
182
|
+
enterprise_siem: 'SIEM 추출',
|
|
183
|
+
enterprise_siem_desc: 'Community에서 외부 이벤트를 전송하지 않고 SIEM envelope를 미리 봅니다.',
|
|
173
184
|
},
|
|
174
185
|
en: {
|
|
175
186
|
admin_sub: 'Admin Dashboard',
|
|
@@ -180,6 +191,7 @@ const A18N = {
|
|
|
180
191
|
nav_users: 'User Management',
|
|
181
192
|
nav_permissions: 'Permission Management',
|
|
182
193
|
nav_sso: 'SSO Management',
|
|
194
|
+
nav_enterprise: 'Enterprise',
|
|
183
195
|
nav_security: 'Security Monitoring',
|
|
184
196
|
nav_audit: 'Audit Logs',
|
|
185
197
|
nav_chat: 'Back to Chat',
|
|
@@ -301,6 +313,16 @@ const A18N = {
|
|
|
301
313
|
section_sensitivity: 'Security monitoring',
|
|
302
314
|
section_audit: 'Audit logs',
|
|
303
315
|
section_sso: 'SSO management',
|
|
316
|
+
enterprise_title: 'Enterprise Admin',
|
|
317
|
+
enterprise_desc: 'Review admin policies, audit export, SIEM export, organization settings, and capability status.',
|
|
318
|
+
enterprise_policies: 'Admin Policies',
|
|
319
|
+
enterprise_policies_desc: 'Effective Community policy and Enterprise policy-pack status.',
|
|
320
|
+
enterprise_org: 'Organization Settings',
|
|
321
|
+
enterprise_org_desc: 'Workspace governance and organization capability status.',
|
|
322
|
+
enterprise_audit_export: 'Audit Export',
|
|
323
|
+
enterprise_audit_export_desc: 'Community local export is available; retention is an Enterprise extension point.',
|
|
324
|
+
enterprise_siem: 'SIEM Export',
|
|
325
|
+
enterprise_siem_desc: 'Preview the SIEM envelope without streaming external events in Community.',
|
|
304
326
|
}
|
|
305
327
|
};
|
|
306
328
|
|
|
@@ -310,6 +332,7 @@ let latestUsers = [];
|
|
|
310
332
|
let latestSso = null;
|
|
311
333
|
let latestSensitivity = null;
|
|
312
334
|
let latestAudit = null;
|
|
335
|
+
let latestEnterprise = null;
|
|
313
336
|
|
|
314
337
|
function t(key) {
|
|
315
338
|
return (A18N[currentLang] || A18N.ko)[key] || key;
|
|
@@ -352,6 +375,7 @@ function setLang(lang) {
|
|
|
352
375
|
renderSso(latestSso);
|
|
353
376
|
renderSensitivity(latestSensitivity);
|
|
354
377
|
renderAudit(latestAudit);
|
|
378
|
+
renderEnterpriseAdmin(latestEnterprise);
|
|
355
379
|
loadDashboard();
|
|
356
380
|
}
|
|
357
381
|
|
|
@@ -788,6 +812,97 @@ function renderAudit(audit) {
|
|
|
788
812
|
` : `<div class="preview" style="padding:14px">${t('audit_no_events')}</div>`;
|
|
789
813
|
}
|
|
790
814
|
|
|
815
|
+
function enterpriseStatusTag(label, enabled) {
|
|
816
|
+
return `<span class="tag ${enabled ? 'low' : 'medium'}">${esc(label)}: ${enabled ? 'enabled' : 'disabled'}</span>`;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
function renderKeyValues(targetId, rows) {
|
|
820
|
+
const target = document.getElementById(targetId);
|
|
821
|
+
if (!target) return;
|
|
822
|
+
target.innerHTML = `
|
|
823
|
+
<div class="enterprise-kv">
|
|
824
|
+
${rows.map(([label, value]) => `
|
|
825
|
+
<div>
|
|
826
|
+
<span>${esc(label)}</span>
|
|
827
|
+
<strong>${esc(value)}</strong>
|
|
828
|
+
</div>
|
|
829
|
+
`).join('')}
|
|
830
|
+
</div>
|
|
831
|
+
`;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
function renderEnterpriseAdmin(payload) {
|
|
835
|
+
latestEnterprise = payload || null;
|
|
836
|
+
const enterprise = payload || {};
|
|
837
|
+
const edition = enterprise.edition || {};
|
|
838
|
+
const caps = edition.capabilities || {};
|
|
839
|
+
const tags = document.getElementById('enterprise-status-tags');
|
|
840
|
+
if (tags) {
|
|
841
|
+
tags.innerHTML = [
|
|
842
|
+
enterpriseStatusTag('edition', Boolean(edition.is_enterprise)),
|
|
843
|
+
enterpriseStatusTag('policy packs', Boolean(enterprise.admin_policies?.enabled)),
|
|
844
|
+
enterpriseStatusTag('siem', Boolean(enterprise.siem_export?.enabled)),
|
|
845
|
+
].join('');
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
const grid = document.getElementById('enterprise-capability-status');
|
|
849
|
+
if (grid) {
|
|
850
|
+
const entries = Object.keys(caps).length ? Object.entries(caps) : [];
|
|
851
|
+
grid.innerHTML = entries.length ? entries.map(([name, enabled]) => `
|
|
852
|
+
<div class="enterprise-cap-card ${enabled ? 'on' : 'off'}">
|
|
853
|
+
<i class="ti ${enabled ? 'ti-circle-check' : 'ti-lock'}"></i>
|
|
854
|
+
<span>${esc(name.replaceAll('_', ' '))}</span>
|
|
855
|
+
<strong>${enabled ? 'enabled' : 'disabled'}</strong>
|
|
856
|
+
</div>
|
|
857
|
+
`).join('') : `<div class="preview" style="padding:14px">Capability status unavailable.</div>`;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
const policies = enterprise.admin_policies || {};
|
|
861
|
+
renderKeyValues('enterprise-admin-policies', [
|
|
862
|
+
['Capability', policies.capability || 'admin_policy_packs'],
|
|
863
|
+
['Enabled', Boolean(policies.enabled)],
|
|
864
|
+
['Enforced', Boolean(policies.enforced)],
|
|
865
|
+
['Base roles', (policies.effective_policy?.base_roles || []).join(', ')],
|
|
866
|
+
['Local file access', policies.effective_policy?.local_file_access || 'approval-token gated'],
|
|
867
|
+
['Package install', policies.effective_policy?.package_install || 'admin-only'],
|
|
868
|
+
['Note', policies.note || 'Community features remain available.'],
|
|
869
|
+
]);
|
|
870
|
+
|
|
871
|
+
const org = enterprise.organization_settings || {};
|
|
872
|
+
renderKeyValues('enterprise-org-settings', [
|
|
873
|
+
['Workspaces', (org.community_baseline?.workspaces || []).join(', ')],
|
|
874
|
+
['Roles', (org.community_baseline?.roles || []).join(', ')],
|
|
875
|
+
['Data isolation', org.community_baseline?.data_isolation || 'single-tenant local storage'],
|
|
876
|
+
['Governance enabled', Object.values(org.governance_capabilities || {}).filter(Boolean).length],
|
|
877
|
+
['Note', org.note || 'Enterprise governance is an extension point.'],
|
|
878
|
+
]);
|
|
879
|
+
|
|
880
|
+
const audit = enterprise.audit_export || {};
|
|
881
|
+
renderKeyValues('enterprise-audit-export', [
|
|
882
|
+
['Local export', audit.local_export?.available ? 'available' : 'unavailable'],
|
|
883
|
+
['Endpoint', audit.local_export?.endpoint || '/admin/security/export'],
|
|
884
|
+
['Formats', (audit.local_export?.formats || []).join(', ')],
|
|
885
|
+
['SIEM streaming', audit.siem_streaming?.enabled ? 'enabled' : 'disabled'],
|
|
886
|
+
['Retention', audit.compliance_retention?.enabled ? 'enabled' : 'disabled'],
|
|
887
|
+
]);
|
|
888
|
+
|
|
889
|
+
const siem = enterprise.siem_export || {};
|
|
890
|
+
renderKeyValues('enterprise-siem-export', [
|
|
891
|
+
['Capability', siem.capability || 'siem_export'],
|
|
892
|
+
['Enabled', Boolean(siem.enabled)],
|
|
893
|
+
['Streamed', Boolean(siem.streamed)],
|
|
894
|
+
['Destination', siem.destination || 'not configured'],
|
|
895
|
+
]);
|
|
896
|
+
const preview = document.getElementById('enterprise-siem-preview');
|
|
897
|
+
if (preview) preview.textContent = JSON.stringify(siem.preview_envelope || {}, null, 2);
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
async function refreshSiemPreview() {
|
|
901
|
+
const res = await apiFetch('/admin/enterprise/siem-export', { headers: adminHeaders() });
|
|
902
|
+
const data = res.ok ? await res.json() : {};
|
|
903
|
+
renderEnterpriseAdmin({ ...(latestEnterprise || {}), siem_export: data });
|
|
904
|
+
}
|
|
905
|
+
|
|
791
906
|
function cellValue(value) {
|
|
792
907
|
if (value === null || value === undefined) return '';
|
|
793
908
|
if (Array.isArray(value)) return value.map(cellValue).filter(Boolean).join('; ');
|
|
@@ -1074,7 +1189,7 @@ async function loadDashboard() {
|
|
|
1074
1189
|
access.style.display = 'none';
|
|
1075
1190
|
|
|
1076
1191
|
try {
|
|
1077
|
-
const [healthRes, vpcRes, summaryRes, usersRes, sensitivityRes, inviteRes, statsRes, auditRes, ssoRes] = await Promise.all([
|
|
1192
|
+
const [healthRes, vpcRes, summaryRes, usersRes, sensitivityRes, inviteRes, statsRes, auditRes, ssoRes, enterpriseRes] = await Promise.all([
|
|
1078
1193
|
apiFetch('/health'),
|
|
1079
1194
|
apiFetch('/vpc/status'),
|
|
1080
1195
|
apiFetch('/admin/summary', { headers: adminHeaders() }),
|
|
@@ -1084,6 +1199,7 @@ async function loadDashboard() {
|
|
|
1084
1199
|
apiFetch('/admin/stats', { headers: adminHeaders() }),
|
|
1085
1200
|
apiFetch('/admin/audit', { headers: adminHeaders() }),
|
|
1086
1201
|
apiFetch('/admin/sso', { headers: adminHeaders() }),
|
|
1202
|
+
apiFetch('/admin/enterprise', { headers: adminHeaders() }),
|
|
1087
1203
|
]);
|
|
1088
1204
|
|
|
1089
1205
|
const health = healthRes.ok ? await healthRes.json() : null;
|
|
@@ -1095,6 +1211,7 @@ async function loadDashboard() {
|
|
|
1095
1211
|
const stats = statsRes.ok ? await statsRes.json() : null;
|
|
1096
1212
|
const audit = auditRes.ok ? await auditRes.json() : null;
|
|
1097
1213
|
const sso = ssoRes.ok ? await ssoRes.json() : null;
|
|
1214
|
+
const enterprise = enterpriseRes.ok ? await enterpriseRes.json() : null;
|
|
1098
1215
|
|
|
1099
1216
|
renderSummary(health, summary, vpc);
|
|
1100
1217
|
fillVpcForm(vpc);
|
|
@@ -1103,6 +1220,7 @@ async function loadDashboard() {
|
|
|
1103
1220
|
renderSensitivity(sensitivity);
|
|
1104
1221
|
renderAudit(audit);
|
|
1105
1222
|
renderSso(sso);
|
|
1223
|
+
renderEnterpriseAdmin(enterprise);
|
|
1106
1224
|
|
|
1107
1225
|
if (invite) {
|
|
1108
1226
|
document.getElementById('invite-link-input').value = invite.invite_url;
|
|
@@ -1118,6 +1236,7 @@ async function loadDashboard() {
|
|
|
1118
1236
|
if (!sensitivityRes.ok) failedSections.push(t('section_sensitivity'));
|
|
1119
1237
|
if (!auditRes.ok) failedSections.push(t('section_audit'));
|
|
1120
1238
|
if (!ssoRes.ok) failedSections.push(t('section_sso'));
|
|
1239
|
+
if (!enterpriseRes.ok) failedSections.push('Enterprise');
|
|
1121
1240
|
|
|
1122
1241
|
if (failedSections.length) {
|
|
1123
1242
|
access.style.display = 'block';
|
|
@@ -1189,6 +1308,7 @@ document.getElementById('save-sso-btn').addEventListener('click', saveSso);
|
|
|
1189
1308
|
document.getElementById('test-sso-btn').addEventListener('click', () => {
|
|
1190
1309
|
window.location.href = `${API_BASE}/auth/sso/login`;
|
|
1191
1310
|
});
|
|
1311
|
+
document.getElementById('refresh-siem-btn')?.addEventListener('click', () => refreshSiemPreview().catch(e => alert(String(e))));
|
|
1192
1312
|
document.getElementById('security-export-toggle')?.addEventListener('click', () => toggleExportOptions('security'));
|
|
1193
1313
|
document.getElementById('audit-export-toggle')?.addEventListener('click', () => toggleExportOptions('audit'));
|
|
1194
1314
|
document.querySelectorAll('[data-export-scope][data-export-format]').forEach(btn => {
|
package/static/scripts/chat.js
CHANGED
|
@@ -1033,6 +1033,7 @@ const chatViewport = document.getElementById('chat-viewport');
|
|
|
1033
1033
|
const data = await res.json();
|
|
1034
1034
|
if (!res.ok) return;
|
|
1035
1035
|
const rec = (data && data.recommendations) || {};
|
|
1036
|
+
const profile = (data && data.profile) || {};
|
|
1036
1037
|
const families = rec.families || [];
|
|
1037
1038
|
if (!families.length) return;
|
|
1038
1039
|
const counts = rec.counts || {};
|
|
@@ -1043,25 +1044,45 @@ const chatViewport = document.getElementById('chat-viewport');
|
|
|
1043
1044
|
not_recommended: ['권장 안 함', '#9ca3af'],
|
|
1044
1045
|
};
|
|
1045
1046
|
const [label, color] = map[status] || ['', '#9ca3af'];
|
|
1046
|
-
return `<span style="display:inline-block;padding:1px
|
|
1047
|
+
return `<span style="display:inline-block;padding:1px 8px;border-radius:999px;font-size:11px;font-weight:700;color:#fff;background:${color}">${label}</span>`;
|
|
1047
1048
|
};
|
|
1049
|
+
const ram = (m) => (m.required_ram_gb != null) ? `~${m.required_ram_gb}GB RAM (est.)` : '';
|
|
1050
|
+
const nextStep = (engine) => engine === 'ollama'
|
|
1051
|
+
? 'Next: ollama pull'
|
|
1052
|
+
: engine === 'local_mlx' ? 'Next: download & load' : 'Next: connect engine';
|
|
1053
|
+
|
|
1054
|
+
// Top pick callout
|
|
1055
|
+
const top = rec.top_pick;
|
|
1056
|
+
const topHtml = top ? `
|
|
1057
|
+
<div style="border:1px solid #16a34a;background:#f0fdf4;border-radius:10px;padding:10px 12px;margin:8px 0">
|
|
1058
|
+
<div style="font-weight:700">⭐ Best for this PC — ${escapeHtml(top.name || top.id)} ${badge('recommended')}</div>
|
|
1059
|
+
<div style="font-size:12px;opacity:0.8;margin-top:3px">${escapeHtml(top.reason || '')}</div>
|
|
1060
|
+
<div style="font-size:12px;margin-top:4px">${escapeHtml(top.size || '')} · ${escapeHtml(ram(top))} · ${escapeHtml(nextStep(rec.engine))}</div>
|
|
1061
|
+
</div>` : '';
|
|
1062
|
+
|
|
1048
1063
|
const rows = families.map((fam) => {
|
|
1049
1064
|
const best = fam.best;
|
|
1050
1065
|
const items = (fam.models || []).map((m) => `
|
|
1051
|
-
<div style="display:flex;justify-content:space-between;gap:8px;padding:
|
|
1066
|
+
<div style="display:flex;justify-content:space-between;gap:8px;padding:3px 0;font-size:12px;opacity:${m.status === 'not_recommended' ? 0.55 : 1}">
|
|
1052
1067
|
<span>${escapeHtml(m.name || m.id)}</span>
|
|
1053
|
-
<span>${escapeHtml(m.size || '')} ${badge(m.status)}</span>
|
|
1068
|
+
<span style="white-space:nowrap">${escapeHtml(m.size || '')} · ${escapeHtml(ram(m))} ${badge(m.status)}</span>
|
|
1054
1069
|
</div>`).join('');
|
|
1055
1070
|
return `
|
|
1056
1071
|
<details style="margin:6px 0;border:1px solid var(--border,#e5e7eb);border-radius:8px;padding:8px 10px">
|
|
1057
|
-
<summary style="cursor:pointer;font-weight:600">${escapeHtml(fam.family)} ${best ? badge(best.status) : ''}</summary>
|
|
1072
|
+
<summary style="cursor:pointer;font-weight:600">${escapeHtml(fam.family)} ${best ? badge(best.status) : ''}${best ? ` <span style="font-weight:400;opacity:0.7">${escapeHtml(best.name || '')}</span>` : ''}</summary>
|
|
1058
1073
|
<div style="margin-top:6px">${items}</div>
|
|
1059
1074
|
</details>`;
|
|
1060
1075
|
}).join('');
|
|
1076
|
+
|
|
1077
|
+
const engineLabel = rec.engine === 'local_mlx' ? 'MLX (Apple Silicon)' : rec.engine;
|
|
1078
|
+
const machine = `${profile.os || ''} · RAM ${rec.ram_gb || '?'}GB · ${rec.apple_silicon ? 'Apple Silicon' : (profile.gpu && profile.gpu.vendor) || 'CPU'} · engine ${engineLabel}`;
|
|
1061
1079
|
container.innerHTML = `
|
|
1062
|
-
<h3 style="margin:14px 0 4px">이 PC
|
|
1063
|
-
<p style="font-size:12px;opacity:0.7;margin:0 0
|
|
1064
|
-
${
|
|
1080
|
+
<h3 style="margin:14px 0 4px">이 PC에 맞는 로컬 모델</h3>
|
|
1081
|
+
<p style="font-size:12px;opacity:0.7;margin:0 0 4px">${escapeHtml(machine)}</p>
|
|
1082
|
+
<p style="font-size:12px;opacity:0.7;margin:0 0 6px">${badge('recommended')} ${counts.recommended || 0} · ${badge('compatible')} ${counts.compatible || 0} · ${badge('not_recommended')} ${counts.not_recommended || 0} · estimates are conservative, verify before loading</p>
|
|
1083
|
+
${topHtml}
|
|
1084
|
+
${rows}
|
|
1085
|
+
<p style="font-size:12px;opacity:0.65;margin:8px 0 0">로컬 모델이 부족하면 클라우드 모델(OpenAI·OpenRouter·Groq 등, API 키 필요)을 선택할 수 있습니다.</p>`;
|
|
1065
1086
|
} catch (e) {
|
|
1066
1087
|
/* best-effort enhancement; never break onboarding */
|
|
1067
1088
|
}
|