dodo-mcp 1.3.2 → 1.4.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/dist/config/globalConfig.js +17 -0
- package/dist/config/globalConfig.js.map +1 -1
- package/dist/server/aiAdmin.js +20 -5
- package/dist/server/aiAdmin.js.map +1 -1
- package/dist/server/bootstrap.js +2 -1
- package/dist/server/bootstrap.js.map +1 -1
- package/dist/server/configUi/ui/recovery.js +415 -20
- package/dist/server/configUi/ui/recoveryMaintenance.js +2 -2
- package/dist/server/configUi/workbench.css +50 -1
- package/dist/server/configUi/workbench.js +193 -0
- package/dist/server/installationRuntime.js +1 -0
- package/dist/server/installationRuntime.js.map +1 -1
- package/dist/server/ipcDispatch.js +52 -3
- package/dist/server/ipcDispatch.js.map +1 -1
- package/dist/services/changes/planner.js +11 -2
- package/dist/services/changes/planner.js.map +1 -1
- package/dist/services/git/gitService.js +239 -0
- package/dist/services/git/gitService.js.map +1 -1
- package/dist/services/recovery/contracts.js +6 -0
- package/dist/services/recovery/contracts.js.map +1 -1
- package/dist/services/recovery/maintenance.js +8 -2
- package/dist/services/recovery/maintenance.js.map +1 -1
- package/dist/services/recovery/recoveryService.js +299 -48
- package/dist/services/recovery/recoveryService.js.map +1 -1
- package/dist/tools/context.js +11 -3
- package/dist/tools/context.js.map +1 -1
- package/dist/workspace/ignores.js +32 -6
- package/dist/workspace/ignores.js.map +1 -1
- package/docs/RELEASE_1.4.0.md +25 -0
- package/docs/RELEASE_NOTES.md +7 -0
- package/package.json +2 -1
- package/schemas/global-config.schema.json +20 -0
- package/scripts/setup-multimodal.mjs +0 -0
|
@@ -1,6 +1,172 @@
|
|
|
1
1
|
/* Owner Recovery dashboard. All untrusted values use textContent; no HTML injection. */
|
|
2
2
|
'use strict';
|
|
3
3
|
window.DodoRecovery=({el,section,button,check,field,choice,notice,confirmDanger,help,api,refresh,getSelected})=>{
|
|
4
|
+
function groupCard(step,title,desc){
|
|
5
|
+
const box=el('div',undefined,'rec-group');
|
|
6
|
+
const head=el('div',undefined,'rec-group-head');
|
|
7
|
+
const h=el('h3',undefined,'rec-group-title');
|
|
8
|
+
h.append(el('span',String(step),'rec-group-step'),document.createTextNode(title));
|
|
9
|
+
head.append(h);
|
|
10
|
+
if(desc)head.append(el('p',desc,'rec-group-desc'));
|
|
11
|
+
box.append(head);
|
|
12
|
+
return box;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function statCard(label,val,sub){
|
|
16
|
+
const box=el('div',undefined,'rec-stat');
|
|
17
|
+
box.append(el('div',label,'rec-stat-label'),el('div',String(val),'rec-stat-val'));
|
|
18
|
+
if(sub)box.append(el('div',sub,'rec-stat-sub'));
|
|
19
|
+
return box;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function gitRecoverySection(parent,act,data,selected){
|
|
23
|
+
const box=section('การกู้คืนจาก Git · Git Recovery','เมื่อโปรเจกต์เชื่อม Git ระบบสามารถสร้างจุดกู้คืน ดู Diff และย้อนไฟล์จาก Git Checkpoint หรือ Git Commit ได้โดยตรง โดยไม่กระทบ branch หรือ .git/index ของคุณ');
|
|
24
|
+
parent.append(box);
|
|
25
|
+
const body=el('div');box.append(body);
|
|
26
|
+
const previewBox=el('div');box.append(previewBox);
|
|
27
|
+
|
|
28
|
+
const loadGit=async()=>{
|
|
29
|
+
body.replaceChildren(el('p','กำลังตรวจสอบสถานะ Git ของโปรเจกต์…','wb-status'));
|
|
30
|
+
previewBox.replaceChildren();
|
|
31
|
+
try{
|
|
32
|
+
const g=await act('recovery.git.status',{limit:20});
|
|
33
|
+
body.replaceChildren();
|
|
34
|
+
if(!g.isRepo){
|
|
35
|
+
const empty=el('div',undefined,'rec-alert');
|
|
36
|
+
empty.dataset.tone='info';
|
|
37
|
+
empty.append(
|
|
38
|
+
el('p','โปรเจกต์นี้ยังไม่ได้เชื่อมต่อ Git (.git)','rec-alert-title'),
|
|
39
|
+
el('p','ระบบจึงใช้โหมด DODO Snapshot อัตโนมัติ หากคุณเริ่มต้นใช้งาน Git (git init) ในโฟลเดอร์โปรเจกต์ ระบบจะเปิดใช้งาน Git Recovery ให้ทันที','muted')
|
|
40
|
+
);
|
|
41
|
+
body.append(empty);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const stats=el('div',undefined,'rec-stats');
|
|
45
|
+
stats.append(
|
|
46
|
+
statCard('Branch ปัจจุบัน',g.branch||'Detached HEAD',g.head?`HEAD: ${g.head.slice(0,12)}`:'ยังไม่มี commit แรก'),
|
|
47
|
+
statCard('ไฟล์ที่แก้ไขค้าง (Modified)',`${g.dirtyCount} ไฟล์`,g.untrackedCount?`ไฟล์ใหม่ยังไม่อ้างอิง (Untracked): ${g.untrackedCount} ไฟล์`:'ไม่มีไฟล์ Untracked'),
|
|
48
|
+
statCard('DODO Git Checkpoints',`${g.checkpoints.length} จุดกู้คืน`,'เก็บใน refs/dodo/checkpoints/* แยกจาก branch'),
|
|
49
|
+
statCard('Git Commits ล่าสุด',`${g.commits.length} รายการ`,'สามารถเลือกดู Diff หรือกู้คืนไฟล์จาก commit ใดก็ได้')
|
|
50
|
+
);
|
|
51
|
+
body.append(stats);
|
|
52
|
+
|
|
53
|
+
if(g.entries&&g.entries.length){
|
|
54
|
+
const dirtyDetails=el('details',undefined,'details');
|
|
55
|
+
dirtyDetails.append(el('summary',`ดูรายการไฟล์ที่กำลังเปลี่ยนแปลงใน Working Tree (${g.entries.length} รายการ)`));
|
|
56
|
+
const pillList=el('ul',undefined,'rec-path-list');
|
|
57
|
+
for(const item of g.entries){
|
|
58
|
+
pillList.append(el('li',`${item.status} · ${item.path}`,'rec-path-pill'));
|
|
59
|
+
}
|
|
60
|
+
dirtyDetails.append(pillList);
|
|
61
|
+
body.append(dirtyDetails);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const pathFilter=field(body,'ระบุไฟล์หรือโฟลเดอร์ที่ต้องการกู้คืนจาก Git (เว้นว่างเพื่อกู้คืนทุกไฟล์ที่ต่างกัน)','gitRestorePath','');
|
|
65
|
+
pathFilter.placeholder='เช่น src/lib/env.ts หรือเว้นว่างเพื่อเลือกทั้งหมด';
|
|
66
|
+
|
|
67
|
+
const actions=el('div',undefined,'rec-action-bar');
|
|
68
|
+
actions.append(
|
|
69
|
+
button('สร้าง Git Checkpoint ตอนนี้',async()=>{
|
|
70
|
+
const cp=await act('recovery.git.checkpoint');
|
|
71
|
+
notice(`สร้าง Git Checkpoint สำเร็จ (${cp.id})`);
|
|
72
|
+
await loadGit();
|
|
73
|
+
},'btn primary'),
|
|
74
|
+
button('รีเฟรชข้อมูล Git',loadGit)
|
|
75
|
+
);
|
|
76
|
+
body.append(actions);
|
|
77
|
+
|
|
78
|
+
const showGitPreview=async(ref,label)=>{
|
|
79
|
+
previewBox.replaceChildren(el('p',`กำลังเปรียบเทียบไฟล์ปัจจุบันกับ ${label} (${ref})…`,'wb-status'));
|
|
80
|
+
try{
|
|
81
|
+
const rawPaths=pathFilter.value.split('\n').flatMap(s=>s.split(',')).map(s=>s.trim()).filter(Boolean);
|
|
82
|
+
const p=await act('recovery.git.preview',{ref,...(rawPaths.length?{paths:rawPaths}:{})});
|
|
83
|
+
const boxView=el('div',undefined,'rec-diff-box');
|
|
84
|
+
boxView.append(
|
|
85
|
+
el('h4',`ตรวจสอบ Diff ก่อนกู้คืนจาก Git: ${label}`),
|
|
86
|
+
el('p',`Commit SHA: ${p.resolvedSha} · พบไฟล์ที่ต่างจากสถานะปัจจุบัน ${p.changedFiles.length} ไฟล์`,'muted')
|
|
87
|
+
);
|
|
88
|
+
if(!p.changedFiles.length){
|
|
89
|
+
boxView.append(el('p','ไฟล์ในโปรเจกต์ปัจจุบันตรงกับจุดกู้คืน Git นี้อยู่แล้ว ไม่มีการเปลี่ยนแปลงที่ต้องกู้คืน','wb-status'));
|
|
90
|
+
previewBox.replaceChildren(boxView);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const filePills=el('ul',undefined,'rec-path-list');
|
|
94
|
+
for(const f of p.changedFiles){
|
|
95
|
+
filePills.append(el('li',`${f.status} · ${f.path}`,'rec-path-pill'));
|
|
96
|
+
}
|
|
97
|
+
boxView.append(filePills);
|
|
98
|
+
if(p.diff){
|
|
99
|
+
boxView.append(el('pre',p.diff));
|
|
100
|
+
}
|
|
101
|
+
const restoreBtn=button('ยืนยันกู้คืนไฟล์จาก Git จุดนี้',async()=>{
|
|
102
|
+
if(!await confirmDanger('กู้คืนไฟล์จาก Git?',`ระบบจะสร้าง Git Checkpoint สำรองสถานะปัจจุบันให้อัตโนมัติก่อน แล้วจึงย้อนไฟล์ ${p.changedFiles.length} รายการกลับไปยัง ${label} (${p.resolvedSha.slice(0,10)})`,'กู้คืนจาก Git'))return;
|
|
103
|
+
const res=await act('recovery.git.restore',{
|
|
104
|
+
ref:p.resolvedSha,
|
|
105
|
+
...(rawPaths.length?{paths:rawPaths}:{}),
|
|
106
|
+
workspaceId:selected.workspaceId,
|
|
107
|
+
workspaceEpoch:selected.workspaceEpoch,
|
|
108
|
+
confirm:true,
|
|
109
|
+
});
|
|
110
|
+
notice(`กู้คืนไฟล์จาก Git สำเร็จ (${res.restoredFiles.length} ไฟล์) · สำรองก่อนกู้ไว้ที่ ${res.backupCheckpointId}`);
|
|
111
|
+
await loadGit();
|
|
112
|
+
},'btn primary');
|
|
113
|
+
boxView.append(restoreBtn);
|
|
114
|
+
previewBox.replaceChildren(boxView);
|
|
115
|
+
restoreBtn.focus();
|
|
116
|
+
}catch(e){
|
|
117
|
+
previewBox.replaceChildren(el('p','ตรวจสอบ Diff จาก Git ไม่สำเร็จ: '+e.message,'wb-status'));
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const cols=el('div',undefined,'rec-git-columns');
|
|
122
|
+
const cpCol=el('div',undefined,'rec-group');
|
|
123
|
+
cpCol.append(el('h4','DODO Git Checkpoints (จุดกู้คืนอัตโนมัติ/กดสร้างเอง)'));
|
|
124
|
+
const cpList=el('div',undefined,'rec-git-list');
|
|
125
|
+
if(!g.checkpoints.length){
|
|
126
|
+
cpList.append(el('p','ยังไม่มี Git Checkpoint — กดปุ่ม “สร้าง Git Checkpoint ตอนนี้” เพื่อสร้างจุดกู้คืนแรกได้เลย','muted'));
|
|
127
|
+
}
|
|
128
|
+
for(const cp of g.checkpoints){
|
|
129
|
+
const item=el('div',undefined,'rec-item-card');
|
|
130
|
+
const head=el('div',undefined,'rec-item-head');
|
|
131
|
+
head.append(el('b',cp.id),el('code',cp.sha.slice(0,10)));
|
|
132
|
+
item.append(
|
|
133
|
+
head,
|
|
134
|
+
el('p',`${cp.subject||'DODO Git checkpoint'} · ${cp.date?new Date(cp.date).toLocaleString('th-TH'):''}`,'muted'),
|
|
135
|
+
el('div',undefined,'rec-item-actions')
|
|
136
|
+
);
|
|
137
|
+
item.lastChild.append(button('ดู Diff & กู้คืน',()=>showGitPreview(cp.ref,cp.id),'btn small'));
|
|
138
|
+
cpList.append(item);
|
|
139
|
+
}
|
|
140
|
+
cpCol.append(cpList);
|
|
141
|
+
|
|
142
|
+
const commitCol=el('div',undefined,'rec-group');
|
|
143
|
+
commitCol.append(el('h4','Git Commits ล่าสุดของโปรเจกต์ (git log)'));
|
|
144
|
+
const commitList=el('div',undefined,'rec-git-list');
|
|
145
|
+
if(!g.commits.length){
|
|
146
|
+
commitList.append(el('p','ยังไม่มีประวัติ git commit ใน branch ปัจจุบัน','muted'));
|
|
147
|
+
}
|
|
148
|
+
for(const c of g.commits){
|
|
149
|
+
const item=el('div',undefined,'rec-item-card');
|
|
150
|
+
const head=el('div',undefined,'rec-item-head');
|
|
151
|
+
head.append(el('b',c.subject||'(ไม่มีข้อความ commit)'),el('code',c.sha.slice(0,10)));
|
|
152
|
+
item.append(
|
|
153
|
+
head,
|
|
154
|
+
el('p',`${c.author} · ${c.date?new Date(c.date).toLocaleString('th-TH'):''}`,'muted'),
|
|
155
|
+
el('div',undefined,'rec-item-actions')
|
|
156
|
+
);
|
|
157
|
+
item.lastChild.append(button('ดู Diff & กู้คืน',()=>showGitPreview(c.sha,`Commit ${c.sha.slice(0,8)}`),'btn small'));
|
|
158
|
+
commitList.append(item);
|
|
159
|
+
}
|
|
160
|
+
commitCol.append(commitList);
|
|
161
|
+
cols.append(cpCol,commitCol);
|
|
162
|
+
body.append(cols);
|
|
163
|
+
}catch(e){
|
|
164
|
+
body.replaceChildren(el('p','โหลดข้อมูล Git Recovery ไม่สำเร็จ: '+e.message,'wb-status'));
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
await loadGit();
|
|
168
|
+
}
|
|
169
|
+
|
|
4
170
|
async function recoveryDrift(parent,act,data,selected){
|
|
5
171
|
const box=section('การเปลี่ยนไฟล์นอก journal','เปรียบเทียบ hash จริง ไม่ระบุว่าใครแก้จากเวลาไฟล์ การยอมรับสถานะไม่ใช่ผลทดสอบและไม่เพิ่มสิทธิ์');parent.append(box);
|
|
6
172
|
const view=el('div');box.append(view);
|
|
@@ -21,34 +187,264 @@ window.DodoRecovery=({el,section,button,check,field,choice,notice,confirmDanger,
|
|
|
21
187
|
}
|
|
22
188
|
|
|
23
189
|
async function recoveryPanel(parent) {
|
|
24
|
-
const card=section('Recovery ·
|
|
190
|
+
const card=section('Recovery · ระบบสำรองและกู้คืนโปรเจกต์','ตั้งค่าโหมดกู้คืน การใช้ Git อัตโนมัติ การไม่บล็อกงานเมื่อพบไฟล์ติดปัญหา และกำหนดขอบเขตการสำรองทั้งหมดผ่าน UI');parent.append(card);
|
|
25
191
|
const status=el('p','กำลังตรวจสอบจุดกู้คืน…','wb-status');status.setAttribute('role','status');card.append(status);
|
|
26
192
|
const selected={...getSelected()};
|
|
27
193
|
const act=(operation,args={})=>api('admin/action',{projectId:selected.projectId,operation,args},true);
|
|
28
194
|
try {
|
|
29
195
|
const data=await act('recovery.status');
|
|
30
|
-
const
|
|
196
|
+
const effectiveMode=data.effectiveMode||(data.hasGitRepo?'git':'snapshot');
|
|
197
|
+
const labels={
|
|
198
|
+
INITIALIZING:'กำลังเตรียมสำเนา — พร้อมใช้งานโดยไม่บล็อกการทำงาน',
|
|
199
|
+
READY:effectiveMode==='git'?'SAVED · พร้อมใช้งาน (โหมด Git Recovery)':'SAVED · มีสำเนาที่ตรวจ integrity แล้ว (โหมด Snapshot)',
|
|
200
|
+
BLOCKED:'สำรองไม่ได้ — หยุดการเปลี่ยน source (สามารถกดล้างสถานะบล็อกหรือปิด Strict Mode ด้านล่าง)',
|
|
201
|
+
DISABLED_BY_OWNER:'เจ้าของปิดการสำรองอัตโนมัติ',
|
|
202
|
+
};
|
|
31
203
|
status.textContent=labels[data.state]||data.state;
|
|
32
|
-
if(data.awaitingActivation)status.textContent='เปิดการป้องกันไว้ — รอสร้างจุดกู้คืนแรก';
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
204
|
+
if(data.awaitingActivation&&data.enabled)status.textContent='เปิดการป้องกันไว้ — รอสร้างจุดกู้คืนแรก';
|
|
205
|
+
|
|
206
|
+
const headerBar=el('div',undefined,'rec-header');
|
|
207
|
+
const headerCopy=el('div',undefined,'rec-header-copy');
|
|
208
|
+
headerCopy.append(
|
|
209
|
+
el('strong',data.hasGitRepo?'ตรวจพบ Git Repository ในโปรเจกต์นี้ (.git)':'โปรเจกต์ทั่วไป (ยังไม่ได้เชื่อม Git)'),
|
|
210
|
+
el('span',data.policy.strictEnforcement?'โหมดเข้มงวด (Strict Blocking เปิดอยู่)':'โหมดไม่บังคับบล็อก (แก้ไฟล์และรันคำสั่งได้ตลอดเวลา ไม่ติด RECOVERY_REQUIRED)','muted')
|
|
211
|
+
);
|
|
212
|
+
const badge=el('span',
|
|
213
|
+
data.state==='BLOCKED'?`ติดบล็อก · ${data.errorCode||'BLOCKED'}`:
|
|
214
|
+
data.state==='DISABLED_BY_OWNER'?'ปิดการใช้งาน':
|
|
215
|
+
effectiveMode==='git'?'พร้อมใช้งาน · Git Recovery':'พร้อมใช้งาน · DODO Snapshot',
|
|
216
|
+
'rec-badge'
|
|
217
|
+
);
|
|
218
|
+
badge.dataset.tone=data.state==='BLOCKED'?'danger':data.state==='DISABLED_BY_OWNER'?'muted':'ok';
|
|
219
|
+
headerBar.append(headerCopy,badge);
|
|
220
|
+
card.append(headerBar);
|
|
221
|
+
|
|
222
|
+
const metricGrid=el('div',undefined,'rec-stats');
|
|
223
|
+
metricGrid.append(
|
|
224
|
+
statCard(
|
|
225
|
+
'โหมดที่ใช้งานจริง',
|
|
226
|
+
effectiveMode==='git'?'Git Recovery':effectiveMode==='off'?'ปิดการใช้งาน':'DODO Snapshot',
|
|
227
|
+
`ตั้งค่า: ${(data.policy.mode||'auto').toUpperCase()} · ${data.policy.strictEnforcement?'บล็อกเมื่อพบปัญหา':'ไม่บล็อกงาน'}`
|
|
228
|
+
),
|
|
229
|
+
statCard(
|
|
230
|
+
'การตรวจสอบล่าสุด',
|
|
231
|
+
data.lastCheckedAt?new Date(data.lastCheckedAt).toLocaleTimeString('th-TH'):'ยังไม่มี',
|
|
232
|
+
data.lastCheckedAt?new Date(data.lastCheckedAt).toLocaleDateString('th-TH'):`เหตุการณ์: ${data.lastTrigger||'ยังไม่มี'}`
|
|
233
|
+
),
|
|
234
|
+
statCard(
|
|
235
|
+
'ไฟล์ที่คุ้มครอง / ยกเว้น',
|
|
236
|
+
`${data.counts.included} ไฟล์`,
|
|
237
|
+
`ยกเว้นตามนโยบาย ${data.counts.excludedByPolicy} รายการ · ข้าม Secret ${(data.skippedSecretFiles||[]).length} ไฟล์`
|
|
238
|
+
),
|
|
239
|
+
statCard(
|
|
240
|
+
'พื้นที่จัดเก็บโปรเจกต์',
|
|
241
|
+
`${(data.projectBytes/1048576).toFixed(2)} MiB`,
|
|
242
|
+
`โควตา ${(data.policy.projectBytes/1073741824).toFixed(1)} GiB · สแกนแล้ว ${data.progress.scanned} รายการ`
|
|
243
|
+
)
|
|
244
|
+
);
|
|
245
|
+
card.append(metricGrid);
|
|
246
|
+
|
|
247
|
+
// Diagnostic & Quick-Fix Callout for blockedPath, errorCode, skippedSecretFiles, or lastWarning
|
|
248
|
+
const problemPaths=[
|
|
249
|
+
...(data.blockedPath?[data.blockedPath]:[]),
|
|
250
|
+
...(Array.isArray(data.skippedSecretFiles)?data.skippedSecretFiles:[]),
|
|
251
|
+
].filter((v,i,a)=>Boolean(v)&&a.indexOf(v)===i);
|
|
252
|
+
|
|
253
|
+
if(data.state==='BLOCKED'||data.errorCode||data.lastWarning||problemPaths.length){
|
|
254
|
+
const alertBox=el('div',undefined,'rec-alert');
|
|
255
|
+
alertBox.dataset.tone=data.state==='BLOCKED'?'danger':'warn';
|
|
256
|
+
alertBox.append(
|
|
257
|
+
el('p',
|
|
258
|
+
data.state==='BLOCKED'
|
|
259
|
+
?`พบปัญหาที่ทำให้ Recovery ติดสถานะ BLOCKED (${data.errorCode||'UNKNOWN'})`
|
|
260
|
+
:`คำเตือนจากระบบ Recovery (ไม่บล็อกการแก้ไฟล์หรือรันคำสั่ง)`,
|
|
261
|
+
'rec-alert-title'
|
|
262
|
+
)
|
|
263
|
+
);
|
|
264
|
+
if(data.errorDetail||data.lastWarning){
|
|
265
|
+
alertBox.append(el('p',data.errorDetail||data.lastWarning,'muted'));
|
|
266
|
+
}
|
|
267
|
+
if(problemPaths.length){
|
|
268
|
+
alertBox.append(el('p','ไฟล์ที่เป็นต้นเหตุ (เช่น มีข้อความคล้าย API key / Private key หรือขนาดเกินขีดจำกัด):'));
|
|
269
|
+
const ul=el('ul',undefined,'rec-path-list');
|
|
270
|
+
for(const p of problemPaths)ul.append(el('li',p,'rec-path-pill'));
|
|
271
|
+
alertBox.append(ul);
|
|
272
|
+
}else if(data.errorCode==='SECRET_PATH_DENIED'){
|
|
273
|
+
alertBox.append(el('p','คำแนะนำ: มักเกิดจากไฟล์ตัวอย่างที่มีรูปแบบคีย์ เช่น env.example, scripts/setup-env.sh หรือ src/lib/env.ts คุณสามารถกดปุ่มด้านล่างเพื่อล้างสถานะบล็อก หรือเพิ่มไฟล์เข้า Exclude ได้ทันที','muted'));
|
|
274
|
+
}
|
|
275
|
+
const quickActions=el('div',undefined,'rec-action-bar');
|
|
276
|
+
if(problemPaths.length){
|
|
277
|
+
quickActions.append(button('เพิ่มไฟล์ต้นเหตุเข้า Exclude ทันที',async()=>{
|
|
278
|
+
const merged=[...new Set([...(data.policy.excludePaths||[]),...problemPaths])];
|
|
279
|
+
await act('recovery.configure',{confirm:true,policy:{...data.policy,strictEnforcement:false,skipSecretContentFiles:true,excludePaths:merged}});
|
|
280
|
+
await act('recovery.reset');
|
|
281
|
+
notice(`เพิ่ม ${problemPaths.join(', ')} เข้า Exclude และปลดบล็อกเรียบร้อยแล้ว`);
|
|
282
|
+
await refresh();
|
|
283
|
+
},'btn primary'));
|
|
284
|
+
quickActions.append(button('อนุญาตไฟล์ต้นเหตุในสิทธิ์ไฟล์ (Secret Allow)',async()=>{
|
|
285
|
+
const cfg=await api('admin/config');
|
|
286
|
+
const nextAllow=[...new Set([...(cfg.secretAllow||[]),...problemPaths])];
|
|
287
|
+
await api('admin/config',{secretAllow:nextAllow});
|
|
288
|
+
await act('recovery.configure',{confirm:true,policy:{...data.policy,strictEnforcement:false,skipSecretContentFiles:true}});
|
|
289
|
+
await act('recovery.reset');
|
|
290
|
+
notice(`อนุญาต ${problemPaths.join(', ')} ในสิทธิ์ไฟล์และปลดบล็อกเรียบร้อยแล้ว`);
|
|
291
|
+
await refresh();
|
|
292
|
+
},'btn'));
|
|
293
|
+
}
|
|
294
|
+
if(data.errorCode==='SECRET_PATH_DENIED'||problemPaths.length){
|
|
295
|
+
quickActions.append(button('อนุญาตทุกไฟล์ (ปิดการบล็อก SECRET_PATH_DENIED)',async()=>{
|
|
296
|
+
if(!await confirmDanger('ยืนยันอนุญาตให้เข้าถึงทุกไฟล์?','ระบบจะเปิดโหมดอนุญาตทุกไฟล์ (Allow All) และปิดการบล็อก SECRET_PATH_DENIED ทั้งชื่อไฟล์และเนื้อหา เพื่อให้คุณและ AI แก้ไขได้ทุกไฟล์','ยืนยันอนุญาตทุกไฟล์'))return;
|
|
297
|
+
await api('admin/config',{secretPolicyMode:'off'});
|
|
298
|
+
await act('recovery.configure',{confirm:true,policy:{...data.policy,strictEnforcement:false,scanContentSecrets:false,skipSecretContentFiles:true}});
|
|
299
|
+
await act('recovery.reset');
|
|
300
|
+
notice('เปิดโหมดอนุญาตทุกไฟล์และปลดบล็อกเรียบร้อยแล้ว');
|
|
301
|
+
await refresh();
|
|
302
|
+
},'btn'));
|
|
303
|
+
}
|
|
304
|
+
if(data.hasGitRepo&&data.policy.mode!=='git'){
|
|
305
|
+
quickActions.append(button('สลับใช้ Git Recovery ล้วน (ไม่สแกนไฟล์ CAS)',async()=>{
|
|
306
|
+
await act('recovery.configure',{confirm:true,policy:{...data.policy,enabled:true,mode:'git',strictEnforcement:false}});
|
|
307
|
+
await act('recovery.reset');
|
|
308
|
+
notice('สลับเป็นโหมด Git Recovery เรียบร้อยแล้ว');
|
|
309
|
+
await refresh();
|
|
310
|
+
},'btn'));
|
|
311
|
+
}
|
|
312
|
+
quickActions.append(button('ล้างสถานะบล็อก / สแกนใหม่ทันที',async()=>{
|
|
313
|
+
await act('recovery.reset');
|
|
314
|
+
notice('ล้างสถานะบล็อกและสแกนใหม่เรียบร้อยแล้ว');
|
|
315
|
+
await refresh();
|
|
316
|
+
},'btn'));
|
|
317
|
+
alertBox.append(quickActions);
|
|
318
|
+
card.append(alertBox);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// ---- Git Recovery Section ----
|
|
322
|
+
await gitRecoverySection(card,act,data,selected);
|
|
323
|
+
|
|
324
|
+
// ---- Categorized 4-Group Settings Dashboard ----
|
|
325
|
+
const settingsHeader=el('h3','ตั้งค่าระบบ Recovery ทั้งหมด (แยกตามหมวดหมู่)');
|
|
326
|
+
card.append(settingsHeader);
|
|
327
|
+
const settingsGrid=el('div',undefined,'rec-settings-grid');
|
|
328
|
+
card.append(settingsGrid);
|
|
329
|
+
|
|
330
|
+
// Group 1: Mode & Blocking Behavior
|
|
331
|
+
const g1=groupCard(1,'โหมดการทำงานและการบล็อกงาน','กำหนดว่าจะใช้ Git หรือ Snapshot และป้องกันไม่ให้ระบบบล็อกการแก้ไฟล์/รันคำสั่ง');
|
|
332
|
+
const modeSelect=choice(g1,'โหมดการสำรองและกู้คืน (Recovery Mode)','recoveryMode',[
|
|
333
|
+
['auto','Auto (แนะนำ — ใช้ Git อัตโนมัติถ้าโปรเจกต์มี .git)'],
|
|
334
|
+
['git','Git เท่านั้น (ใช้ Git ล้วน ไม่สแกนไฟล์ CAS ของ DODO)'],
|
|
335
|
+
['snapshot','DODO Snapshot (สำรองไฟล์เข้าคลัง CAS ของ DODO)'],
|
|
336
|
+
['off','ปิดการใช้งาน Recovery'],
|
|
337
|
+
],data.policy.mode||'auto');
|
|
338
|
+
const enabled=check(g1,'เปิดการสำรองอัตโนมัติ','recoveryEnabled',data.enabled);enabled.setAttribute('role','switch');
|
|
339
|
+
const strictEnforcement=check(g1,'บังคับบล็อกการแก้ไฟล์/รันคำสั่งเมื่อสำรองไม่สำเร็จ (Strict Mode — แนะนำให้ปิด)','recoveryStrict',Boolean(data.policy.strictEnforcement));
|
|
340
|
+
strictEnforcement.setAttribute('role','switch');
|
|
341
|
+
g1.append(el('p','เมื่อปิด Strict Mode: ต่อให้ไฟล์ติด SECRET_PATH_DENIED หรือเกิดข้อผิดพลาดระหว่างสำรอง คุณยังสามารถแก้ไฟล์และรันคำสั่งได้ตามปกติ','muted'));
|
|
342
|
+
settingsGrid.append(g1);
|
|
343
|
+
|
|
344
|
+
// Group 2: Secret Content Screening & Drift Policy
|
|
345
|
+
const g2=groupCard(2,'การตรวจไฟล์ความลับและไฟล์เปลี่ยนภายนอก','จัดการการตรวจจับคีย์ตัวอย่างในโค้ด (เช่น env.example) และการแก้ไฟล์จากภายนอก');
|
|
346
|
+
const scanContentSecrets=check(g2,'สแกนหาคีย์ความลับในเนื้อหาไฟล์ระหว่างสำรอง Snapshot','recoveryScanSecrets',data.policy.scanContentSecrets!==false);
|
|
347
|
+
scanContentSecrets.setAttribute('role','switch');
|
|
348
|
+
const skipSecretContentFiles=check(g2,'ข้ามไฟล์ที่พบเนื้อหาคล้าย Secret อัตโนมัติ (ไม่ให้ติดบล็อก SECRET_PATH_DENIED)','recoverySkipSecrets',data.policy.skipSecretContentFiles!==false);
|
|
349
|
+
skipSecretContentFiles.setAttribute('role','switch');
|
|
350
|
+
const checkDrift=check(g2,'ตรวจจับการแก้ไฟล์จากภายนอก DODO (Drift Detection)','recoveryCheckDrift',data.policy.checkDrift!==false);
|
|
351
|
+
checkDrift.setAttribute('role','switch');
|
|
352
|
+
const driftCols=el('div',undefined,'rec-three-col');
|
|
353
|
+
const massFilesInput=field(driftCols,'จำนวนไฟล์เปลี่ยนเตือน (ไฟล์)','recoveryMassFiles',data.policy.massFiles??20,'number');massFilesInput.min='1';massFilesInput.max='100000';
|
|
354
|
+
const massPercentInput=field(driftCols,'สัดส่วนไฟล์เปลี่ยน (%)','recoveryMassPercent',data.policy.massPercent??20,'number');massPercentInput.min='0';massPercentInput.max='100';
|
|
355
|
+
const deletedFilesInput=field(driftCols,'จำนวนไฟล์ถูกลบเตือน','recoveryDeletedFiles',data.policy.deletedFiles??10,'number');deletedFilesInput.min='1';deletedFilesInput.max='100000';
|
|
356
|
+
g2.append(driftCols);
|
|
357
|
+
settingsGrid.append(g2);
|
|
358
|
+
|
|
359
|
+
// Group 3: Exclusions & Data Roots
|
|
360
|
+
const g3=groupCard(3,'ไฟล์และโฟลเดอร์ที่ยกเว้น (Exclusions)','ระบุไฟล์หรือโฟลเดอร์ที่ไม่ต้องการให้ DODO สแกนสำรอง (เช่น node_modules, .next, env.example)');
|
|
361
|
+
const excludePathsField=field(g3,'รายการไฟล์/โฟลเดอร์ที่ยกเว้น (บรรทัดละ 1 รายการ)','recoveryExcludePaths',(data.policy.excludePaths||[]).join('\n'),'textarea');
|
|
362
|
+
excludePathsField.placeholder='node_modules\n.next\nenv.example\nscripts/setup-env.sh';
|
|
363
|
+
const chipBar=el('div',undefined,'rec-chip-bar');
|
|
364
|
+
chipBar.append(el('span','เพิ่มด่วน:','muted'));
|
|
365
|
+
for(const preset of ['node_modules','.next','dist','build','models','env.example','scripts/setup-env.sh','src/lib/env.ts','.env.docker']){
|
|
366
|
+
const chip=el('button',`+ ${preset}`,'rec-chip-btn');
|
|
367
|
+
chip.type='button';
|
|
368
|
+
chip.onclick=()=>{
|
|
369
|
+
const lines=excludePathsField.value.split('\n').map(s=>s.trim()).filter(Boolean);
|
|
370
|
+
if(!lines.includes(preset)){
|
|
371
|
+
lines.push(preset);
|
|
372
|
+
excludePathsField.value=lines.join('\n');
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
chipBar.append(chip);
|
|
376
|
+
}
|
|
377
|
+
g3.append(chipBar);
|
|
378
|
+
const roots=field(g3,'โฟลเดอร์ข้อมูล runtime ที่ไม่สำรอง (relative path หนึ่งรายการต่อบรรทัด)','recoveryDataRoots',(data.policy.dataRoots||[]).join('\n'),'textarea');
|
|
379
|
+
roots.placeholder='data\nuploads';
|
|
380
|
+
settingsGrid.append(g3);
|
|
381
|
+
|
|
382
|
+
// Group 4: Quotas, Scan Limits & Retention
|
|
383
|
+
const g4=groupCard(4,'โควตาพื้นที่ ขีดจำกัดการสแกน และระยะเวลาเก็บ','ปรับแต่งขนาดพื้นที่สำรอง ขีดจำกัดขนาดไฟล์ เวลาสแกน และจำนวนจุดกู้คืน');
|
|
384
|
+
const quotaCols=el('div',undefined,'rec-two-col');
|
|
385
|
+
const projectGiBInput=field(quotaCols,'โควตาโปรเจกต์ (GiB)','recoveryProjectGiB',Number((data.policy.projectBytes/1073741824).toFixed(2)),'number');projectGiBInput.min='0.01';projectGiBInput.max='100';projectGiBInput.step='0.1';
|
|
386
|
+
const fileMiBInput=field(quotaCols,'ขนาดไฟล์สูงสุด (MiB)','recoveryFileMiB',Number((data.policy.fileBytes/1048576).toFixed(2)),'number');fileMiBInput.min='1';fileMiBInput.max='1024';fileMiBInput.step='1';
|
|
387
|
+
const maxEntriesInput=field(quotaCols,'จำนวนไฟล์สูงสุดที่สแกน','recoveryMaxEntries',data.policy.maxEntries??100000,'number');maxEntriesInput.min='100';maxEntriesInput.max='1000000';
|
|
388
|
+
const scanMsInput=field(quotaCols,'เวลาสแกนสูงสุด (ms)','recoveryScanMs',data.policy.scanMs??15000,'number');scanMsInput.min='500';scanMsInput.max='60000';
|
|
389
|
+
const retentionDaysInput=field(quotaCols,'ระยะเวลาเก็บสำเนา (วัน)','recoveryRetentionDays',data.policy.retentionDays??30,'number');retentionDaysInput.min='1';retentionDaysInput.max='3650';
|
|
390
|
+
const retainedPointsInput=field(quotaCols,'จำนวนจุดกู้คืนสูงสุด','recoveryRetainedPoints',data.policy.retainedPoints??200,'number');retainedPointsInput.min='1';retainedPointsInput.max='10000';
|
|
391
|
+
g4.append(quotaCols);
|
|
392
|
+
|
|
393
|
+
const advanced=el('details',undefined,'details');advanced.append(el('summary','ขอบเขตและพื้นที่สำรอง (Git Copy แยกโฟลเดอร์ขั้นสูง)'));
|
|
40
394
|
const gitRequired=check(advanced,'ต้องมี Git recovery copy ก่อนทำงาน','recoveryGitRequired',data.policy.gitRequired);
|
|
41
395
|
const gitDirectory=field(advanced,'ที่เก็บ Git สำรอง (absolute path; เว้นว่างใช้ private state)','recoveryGitDirectory',data.policy.gitDirectory||'');
|
|
42
|
-
advanced.append(el('p','โฟลเดอร์สำรองต้องมีอยู่แล้ว เป็น private ของเจ้าของ และอยู่นอกโปรเจกต์
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
396
|
+
advanced.append(el('p','โฟลเดอร์สำรองต้องมีอยู่แล้ว เป็น private ของเจ้าของ และอยู่นอกโปรเจกต์','muted'));
|
|
397
|
+
g4.append(advanced);
|
|
398
|
+
settingsGrid.append(g4);
|
|
399
|
+
|
|
400
|
+
modeSelect.onchange=()=>{
|
|
401
|
+
if(modeSelect.value==='off')enabled.checked=false;
|
|
402
|
+
else enabled.checked=true;
|
|
403
|
+
};
|
|
404
|
+
enabled.onchange=()=>{
|
|
405
|
+
if(!enabled.checked&&modeSelect.value!=='off')modeSelect.value='off';
|
|
406
|
+
else if(enabled.checked&&modeSelect.value==='off')modeSelect.value='auto';
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
const mainActions=el('div',undefined,'rec-action-bar');
|
|
410
|
+
mainActions.append(
|
|
411
|
+
button('บันทึกการสำรอง',async()=>{
|
|
412
|
+
const isEnabled=enabled.checked&&modeSelect.value!=='off';
|
|
413
|
+
if(!isEnabled&&!await confirmDanger('ปิดการสำรองอัตโนมัติ?','การแก้ไฟล์และคำสั่งหลังจากนี้จะไม่มี source checkpoint ใหม่ สำเนาเดิมและ file journal ยังอยู่','ปิดการสำรอง'))return;
|
|
414
|
+
const nextPolicy={
|
|
415
|
+
...data.policy,
|
|
416
|
+
enabled:isEnabled,
|
|
417
|
+
mode:isEnabled?(modeSelect.value==='off'?'auto':modeSelect.value):'off',
|
|
418
|
+
strictEnforcement:strictEnforcement.checked,
|
|
419
|
+
scanContentSecrets:scanContentSecrets.checked,
|
|
420
|
+
skipSecretContentFiles:skipSecretContentFiles.checked,
|
|
421
|
+
checkDrift:checkDrift.checked,
|
|
422
|
+
massFiles:Math.max(1,Math.round(Number(massFilesInput.value)||20)),
|
|
423
|
+
massPercent:Math.min(100,Math.max(0,Number(massPercentInput.value)||20)),
|
|
424
|
+
deletedFiles:Math.max(1,Math.round(Number(deletedFilesInput.value)||10)),
|
|
425
|
+
projectBytes:Math.max(1048576,Math.round((Number(projectGiBInput.value)||5)*1073741824)),
|
|
426
|
+
fileBytes:Math.max(1024,Math.round((Number(fileMiBInput.value)||64)*1048576)),
|
|
427
|
+
maxEntries:Math.max(1,Math.round(Number(maxEntriesInput.value)||100000)),
|
|
428
|
+
scanMs:Math.min(60000,Math.max(100,Math.round(Number(scanMsInput.value)||15000))),
|
|
429
|
+
retentionDays:Math.max(1,Math.round(Number(retentionDaysInput.value)||30)),
|
|
430
|
+
retainedPoints:Math.max(1,Math.round(Number(retainedPointsInput.value)||200)),
|
|
431
|
+
gitRequired:gitRequired.checked,
|
|
432
|
+
gitDirectory:gitDirectory.value.trim()||null,
|
|
433
|
+
excludePaths:excludePathsField.value.split('\n').map(s=>s.trim()).filter(Boolean),
|
|
434
|
+
dataRoots:roots.value.split('\n').map(s=>s.trim()).filter(Boolean),
|
|
435
|
+
};
|
|
436
|
+
const result=await act('recovery.configure',{confirm:true,policy:nextPolicy});
|
|
437
|
+
notice(result.enabled?'บันทึกการตั้งค่า Recovery ทั้งหมดเรียบร้อยแล้ว':'บันทึกแล้ว เจ้าของปิดการสำรองอัตโนมัติ');
|
|
438
|
+
await refresh();
|
|
439
|
+
},'btn primary'),
|
|
440
|
+
button('สร้างจุดกู้คืนตอนนี้',async()=>{await act('recovery.checkpoint');notice('สร้างจุดกู้คืนเรียบร้อยแล้ว');await refresh();}),
|
|
441
|
+
button('ล้างสถานะบล็อก / สแกนใหม่',async()=>{await act('recovery.reset');notice('ล้างสถานะบล็อกและสแกนใหม่แล้ว');await refresh();}),
|
|
442
|
+
button('ตรวจสถานะใหม่',async()=>refresh())
|
|
443
|
+
);
|
|
444
|
+
card.append(mainActions);
|
|
51
445
|
card.append(el('p','กู้คืนเฉพาะ source ที่แสดงในแผน ไม่รวมฐานข้อมูล secrets, volumes หรือผลภายนอก และไม่ใช่ OS sandbox','muted'));
|
|
446
|
+
|
|
447
|
+
await window.DodoRecoveryMaintenance({parent:card,act,selected,el,section,button,field,check,notice,confirmDanger,refresh});
|
|
52
448
|
await verificationPanel(card,act,selected);
|
|
53
449
|
await recoveryDrift(card,act,data,selected);
|
|
54
450
|
await recoveryHistory(card,act,selected);
|
|
@@ -115,7 +511,6 @@ window.DodoRecovery=({el,section,button,check,field,choice,notice,confirmDanger,
|
|
|
115
511
|
await load();
|
|
116
512
|
}
|
|
117
513
|
|
|
118
|
-
|
|
119
514
|
async function verificationPanel(parent,act,selected){
|
|
120
515
|
const box=section('หลักฐานและชื่อสำเนา','SAVED คือมีสำเนา · VERIFIED คือ checks ที่เลือกผ่าน · stable เป็นชื่อที่เจ้าของตั้ง');parent.append(box);
|
|
121
516
|
box.append(help('ผลทดสอบอ้างเฉพาะ source และ recipes ที่ตรวจในเวลานั้น ไม่ครอบคลุม environment ภายนอก การแก้แล้วเปลี่ยนกลับระหว่าง checks หรือความถูกต้องทุกกรณี','ขอบเขตหลักฐาน'));
|
|
@@ -7,8 +7,8 @@ window.DodoRecoveryMaintenance=async({parent,act,selected,el,section,button,fiel
|
|
|
7
7
|
const quota=field(settings,'โควตา Recovery ของโปรเจกต์ (GiB)','maintenanceQuota','','number');quota.min='0.001';quota.max='100';quota.step='0.1';
|
|
8
8
|
const days=field(settings,'ระยะเก็บสำรอง (วัน)','maintenanceDays','','number');days.min='1';days.max='3650';
|
|
9
9
|
const count=field(settings,'จำนวนสำเนาที่เก็บตาม retention','maintenanceCount','','number');count.min='1';count.max='10000';
|
|
10
|
-
const excludes=field(settings,'ไม่สำรองไฟล์หรือโฟลเดอร์ (relative path บรรทัดละรายการ)','maintenanceExcludes','','textarea');excludes.placeholder='models
|
|
11
|
-
settings.append(el('p','เช่น models
|
|
10
|
+
const excludes=field(settings,'ไม่สำรองไฟล์หรือโฟลเดอร์ (relative path บรรทัดละรายการ)','maintenanceExcludes','','textarea');excludes.placeholder='models/\nnode_modules/\n.next/\nenv.example';
|
|
11
|
+
settings.append(el('p','เช่น models/, node_modules/, .next/, env.example — ใช้กับสำเนาใหม่เท่านั้น สำเนาเก่ายังคงอยู่ ในโหมดไม่บังคับบล็อกสามารถแก้ไฟล์ที่ยกเว้นได้ตามปกติ','muted'));
|
|
12
12
|
let cursor=0;const chosen=new Set();const size=n=>(n/1048576).toFixed(2)+' MiB';
|
|
13
13
|
const reasons={active_baseline:'จุดปัจจุบัน',latest_backup:'สำเนาล่าสุด',pinned:'Pin ไว้',drift_baseline:'ใช้ตรวจไฟล์เปลี่ยน',named_checkpoint:'มีชื่ออ้างถึง',deployment_provenance:'ใช้กับ deployment',database_compatibility_rule:'ผูกกติกาฐานข้อมูล',reviewed_restore_plan:'มีแผนกู้คืนใช้อยู่',open_session:'session ยังเปิด',unfinished_work:'งานยังไม่จบ',select_complete_session:'ต้องเลือกสำเนาของ session ให้ครบ',other_caller_session:'ประวัติของผู้เรียกอื่น',active_capture:'กำลังสำรอง'};
|
|
14
14
|
const review=async(operation,args)=>{
|
|
@@ -86,4 +86,53 @@
|
|
|
86
86
|
.wb-receipt { border:1px solid var(--border);border-radius:8px;padding:12px;min-width:0; }
|
|
87
87
|
.wb-receipt summary { cursor:pointer;font-size:.9rem;overflow-wrap:anywhere; }
|
|
88
88
|
.wb-receipt summary:focus-visible { outline:2px solid var(--accent);outline-offset:4px; }
|
|
89
|
-
|
|
89
|
+
|
|
90
|
+
/* ---- Recovery Dashboard & Categorized Settings UI ---- */
|
|
91
|
+
.rec-header { display:flex;align-items:flex-start;justify-content:space-between;gap:var(--s3);flex-wrap:wrap;padding-bottom:var(--s3);border-bottom:1px solid var(--border); }
|
|
92
|
+
.rec-header-copy { display:flex;flex-direction:column;gap:4px;min-width:0; }
|
|
93
|
+
.rec-badge { display:inline-flex;align-items:center;gap:7px;padding:6px 12px;border-radius:999px;font-size:.82rem;font-weight:700;border:1px solid var(--border-strong);background:var(--panel-2);color:var(--text); }
|
|
94
|
+
.rec-badge[data-tone="ok"] { color:var(--ok);border-color:color-mix(in srgb,var(--ok) 48%,var(--border));background:color-mix(in srgb,var(--ok) 12%,var(--panel-2)); }
|
|
95
|
+
.rec-badge[data-tone="warn"] { color:var(--warn);border-color:color-mix(in srgb,var(--warn) 48%,var(--border));background:color-mix(in srgb,var(--warn) 12%,var(--panel-2)); }
|
|
96
|
+
.rec-badge[data-tone="danger"] { color:var(--danger);border-color:color-mix(in srgb,var(--danger) 48%,var(--border));background:color-mix(in srgb,var(--danger) 12%,var(--panel-2)); }
|
|
97
|
+
.rec-badge[data-tone="info"] { color:var(--info);border-color:color-mix(in srgb,var(--info) 48%,var(--border));background:color-mix(in srgb,var(--info) 12%,var(--panel-2)); }
|
|
98
|
+
.rec-stats { display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:var(--s3); }
|
|
99
|
+
.rec-stat { border:1px solid var(--border);border-radius:var(--radius-sm);padding:12px 14px;background:var(--panel-2);display:flex;flex-direction:column;gap:4px;min-width:0; }
|
|
100
|
+
.rec-stat-label { color:var(--muted);font-size:.78rem;font-weight:600;text-transform:uppercase;letter-spacing:.02em; }
|
|
101
|
+
.rec-stat-val { font-family:var(--mono);font-size:1rem;font-weight:700;color:var(--text);overflow-wrap:anywhere; }
|
|
102
|
+
.rec-stat-sub { color:var(--muted);font-size:.8rem;overflow-wrap:anywhere; }
|
|
103
|
+
.rec-alert { border:1px solid var(--border-strong);border-radius:var(--radius-sm);padding:14px 16px;background:var(--panel-2);display:flex;flex-direction:column;gap:10px; }
|
|
104
|
+
.rec-alert[data-tone="warn"] { border-color:color-mix(in srgb,var(--warn) 55%,var(--border));background:color-mix(in srgb,var(--warn) 8%,var(--panel-2)); }
|
|
105
|
+
.rec-alert[data-tone="danger"] { border-color:color-mix(in srgb,var(--danger) 55%,var(--border));background:color-mix(in srgb,var(--danger) 8%,var(--panel-2)); }
|
|
106
|
+
.rec-alert[data-tone="info"] { border-color:color-mix(in srgb,var(--info) 55%,var(--border));background:color-mix(in srgb,var(--info) 8%,var(--panel-2)); }
|
|
107
|
+
.rec-alert-title { font-weight:700;font-size:.94rem;color:var(--text);margin:0; }
|
|
108
|
+
.rec-path-list { display:flex;flex-wrap:wrap;gap:6px;margin:0;padding:0;list-style:none; }
|
|
109
|
+
.rec-path-pill { font-family:var(--mono);font-size:.8rem;padding:4px 9px;border-radius:6px;background:var(--code-bg);border:1px solid var(--border-strong);color:var(--text);overflow-wrap:anywhere; }
|
|
110
|
+
.rec-settings-grid { display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:var(--s4); }
|
|
111
|
+
.rec-group { border:1px solid var(--border);border-radius:var(--radius);padding:16px 18px;background:var(--panel-2);display:flex;flex-direction:column;gap:12px;min-width:0; }
|
|
112
|
+
.rec-group-head { display:flex;flex-direction:column;gap:3px;padding-bottom:8px;border-bottom:1px solid var(--border); }
|
|
113
|
+
.rec-group-title { font-size:.96rem;font-weight:700;color:var(--text);margin:0;display:flex;align-items:center;gap:8px; }
|
|
114
|
+
.rec-group-step { display:inline-grid;place-items:center;width:22px;height:22px;border-radius:6px;background:var(--accent-soft);color:var(--accent-strong);font-size:.78rem;font-weight:800;flex:none; }
|
|
115
|
+
.rec-group-desc { font-size:.82rem;color:var(--muted);margin:0; }
|
|
116
|
+
.rec-two-col { display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px; }
|
|
117
|
+
.rec-three-col { display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px; }
|
|
118
|
+
.rec-chip-bar { display:flex;flex-wrap:wrap;gap:6px;align-items:center; }
|
|
119
|
+
.rec-chip-btn { font:inherit;font-family:var(--mono);font-size:.76rem;padding:4px 9px;border-radius:999px;border:1px solid var(--border-strong);background:var(--panel);color:var(--muted);cursor:pointer;transition:border-color .15s ease,color .15s ease,background-color .15s ease; }
|
|
120
|
+
.rec-chip-btn:hover { border-color:var(--accent);color:var(--text);background:var(--raised); }
|
|
121
|
+
.rec-action-bar { display:flex;flex-wrap:wrap;gap:10px;align-items:center;padding-top:var(--s2); }
|
|
122
|
+
.rec-git-columns { display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:var(--s4); }
|
|
123
|
+
.rec-git-list { display:flex;flex-direction:column;gap:8px;max-height:360px;overflow-y:auto;padding-right:2px; }
|
|
124
|
+
.rec-item-card,.wb-card { border:1px solid var(--border);border-radius:var(--radius-sm);padding:12px 14px;background:var(--panel-2);display:flex;flex-direction:column;gap:8px;min-width:0; }
|
|
125
|
+
.rec-item-head { display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap; }
|
|
126
|
+
.rec-item-actions { display:flex;flex-wrap:wrap;gap:8px;align-items:center; }
|
|
127
|
+
.rec-diff-box { border:1px solid var(--border-strong);border-radius:var(--radius-sm);padding:14px;background:var(--panel-2);display:flex;flex-direction:column;gap:10px; }
|
|
128
|
+
.rec-diff-box pre { max-height:320px;overflow:auto;margin:0; }
|
|
129
|
+
.rec-setting-group { border:1px solid var(--border);border-radius:var(--radius);padding:16px 18px;background:var(--panel-2);display:flex;flex-direction:column;gap:12px;min-width:0; }
|
|
130
|
+
.rec-setting-group > h4 { margin:0;font-size:.95rem;font-weight:700;color:var(--text);padding-bottom:6px;border-bottom:1px solid var(--border); }
|
|
131
|
+
.rec-chip-row { display:flex;flex-wrap:wrap;gap:6px;align-items:center; }
|
|
132
|
+
.wb-subtabs { display:flex;flex-wrap:wrap;gap:8px;padding:10px 12px;margin:8px 0 4px;border:1px solid var(--border);border-radius:var(--radius);background:var(--panel-2); }
|
|
133
|
+
.wb-subtab-btn { font:inherit;font-size:.86rem;font-weight:600;padding:7px 14px;border-radius:999px;border:1px solid var(--border-strong);background:var(--panel);color:var(--muted);cursor:pointer;transition:all .15s ease; }
|
|
134
|
+
.wb-subtab-btn:hover { color:var(--text);border-color:var(--accent); }
|
|
135
|
+
.wb-subtab-btn.active,.wb-subtab-btn[aria-selected="true"] { background:var(--accent);color:var(--accent-fg,#fff);border-color:var(--accent); }
|
|
136
|
+
|
|
137
|
+
@media(max-width:860px) { .rec-settings-grid,.rec-git-columns { grid-template-columns:1fr; } }
|
|
138
|
+
@media(max-width:640px) { .wb-form,.rec-two-col,.rec-three-col { grid-template-columns:1fr; } .wb-section { padding:12px; } .workbench-nav { padding:10px 12px; } .wb-toolbar { align-items:stretch; } .wb-tool-controls { grid-template-columns:1fr; } .wb-tool-row { grid-template-columns:1fr; } .wb-tool-row-state { max-width:none;text-align:left;padding-left:52px; } .wb-tool-group > summary { align-items:flex-start; } }
|