ltcai 0.3.0 → 0.3.1
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 +24 -0
- package/docs/CHANGELOG.md +82 -0
- package/knowledge_graph_api.py +10 -2
- package/latticeai/api/security_dashboard.py +580 -0
- package/latticeai/core/graph_curator.py +417 -0
- package/latticeai/core/model_compat.py +407 -0
- package/latticeai/core/model_resolution.py +227 -0
- package/package.json +1 -1
- package/server.py +254 -16
- package/static/account.html +2 -2
- package/static/admin.html +75 -1
- package/static/chat.html +2 -2
- package/static/graph.html +2 -2
- package/static/lattice-reference.css +82 -50
- package/static/scripts/account.js +10 -2
- package/static/scripts/admin.js +296 -0
- package/static/scripts/chat.js +82 -9
- package/static/scripts/graph.js +6 -2
- package/static/sw.js +1 -1
|
@@ -3051,24 +3051,49 @@ body.lattice-ref-graph {
|
|
|
3051
3051
|
}
|
|
3052
3052
|
|
|
3053
3053
|
.local-option-row {
|
|
3054
|
-
display:
|
|
3055
|
-
|
|
3056
|
-
gap:
|
|
3054
|
+
display: grid;
|
|
3055
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
3056
|
+
gap: 7px;
|
|
3057
3057
|
}
|
|
3058
3058
|
|
|
3059
|
-
.local-option-
|
|
3059
|
+
.local-option-btn {
|
|
3060
|
+
min-width: 0;
|
|
3061
|
+
height: 36px;
|
|
3062
|
+
border: 1px solid rgba(111,66,232,0.18);
|
|
3063
|
+
border-radius: 8px;
|
|
3064
|
+
background: #fff;
|
|
3065
|
+
color: var(--muted);
|
|
3066
|
+
cursor: pointer;
|
|
3060
3067
|
display: inline-flex;
|
|
3061
3068
|
align-items: center;
|
|
3069
|
+
justify-content: center;
|
|
3062
3070
|
gap: 6px;
|
|
3063
|
-
|
|
3071
|
+
padding: 0 9px;
|
|
3064
3072
|
font-size: 12px;
|
|
3065
|
-
|
|
3066
|
-
|
|
3073
|
+
font-weight: 650;
|
|
3074
|
+
line-height: 1;
|
|
3075
|
+
text-align: center;
|
|
3076
|
+
white-space: nowrap;
|
|
3067
3077
|
}
|
|
3068
3078
|
|
|
3069
|
-
.local-option-
|
|
3070
|
-
|
|
3071
|
-
|
|
3079
|
+
.local-option-btn span {
|
|
3080
|
+
min-width: 0;
|
|
3081
|
+
overflow: hidden;
|
|
3082
|
+
text-overflow: ellipsis;
|
|
3083
|
+
white-space: nowrap;
|
|
3084
|
+
}
|
|
3085
|
+
|
|
3086
|
+
.local-option-btn:hover {
|
|
3087
|
+
border-color: rgba(111,66,232,0.42);
|
|
3088
|
+
color: var(--accent);
|
|
3089
|
+
background: rgba(111,66,232,0.07);
|
|
3090
|
+
}
|
|
3091
|
+
|
|
3092
|
+
.local-option-btn.active {
|
|
3093
|
+
border-color: rgba(111,66,232,0.48);
|
|
3094
|
+
background: rgba(111,66,232,0.11);
|
|
3095
|
+
color: var(--accent);
|
|
3096
|
+
box-shadow: inset 0 0 0 1px rgba(111,66,232,0.12);
|
|
3072
3097
|
}
|
|
3073
3098
|
|
|
3074
3099
|
.local-audit-grid {
|
|
@@ -3815,7 +3840,7 @@ body.lattice-ref-graph {
|
|
|
3815
3840
|
display: none;
|
|
3816
3841
|
position: fixed;
|
|
3817
3842
|
inset: 0;
|
|
3818
|
-
background: rgba(
|
|
3843
|
+
background: rgba(111,66,232,0.18);
|
|
3819
3844
|
backdrop-filter: blur(4px);
|
|
3820
3845
|
z-index: 1000;
|
|
3821
3846
|
align-items: center;
|
|
@@ -3828,7 +3853,7 @@ body.lattice-ref-graph {
|
|
|
3828
3853
|
display: none;
|
|
3829
3854
|
position: fixed;
|
|
3830
3855
|
inset: 0;
|
|
3831
|
-
background: rgba(
|
|
3856
|
+
background: rgba(111,66,232,0.18);
|
|
3832
3857
|
backdrop-filter: blur(4px);
|
|
3833
3858
|
z-index: 1000;
|
|
3834
3859
|
align-items: center;
|
|
@@ -3836,8 +3861,8 @@ body.lattice-ref-graph {
|
|
|
3836
3861
|
}
|
|
3837
3862
|
.mcp-modal-overlay.open { display: flex; }
|
|
3838
3863
|
.mcp-modal {
|
|
3839
|
-
background:
|
|
3840
|
-
border: 1px solid rgba(
|
|
3864
|
+
background: rgba(255,255,255,0.96);
|
|
3865
|
+
border: 1px solid rgba(111,66,232,0.16);
|
|
3841
3866
|
border-radius: 16px;
|
|
3842
3867
|
width: 100%;
|
|
3843
3868
|
max-width: 560px;
|
|
@@ -3849,12 +3874,12 @@ body.lattice-ref-graph {
|
|
|
3849
3874
|
}
|
|
3850
3875
|
.mcp-modal-header {
|
|
3851
3876
|
padding: 18px 20px;
|
|
3852
|
-
border-bottom: 1px solid rgba(
|
|
3877
|
+
border-bottom: 1px solid rgba(111,66,232,0.12);
|
|
3853
3878
|
display: flex; align-items: center; justify-content: space-between;
|
|
3854
3879
|
}
|
|
3855
3880
|
.mcp-modal-header h3 { font-size: 15px; font-weight: 700; color: var(--text); }
|
|
3856
3881
|
.mcp-modal-close { background: none; border: none; color: var(--faint); cursor: pointer; font-size: 18px; padding: 2px 6px; border-radius: 6px; }
|
|
3857
|
-
.mcp-modal-close:hover { color: var(--text); background: rgba(
|
|
3882
|
+
.mcp-modal-close:hover { color: var(--text); background: rgba(111,66,232,0.07); }
|
|
3858
3883
|
.mcp-modal-body.lattice-ref-chat { flex: 1; overflow-y: auto; padding: 16px 20px; }
|
|
3859
3884
|
.mcp-section-label { font-size: 10px; font-weight: 700; color: var(--faint); text-transform: uppercase; letter-spacing: .08em; margin: 12px 0 8px; }
|
|
3860
3885
|
.mcp-item {
|
|
@@ -3917,8 +3942,8 @@ body.lattice-ref-graph {
|
|
|
3917
3942
|
}
|
|
3918
3943
|
.mcp-delete-btn:hover { background: rgba(255,80,80,0.18); }
|
|
3919
3944
|
.acct-modal {
|
|
3920
|
-
background:
|
|
3921
|
-
border: 1px solid rgba(
|
|
3945
|
+
background: rgba(255,255,255,0.96);
|
|
3946
|
+
border: 1px solid rgba(111,66,232,0.16);
|
|
3922
3947
|
border-radius: 16px;
|
|
3923
3948
|
width: 100%;
|
|
3924
3949
|
max-width: 380px;
|
|
@@ -3929,24 +3954,24 @@ body.lattice-ref-graph {
|
|
|
3929
3954
|
}
|
|
3930
3955
|
.acct-tabs {
|
|
3931
3956
|
display: flex;
|
|
3932
|
-
border-bottom: 1px solid rgba(
|
|
3957
|
+
border-bottom: 1px solid rgba(111,66,232,0.12);
|
|
3933
3958
|
}
|
|
3934
3959
|
.acct-tab {
|
|
3935
3960
|
flex: 1; padding: 14px; font-size: 13px; font-weight: 500;
|
|
3936
3961
|
background: none; border: none; color: var(--muted); cursor: pointer;
|
|
3937
3962
|
transition: all .15s; border-bottom: 2px solid transparent;
|
|
3938
3963
|
}
|
|
3939
|
-
.acct-tab.active { color: var(--text, #
|
|
3964
|
+
.acct-tab.active { color: var(--text, #14162c); border-bottom-color: var(--accent, #6366f1); }
|
|
3940
3965
|
.acct-body.lattice-ref-chat { padding: 24px; display: flex; flex-direction: column; gap: 14px; }
|
|
3941
3966
|
.acct-tab-panel { display: none; flex-direction: column; gap: 14px; }
|
|
3942
3967
|
.acct-tab-panel.active { display: flex; }
|
|
3943
3968
|
.pw-field { display: flex; flex-direction: column; gap: 5px; }
|
|
3944
3969
|
.pw-field label { font-size: 11px; color: var(--muted); }
|
|
3945
3970
|
.pw-field input {
|
|
3946
|
-
background: rgba(
|
|
3947
|
-
border: 1px solid rgba(
|
|
3971
|
+
background: rgba(255,255,255,0.88);
|
|
3972
|
+
border: 1px solid rgba(111,66,232,0.18);
|
|
3948
3973
|
border-radius: 8px;
|
|
3949
|
-
color: var(--text, #
|
|
3974
|
+
color: var(--text, #14162c);
|
|
3950
3975
|
padding: 8px 12px;
|
|
3951
3976
|
font-size: 13px;
|
|
3952
3977
|
outline: none;
|
|
@@ -3958,8 +3983,8 @@ body.lattice-ref-graph {
|
|
|
3958
3983
|
flex: 1; padding: 8px; border-radius: 8px; border: none;
|
|
3959
3984
|
cursor: pointer; font-size: 13px; font-weight: 500; transition: all .15s;
|
|
3960
3985
|
}
|
|
3961
|
-
.pw-cancel { background: rgba(
|
|
3962
|
-
.pw-cancel:hover { background: rgba(
|
|
3986
|
+
.pw-cancel { background: rgba(111,66,232,0.08); color: var(--muted); }
|
|
3987
|
+
.pw-cancel:hover { background: rgba(111,66,232,0.13); }
|
|
3963
3988
|
.pw-submit { background: var(--accent, #6366f1); color: #fff; }
|
|
3964
3989
|
.pw-submit:hover { opacity: 0.85; }
|
|
3965
3990
|
.pw-msg { font-size: 12px; min-height: 16px; }
|
|
@@ -3972,8 +3997,8 @@ body.lattice-ref-graph {
|
|
|
3972
3997
|
position: absolute;
|
|
3973
3998
|
top: calc(100% + 6px);
|
|
3974
3999
|
right: 0;
|
|
3975
|
-
background:
|
|
3976
|
-
border: 1px solid rgba(
|
|
4000
|
+
background: rgba(255,255,255,0.96);
|
|
4001
|
+
border: 1px solid rgba(111,66,232,0.16);
|
|
3977
4002
|
border-radius: 10px;
|
|
3978
4003
|
overflow: hidden;
|
|
3979
4004
|
box-shadow: 0 8px 24px rgba(88,72,150,0.16);
|
|
@@ -3986,7 +4011,7 @@ body.lattice-ref-graph {
|
|
|
3986
4011
|
padding: 9px 14px; font-size: 13px; cursor: pointer;
|
|
3987
4012
|
color: var(--muted); transition: background .12s;
|
|
3988
4013
|
}
|
|
3989
|
-
.lang-option:hover { background: rgba(
|
|
4014
|
+
.lang-option:hover { background: rgba(111,66,232,0.07); color: var(--text, #14162c); }
|
|
3990
4015
|
.lang-option.active { color: var(--accent, #6366f1); font-weight: 600; }
|
|
3991
4016
|
|
|
3992
4017
|
.auth-lang-picker {
|
|
@@ -4188,13 +4213,13 @@ body.lattice-ref-graph {
|
|
|
4188
4213
|
overflow: hidden;
|
|
4189
4214
|
border-radius: var(--radius-sm);
|
|
4190
4215
|
margin: 12px 0;
|
|
4191
|
-
background:
|
|
4216
|
+
background: rgba(255,255,255,0.86);
|
|
4192
4217
|
border: 1px solid rgba(129,140,248,0.15);
|
|
4193
|
-
box-shadow: 0 4px 16px rgba(
|
|
4218
|
+
box-shadow: 0 4px 16px rgba(88,72,150,0.10);
|
|
4194
4219
|
}
|
|
4195
4220
|
|
|
4196
4221
|
.code-header {
|
|
4197
|
-
background: rgba(
|
|
4222
|
+
background: rgba(111,66,232,0.08);
|
|
4198
4223
|
color: var(--muted);
|
|
4199
4224
|
font-size: 11px;
|
|
4200
4225
|
padding: 8px 12px;
|
|
@@ -4221,7 +4246,7 @@ body.lattice-ref-graph {
|
|
|
4221
4246
|
white-space: pre;
|
|
4222
4247
|
padding: 16px;
|
|
4223
4248
|
font-size: 13px;
|
|
4224
|
-
background:
|
|
4249
|
+
background: rgba(255,255,255,0.86);
|
|
4225
4250
|
scrollbar-width: thin;
|
|
4226
4251
|
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
|
4227
4252
|
line-height: 1.6;
|
|
@@ -4261,7 +4286,7 @@ body.lattice-ref-graph {
|
|
|
4261
4286
|
display: none;
|
|
4262
4287
|
position: fixed;
|
|
4263
4288
|
inset: 0;
|
|
4264
|
-
background: rgba(
|
|
4289
|
+
background: rgba(111,66,232,0.18);
|
|
4265
4290
|
z-index: 99;
|
|
4266
4291
|
backdrop-filter: blur(2px);
|
|
4267
4292
|
}
|
|
@@ -4709,8 +4734,8 @@ body.lattice-ref-graph {
|
|
|
4709
4734
|
|
|
4710
4735
|
.auth-card {
|
|
4711
4736
|
width: min(400px, 100%);
|
|
4712
|
-
background: rgba(
|
|
4713
|
-
border: 1px solid rgba(
|
|
4737
|
+
background: rgba(255,255,255,0.94);
|
|
4738
|
+
border: 1px solid rgba(111,66,232,0.16);
|
|
4714
4739
|
border-radius: 20px;
|
|
4715
4740
|
padding: 36px 32px;
|
|
4716
4741
|
box-shadow: var(--shadow), 0 0 0 1px rgba(111,66,232,0.05);
|
|
@@ -5349,9 +5374,10 @@ body.lattice-ref-graph {
|
|
|
5349
5374
|
.mcp-dropdown {
|
|
5350
5375
|
display: none;
|
|
5351
5376
|
margin-top: 8px;
|
|
5352
|
-
border: 1px solid rgba(
|
|
5377
|
+
border: 1px solid rgba(111,66,232,0.16);
|
|
5353
5378
|
border-radius: 10px;
|
|
5354
|
-
background: rgba(
|
|
5379
|
+
background: rgba(255,255,255,0.92);
|
|
5380
|
+
box-shadow: 0 16px 36px rgba(96,72,160,0.12);
|
|
5355
5381
|
overflow: hidden;
|
|
5356
5382
|
}
|
|
5357
5383
|
|
|
@@ -5365,7 +5391,12 @@ body.lattice-ref-graph {
|
|
|
5365
5391
|
justify-content: space-between;
|
|
5366
5392
|
gap: 12px;
|
|
5367
5393
|
padding: 11px 14px;
|
|
5368
|
-
border-bottom: 1px solid rgba(111,66,232,0.
|
|
5394
|
+
border-bottom: 1px solid rgba(111,66,232,0.10);
|
|
5395
|
+
color: var(--text);
|
|
5396
|
+
}
|
|
5397
|
+
|
|
5398
|
+
.mcp-dropdown-item:hover {
|
|
5399
|
+
background: rgba(111,66,232,0.05);
|
|
5369
5400
|
}
|
|
5370
5401
|
|
|
5371
5402
|
.mcp-dropdown-item:last-child {
|
|
@@ -5381,6 +5412,7 @@ body.lattice-ref-graph {
|
|
|
5381
5412
|
display: block;
|
|
5382
5413
|
font-size: 13px;
|
|
5383
5414
|
margin-bottom: 2px;
|
|
5415
|
+
color: var(--text);
|
|
5384
5416
|
}
|
|
5385
5417
|
|
|
5386
5418
|
.mcp-dropdown-item-info span {
|
|
@@ -5827,7 +5859,7 @@ body.lattice-ref-graph {
|
|
|
5827
5859
|
transition: transform 0.25s cubic-bezier(0.4,0,0.2,1);
|
|
5828
5860
|
border-right: 1px solid var(--border-strong);
|
|
5829
5861
|
box-shadow: 4px 0 32px rgba(88,72,150,0.18);
|
|
5830
|
-
background: #
|
|
5862
|
+
background: #f6f0ff;
|
|
5831
5863
|
backdrop-filter: none;
|
|
5832
5864
|
-webkit-backdrop-filter: none;
|
|
5833
5865
|
}
|
|
@@ -5888,7 +5920,7 @@ body.lattice-ref-graph {
|
|
|
5888
5920
|
position: fixed;
|
|
5889
5921
|
inset: 0;
|
|
5890
5922
|
z-index: 9000;
|
|
5891
|
-
background: rgba(
|
|
5923
|
+
background: rgba(111, 66, 232, 0.18);
|
|
5892
5924
|
backdrop-filter: blur(14px);
|
|
5893
5925
|
align-items: center;
|
|
5894
5926
|
justify-content: center;
|
|
@@ -6243,8 +6275,8 @@ body.lattice-ref-graph {
|
|
|
6243
6275
|
|
|
6244
6276
|
/* ── MCP 드롭다운 개선 ── */
|
|
6245
6277
|
.mcp-dropdown {
|
|
6246
|
-
background: rgba(
|
|
6247
|
-
border: 1px solid rgba(111,66,232,0.
|
|
6278
|
+
background: rgba(255,255,255,0.96) !important;
|
|
6279
|
+
border: 1px solid rgba(111,66,232,0.16) !important;
|
|
6248
6280
|
border-radius: var(--radius) !important;
|
|
6249
6281
|
box-shadow: var(--shadow), 0 0 20px rgba(111,66,232,0.08) !important;
|
|
6250
6282
|
}
|
|
@@ -6477,17 +6509,17 @@ body.lattice-ref-graph {
|
|
|
6477
6509
|
|
|
6478
6510
|
.app-layout .code-container,
|
|
6479
6511
|
.app-layout pre {
|
|
6480
|
-
background:
|
|
6512
|
+
background: rgba(255,255,255,0.88);
|
|
6481
6513
|
border-color: rgba(218,225,255,0.10);
|
|
6482
6514
|
}
|
|
6483
6515
|
|
|
6484
6516
|
.app-layout .code-header {
|
|
6485
|
-
background:
|
|
6517
|
+
background: rgba(111,66,232,0.08);
|
|
6486
6518
|
border-bottom-color: rgba(218,225,255,0.09);
|
|
6487
6519
|
}
|
|
6488
6520
|
|
|
6489
6521
|
.app-layout .input-area {
|
|
6490
|
-
background: linear-gradient(0deg, rgba(
|
|
6522
|
+
background: linear-gradient(0deg, rgba(246,240,255,0.98) 0%, rgba(246,240,255,0.78) 64%, transparent 100%);
|
|
6491
6523
|
}
|
|
6492
6524
|
|
|
6493
6525
|
.app-layout .input-box {
|
|
@@ -6692,7 +6724,7 @@ body.lattice-ref-graph {
|
|
|
6692
6724
|
align-items: center;
|
|
6693
6725
|
justify-content: center;
|
|
6694
6726
|
padding: 24px;
|
|
6695
|
-
background: rgba(
|
|
6727
|
+
background: rgba(111, 66, 232, 0.18);
|
|
6696
6728
|
backdrop-filter: blur(14px);
|
|
6697
6729
|
}
|
|
6698
6730
|
.workspace-modal-overlay.open,
|
|
@@ -6714,10 +6746,10 @@ body.lattice-ref-graph {
|
|
|
6714
6746
|
.mode-modal {
|
|
6715
6747
|
width: min(520px, 100%);
|
|
6716
6748
|
background:
|
|
6717
|
-
radial-gradient(circle at 18% 8%, rgba(111,75,246,0.
|
|
6718
|
-
linear-gradient(180deg, #
|
|
6719
|
-
color:
|
|
6720
|
-
border-color: rgba(
|
|
6749
|
+
radial-gradient(circle at 18% 8%, rgba(111,75,246,0.18), transparent 32%),
|
|
6750
|
+
linear-gradient(180deg, #ffffff, #f6f0ff);
|
|
6751
|
+
color: var(--text);
|
|
6752
|
+
border-color: rgba(111,66,232,0.16);
|
|
6721
6753
|
}
|
|
6722
6754
|
.modal-kicker {
|
|
6723
6755
|
color: var(--accent);
|
|
@@ -137,6 +137,12 @@ const API_BASE = window.location.protocol === 'file:' ? 'http://localhost:4825'
|
|
|
137
137
|
el.className = 'msg' + (ok ? ' ok' : '');
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
+
function requestSetupAfterLogin() {
|
|
141
|
+
try {
|
|
142
|
+
sessionStorage.setItem('ltcai_force_setup_after_login', 'true');
|
|
143
|
+
} catch (_) {}
|
|
144
|
+
}
|
|
145
|
+
|
|
140
146
|
async function doLogin() {
|
|
141
147
|
const email = document.getElementById('login-email').value.trim();
|
|
142
148
|
const password = document.getElementById('login-pw').value;
|
|
@@ -155,7 +161,8 @@ const API_BASE = window.location.protocol === 'file:' ? 'http://localhost:4825'
|
|
|
155
161
|
localStorage.setItem('ltcai_user_email', data.email);
|
|
156
162
|
localStorage.setItem('ltcai_user_nickname', data.nickname || data.name || data.email);
|
|
157
163
|
localStorage.setItem('ltcai_is_admin', data.is_admin ? 'true' : 'false');
|
|
158
|
-
|
|
164
|
+
requestSetupAfterLogin();
|
|
165
|
+
window.location.href = '/chat?setup=1';
|
|
159
166
|
} else {
|
|
160
167
|
const data = await res.json().catch(() => ({}));
|
|
161
168
|
setMsg('login-msg', data.detail || t('err_login_fail'));
|
|
@@ -197,7 +204,8 @@ const API_BASE = window.location.protocol === 'file:' ? 'http://localhost:4825'
|
|
|
197
204
|
localStorage.setItem('ltcai_user_email', data.email);
|
|
198
205
|
localStorage.setItem('ltcai_user_nickname', data.nickname || data.name || data.email);
|
|
199
206
|
localStorage.setItem('ltcai_is_admin', data.is_admin ? 'true' : 'false');
|
|
200
|
-
|
|
207
|
+
requestSetupAfterLogin();
|
|
208
|
+
window.location.href = '/chat?setup=1';
|
|
201
209
|
}
|
|
202
210
|
});
|
|
203
211
|
} else {
|
package/static/scripts/admin.js
CHANGED
|
@@ -1195,4 +1195,300 @@ document.querySelectorAll('[data-export-scope][data-export-format]').forEach(btn
|
|
|
1195
1195
|
btn.addEventListener('click', () => exportAdminLogs(btn.dataset.exportScope, btn.dataset.exportFormat));
|
|
1196
1196
|
});
|
|
1197
1197
|
|
|
1198
|
+
// ── Security & Audit Command Center (피드백 #5) ─────────────────────────────
|
|
1199
|
+
|
|
1200
|
+
function ccEscape(value) {
|
|
1201
|
+
if (value === null || value === undefined) return '';
|
|
1202
|
+
const str = String(value);
|
|
1203
|
+
return str
|
|
1204
|
+
.replace(/&/g, '&')
|
|
1205
|
+
.replace(/</g, '<')
|
|
1206
|
+
.replace(/>/g, '>')
|
|
1207
|
+
.replace(/"/g, '"')
|
|
1208
|
+
.replace(/'/g, ''');
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
const CC_CARD_LABELS = {
|
|
1212
|
+
events_today: '오늘 이벤트',
|
|
1213
|
+
high_risk_events: 'High Risk',
|
|
1214
|
+
risky_chats: '위험 채팅',
|
|
1215
|
+
risky_files: '위험 파일',
|
|
1216
|
+
secret_blocks: 'Secret 차단',
|
|
1217
|
+
external_blocks: '외부 전송 차단',
|
|
1218
|
+
admin_raw_views: '관리자 원문 조회',
|
|
1219
|
+
review_required: '검토 필요',
|
|
1220
|
+
};
|
|
1221
|
+
|
|
1222
|
+
let ccUserChart = null;
|
|
1223
|
+
let ccFieldChart = null;
|
|
1224
|
+
|
|
1225
|
+
async function ccFetchJson(path) {
|
|
1226
|
+
try {
|
|
1227
|
+
const res = await apiFetch(path, { headers: adminHeaders() });
|
|
1228
|
+
if (!res.ok) {
|
|
1229
|
+
console.warn('Security CC fetch failed', path, res.status);
|
|
1230
|
+
return null;
|
|
1231
|
+
}
|
|
1232
|
+
return await res.json();
|
|
1233
|
+
} catch (e) {
|
|
1234
|
+
console.warn('Security CC fetch error', path, e);
|
|
1235
|
+
return null;
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
function renderCcCards(overview) {
|
|
1240
|
+
const root = document.getElementById('security-cc-cards');
|
|
1241
|
+
if (!root || !overview || !overview.cards) return;
|
|
1242
|
+
const html = Object.entries(overview.cards).map(([key, value]) => `
|
|
1243
|
+
<div class="audit-card">
|
|
1244
|
+
<div class="audit-card-label">${ccEscape(CC_CARD_LABELS[key] || key)}</div>
|
|
1245
|
+
<div class="audit-card-value">${ccEscape(value)}</div>
|
|
1246
|
+
</div>
|
|
1247
|
+
`).join('');
|
|
1248
|
+
root.innerHTML = html;
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
function renderCcUsersTable(users) {
|
|
1252
|
+
const wrap = document.getElementById('security-cc-users');
|
|
1253
|
+
if (!wrap) return;
|
|
1254
|
+
if (!users || users.length === 0) {
|
|
1255
|
+
wrap.innerHTML = '<div class="preview" style="padding:14px">표시할 사용자가 없습니다.</div>';
|
|
1256
|
+
return;
|
|
1257
|
+
}
|
|
1258
|
+
const rows = users.slice(0, 25).map(u => `
|
|
1259
|
+
<tr data-cc-user="${ccEscape(u.email)}" style="cursor:pointer">
|
|
1260
|
+
<td>${ccEscape(u.user)}</td>
|
|
1261
|
+
<td>${ccEscape(u.total_chats)}</td>
|
|
1262
|
+
<td style="color:#2c8a3f">${ccEscape(u.compliant_chats)}</td>
|
|
1263
|
+
<td style="color:#b13030">${ccEscape(u.risky_chats)}</td>
|
|
1264
|
+
<td>${ccEscape(u.uploaded_files)}</td>
|
|
1265
|
+
<td style="color:#2c8a3f">${ccEscape(u.compliant_files)}</td>
|
|
1266
|
+
<td style="color:#b13030">${ccEscape(u.risky_files)}</td>
|
|
1267
|
+
<td>${ccEscape(u.high_risk_events)}</td>
|
|
1268
|
+
<td>${ccEscape(u.risk_rate)}%</td>
|
|
1269
|
+
<td>${ccEscape((u.last_activity_at || '').slice(0, 19).replace('T', ' '))}</td>
|
|
1270
|
+
</tr>
|
|
1271
|
+
`).join('');
|
|
1272
|
+
wrap.innerHTML = `
|
|
1273
|
+
<table class="data-table">
|
|
1274
|
+
<thead><tr>
|
|
1275
|
+
<th>사용자</th><th>총 채팅</th><th>준수 채팅</th><th>위험 채팅</th>
|
|
1276
|
+
<th>총 파일</th><th>준수 파일</th><th>위험 파일</th>
|
|
1277
|
+
<th>High</th><th>위험률</th><th>마지막 활동</th>
|
|
1278
|
+
</tr></thead>
|
|
1279
|
+
<tbody>${rows}</tbody>
|
|
1280
|
+
</table>`;
|
|
1281
|
+
wrap.querySelectorAll('tr[data-cc-user]').forEach(tr => {
|
|
1282
|
+
tr.addEventListener('click', () => ccShowUserDrillDown(tr.dataset.ccUser));
|
|
1283
|
+
});
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
function renderCcUserChart(users) {
|
|
1287
|
+
const canvas = document.getElementById('security-cc-user-chart');
|
|
1288
|
+
if (!canvas || typeof Chart === 'undefined') return;
|
|
1289
|
+
const top = users.slice(0, 8);
|
|
1290
|
+
const labels = top.map(u => u.user);
|
|
1291
|
+
if (ccUserChart) { ccUserChart.destroy(); ccUserChart = null; }
|
|
1292
|
+
ccUserChart = new Chart(canvas, {
|
|
1293
|
+
type: 'bar',
|
|
1294
|
+
data: {
|
|
1295
|
+
labels,
|
|
1296
|
+
datasets: [
|
|
1297
|
+
{ label: '준수 채팅', backgroundColor: '#5cb874', data: top.map(u => u.compliant_chats) },
|
|
1298
|
+
{ label: '위험 채팅', backgroundColor: '#e8636e', data: top.map(u => u.risky_chats) },
|
|
1299
|
+
{ label: '준수 파일', backgroundColor: '#7fb5e6', data: top.map(u => u.compliant_files) },
|
|
1300
|
+
{ label: '위험 파일', backgroundColor: '#d94c4c', data: top.map(u => u.risky_files) },
|
|
1301
|
+
]
|
|
1302
|
+
},
|
|
1303
|
+
options: {
|
|
1304
|
+
responsive: true,
|
|
1305
|
+
scales: { x: { stacked: true }, y: { stacked: true } },
|
|
1306
|
+
plugins: { legend: { position: 'bottom' } },
|
|
1307
|
+
}
|
|
1308
|
+
});
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
function renderCcFieldChart(overview) {
|
|
1312
|
+
const canvas = document.getElementById('security-cc-field-chart');
|
|
1313
|
+
const legend = document.getElementById('security-cc-field-legend');
|
|
1314
|
+
if (!canvas || typeof Chart === 'undefined') return;
|
|
1315
|
+
const counts = overview?.field_counts || {};
|
|
1316
|
+
const labels = Object.keys(counts);
|
|
1317
|
+
const data = labels.map(l => counts[l]);
|
|
1318
|
+
if (ccFieldChart) { ccFieldChart.destroy(); ccFieldChart = null; }
|
|
1319
|
+
if (labels.length === 0) {
|
|
1320
|
+
if (legend) legend.textContent = '감지된 민감정보 유형이 없습니다.';
|
|
1321
|
+
return;
|
|
1322
|
+
}
|
|
1323
|
+
ccFieldChart = new Chart(canvas, {
|
|
1324
|
+
type: 'doughnut',
|
|
1325
|
+
data: { labels, datasets: [{ data, backgroundColor: ['#e8636e','#7fb5e6','#f0b14a','#5cb874','#9b6cd0','#3da9b6','#d18cd4','#a3a3a3'] }] },
|
|
1326
|
+
options: { plugins: { legend: { position: 'bottom' } } }
|
|
1327
|
+
});
|
|
1328
|
+
if (legend) {
|
|
1329
|
+
legend.innerHTML = labels.map((l, i) => `${ccEscape(l)}: ${ccEscape(data[i])}`).join(' · ');
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
async function ccShowUserDrillDown(email) {
|
|
1334
|
+
const data = await ccFetchJson(`/admin/security/events?user=${encodeURIComponent(email)}`);
|
|
1335
|
+
const wrap = document.getElementById('security-cc-timeline');
|
|
1336
|
+
if (!wrap) return;
|
|
1337
|
+
const events = (data && data.events) || [];
|
|
1338
|
+
if (!events.length) {
|
|
1339
|
+
wrap.innerHTML = `<div class="preview" style="padding:14px">${ccEscape(email)} 사용자에 대한 이벤트가 없습니다.</div>`;
|
|
1340
|
+
return;
|
|
1341
|
+
}
|
|
1342
|
+
const rows = events.slice(0, 40).map(e => `
|
|
1343
|
+
<tr>
|
|
1344
|
+
<td>${ccEscape((e.timestamp || '').slice(0, 19).replace('T', ' '))}</td>
|
|
1345
|
+
<td>${ccEscape(e.event_type || '')}</td>
|
|
1346
|
+
<td>${ccEscape(e.sensitivity || 'none')}</td>
|
|
1347
|
+
<td>${ccEscape((e.sensitive_labels || []).join(', '))}</td>
|
|
1348
|
+
<td>${ccEscape((e.content_preview || '').slice(0, 80))}</td>
|
|
1349
|
+
</tr>
|
|
1350
|
+
`).join('');
|
|
1351
|
+
wrap.innerHTML = `
|
|
1352
|
+
<div style="margin-bottom:8px;color:var(--muted-text);font-size:12px">${ccEscape(email)} 사용자의 보안 이벤트 ${events.length}건</div>
|
|
1353
|
+
<table class="data-table">
|
|
1354
|
+
<thead><tr><th>시각</th><th>유형</th><th>민감도</th><th>라벨</th><th>마스킹 preview</th></tr></thead>
|
|
1355
|
+
<tbody>${rows}</tbody>
|
|
1356
|
+
</table>`;
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
async function loadSecurityCommandCenter() {
|
|
1360
|
+
const [overview, usersResp, eventsResp, filesResp] = await Promise.all([
|
|
1361
|
+
ccFetchJson('/admin/security/overview'),
|
|
1362
|
+
ccFetchJson('/admin/security/users'),
|
|
1363
|
+
ccFetchJson('/admin/security/events?limit=50'),
|
|
1364
|
+
ccFetchJson('/admin/security/files'),
|
|
1365
|
+
]);
|
|
1366
|
+
|
|
1367
|
+
if (overview) {
|
|
1368
|
+
renderCcCards(overview);
|
|
1369
|
+
renderCcFieldChart(overview);
|
|
1370
|
+
}
|
|
1371
|
+
if (usersResp && Array.isArray(usersResp.users)) {
|
|
1372
|
+
renderCcUsersTable(usersResp.users);
|
|
1373
|
+
renderCcUserChart(usersResp.users);
|
|
1374
|
+
}
|
|
1375
|
+
if (eventsResp && Array.isArray(eventsResp.events)) {
|
|
1376
|
+
const chats = eventsResp.events.filter(e => (e.sensitivity || 'none') !== 'none' && e.event_type === 'chat_message').slice(0, 20);
|
|
1377
|
+
const chatWrap = document.getElementById('security-cc-chats');
|
|
1378
|
+
if (chatWrap) {
|
|
1379
|
+
chatWrap.innerHTML = chats.length ? `
|
|
1380
|
+
<table class="data-table">
|
|
1381
|
+
<thead><tr><th>시각</th><th>사용자</th><th>민감도</th><th>라벨</th><th>마스킹 preview</th></tr></thead>
|
|
1382
|
+
<tbody>${chats.map(e => `
|
|
1383
|
+
<tr>
|
|
1384
|
+
<td>${ccEscape((e.timestamp || '').slice(0, 19).replace('T', ' '))}</td>
|
|
1385
|
+
<td>${ccEscape(e.user_nickname || e.user_email || 'Unknown')}</td>
|
|
1386
|
+
<td>${ccEscape(e.sensitivity)}</td>
|
|
1387
|
+
<td>${ccEscape((e.sensitive_labels || []).join(', '))}</td>
|
|
1388
|
+
<td>${ccEscape((e.content_preview || '').slice(0, 100))}</td>
|
|
1389
|
+
</tr>`).join('')}
|
|
1390
|
+
</tbody>
|
|
1391
|
+
</table>` : '<div class="preview" style="padding:14px">감지된 민감 채팅이 없습니다.</div>';
|
|
1392
|
+
}
|
|
1393
|
+
const timelineWrap = document.getElementById('security-cc-timeline');
|
|
1394
|
+
if (timelineWrap && !timelineWrap.querySelector('table')) {
|
|
1395
|
+
const rows = eventsResp.events.slice(0, 30).map(e => `
|
|
1396
|
+
<tr>
|
|
1397
|
+
<td>${ccEscape((e.timestamp || '').slice(0, 19).replace('T', ' '))}</td>
|
|
1398
|
+
<td>${ccEscape(e.event_type || '')}</td>
|
|
1399
|
+
<td>${ccEscape(e.user_nickname || e.user_email || 'Unknown')}</td>
|
|
1400
|
+
<td>${ccEscape(e.sensitivity || 'none')}</td>
|
|
1401
|
+
</tr>
|
|
1402
|
+
`).join('');
|
|
1403
|
+
timelineWrap.innerHTML = rows ? `
|
|
1404
|
+
<table class="data-table">
|
|
1405
|
+
<thead><tr><th>시각</th><th>유형</th><th>사용자</th><th>민감도</th></tr></thead>
|
|
1406
|
+
<tbody>${rows}</tbody>
|
|
1407
|
+
</table>` : '<div class="preview" style="padding:14px">감사 이벤트가 없습니다.</div>';
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
if (filesResp && Array.isArray(filesResp.files)) {
|
|
1411
|
+
const files = filesResp.files.filter(f => (f.sensitivity || 'none') !== 'none' || (f.sensitive_labels || []).length > 0).slice(0, 20);
|
|
1412
|
+
const fileWrap = document.getElementById('security-cc-files');
|
|
1413
|
+
if (fileWrap) {
|
|
1414
|
+
fileWrap.innerHTML = files.length ? `
|
|
1415
|
+
<table class="data-table">
|
|
1416
|
+
<thead><tr><th>파일</th><th>업로드 사용자</th><th>민감도</th><th>라벨</th><th>크기</th></tr></thead>
|
|
1417
|
+
<tbody>${files.map(f => `
|
|
1418
|
+
<tr>
|
|
1419
|
+
<td>${ccEscape(f.filename || f.file_id)}</td>
|
|
1420
|
+
<td>${ccEscape(f.user_nickname || f.user_email || 'Unknown')}</td>
|
|
1421
|
+
<td>${ccEscape(f.sensitivity || 'none')}</td>
|
|
1422
|
+
<td>${ccEscape((f.sensitive_labels || []).join(', '))}</td>
|
|
1423
|
+
<td>${ccEscape(f.bytes || '')}</td>
|
|
1424
|
+
</tr>`).join('')}
|
|
1425
|
+
</tbody>
|
|
1426
|
+
</table>` : '<div class="preview" style="padding:14px">위험 등급 파일이 없습니다.</div>';
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
async function ccLoadRaw(scope) {
|
|
1432
|
+
const pre = document.getElementById('security-cc-raw');
|
|
1433
|
+
if (!pre) return;
|
|
1434
|
+
pre.textContent = '불러오는 중...';
|
|
1435
|
+
try {
|
|
1436
|
+
const res = await apiFetch(`/admin/security/raw?scope=${encodeURIComponent(scope)}`, { headers: adminHeaders() });
|
|
1437
|
+
if (!res.ok) { pre.textContent = `요청 실패 (HTTP ${res.status})`; return; }
|
|
1438
|
+
const text = await res.text();
|
|
1439
|
+
try {
|
|
1440
|
+
pre.textContent = JSON.stringify(JSON.parse(text), null, 2);
|
|
1441
|
+
} catch (_) {
|
|
1442
|
+
pre.textContent = text;
|
|
1443
|
+
}
|
|
1444
|
+
} catch (e) {
|
|
1445
|
+
pre.textContent = String(e);
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
async function ccExport(scope, format) {
|
|
1450
|
+
try {
|
|
1451
|
+
const res = await apiFetch('/admin/security/export', {
|
|
1452
|
+
method: 'POST',
|
|
1453
|
+
headers: { ...adminHeaders(), 'Content-Type': 'application/json' },
|
|
1454
|
+
body: JSON.stringify({ scope, format }),
|
|
1455
|
+
});
|
|
1456
|
+
if (!res.ok) {
|
|
1457
|
+
alert('보안 리포트 추출 실패 (HTTP ' + res.status + ')');
|
|
1458
|
+
return;
|
|
1459
|
+
}
|
|
1460
|
+
const blob = await res.blob();
|
|
1461
|
+
const url = URL.createObjectURL(blob);
|
|
1462
|
+
const a = document.createElement('a');
|
|
1463
|
+
a.href = url;
|
|
1464
|
+
a.download = `security_${scope}.${format === 'xlsx' ? 'xlsx' : format}`;
|
|
1465
|
+
a.click();
|
|
1466
|
+
setTimeout(() => URL.revokeObjectURL(url), 5000);
|
|
1467
|
+
} catch (e) {
|
|
1468
|
+
alert(String(e));
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
document.getElementById('security-cc-export-toggle')?.addEventListener('click', () => {
|
|
1473
|
+
const opts = document.getElementById('security-cc-export-options');
|
|
1474
|
+
if (opts) opts.classList.toggle('open');
|
|
1475
|
+
});
|
|
1476
|
+
document.querySelectorAll('[data-cc-scope][data-cc-format]').forEach(btn => {
|
|
1477
|
+
btn.addEventListener('click', () => ccExport(btn.dataset.ccScope, btn.dataset.ccFormat));
|
|
1478
|
+
});
|
|
1479
|
+
document.querySelectorAll('[data-cc-raw]').forEach(btn => {
|
|
1480
|
+
btn.addEventListener('click', () => ccLoadRaw(btn.dataset.ccRaw));
|
|
1481
|
+
});
|
|
1482
|
+
|
|
1483
|
+
// 보안 탭 진입 시 자동 로드
|
|
1484
|
+
document.querySelectorAll('[data-admin-nav="security"]').forEach(el => {
|
|
1485
|
+
el.addEventListener('click', () => { setTimeout(loadSecurityCommandCenter, 50); });
|
|
1486
|
+
});
|
|
1487
|
+
// 메뉴 셀렉터를 모를 수도 있으니 hash 변경 시에도 시도
|
|
1488
|
+
window.addEventListener('hashchange', () => {
|
|
1489
|
+
if (location.hash.indexOf('security') >= 0) loadSecurityCommandCenter();
|
|
1490
|
+
});
|
|
1491
|
+
|
|
1198
1492
|
loadDashboard();
|
|
1493
|
+
// 보안 콘솔도 첫 진입 시 로드 시도 (실패해도 무해)
|
|
1494
|
+
setTimeout(loadSecurityCommandCenter, 600);
|