docs: devlog #003 추가 — workbench.html 크로스 복원 CSS 수정 (직전 세션 기록 복구)
This commit is contained in:
@@ -4,3 +4,4 @@
|
|||||||
|---|------|----------|------|------|
|
|---|------|----------|------|------|
|
||||||
| 001 | 00:34~00:47 | 429 Rate Limit 무한 루프 디버깅 — 지수 백오프 + rate limit 완화 + Collector 폴링 보호 | `d9b36cf` | ✅ |
|
| 001 | 00:34~00:47 | 429 Rate Limit 무한 루프 디버깅 — 지수 백오프 + rate limit 완화 + Collector 폴링 보호 | `d9b36cf` | ✅ |
|
||||||
| 002 | 16:45~17:04 | workbench.html 0-byte 파괴 복구 — 멀티 인스턴스 race condition 방지 안전 가드 추가 | `a9feee6` | ✅ |
|
| 002 | 16:45~17:04 | workbench.html 0-byte 파괴 복구 — 멀티 인스턴스 race condition 방지 안전 가드 추가 | `a9feee6` | ✅ |
|
||||||
|
| 003 | 17:10~17:55 | workbench.html 크로스 복원 CSS 깨짐 수정 — pre-patch backup + requiredMarker 구조 검증 + .orig 자동 복원 | `6d8c6f1` | ✅ |
|
||||||
|
|||||||
@@ -450,27 +450,76 @@ async function setupApprovalObserver() {
|
|||||||
// workbench.html — loaded by DevTools/standard mode
|
// workbench.html — loaded by DevTools/standard mode
|
||||||
// workbench-jetski-agent.html — loaded by AG agent mode
|
// workbench-jetski-agent.html — loaded by AG agent mode
|
||||||
const scriptDir = path.dirname(scriptPath);
|
const scriptDir = path.dirname(scriptPath);
|
||||||
const htmlFiles = ['workbench.html', 'workbench-jetski-agent.html'];
|
// Each HTML file has DIFFERENT CSS/JS entry points — they are NOT interchangeable:
|
||||||
for (const htmlFileName of htmlFiles) {
|
// workbench.html → workbench.desktop.main.css + workbench.js
|
||||||
const htmlPath = path.join(scriptDir, htmlFileName);
|
// workbench-jetski-agent.html → tw-base.tailwind.css + jetskiMain.tailwind.css + jetskiAgent.js
|
||||||
|
// Cross-restoring between them causes CSS to not load → layout broken (elements visible but all shifted left).
|
||||||
|
const htmlFileSpecs = [
|
||||||
|
{
|
||||||
|
name: 'workbench.html',
|
||||||
|
requiredMarker: 'workbench.desktop.main.css', // CSS unique to this file
|
||||||
|
requiredScript: 'workbench.js', // JS entry point
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'workbench-jetski-agent.html',
|
||||||
|
requiredMarker: 'jetskiMain.tailwind.css', // CSS unique to this file
|
||||||
|
requiredScript: 'jetskiAgent.js', // JS entry point
|
||||||
|
},
|
||||||
|
];
|
||||||
|
for (const spec of htmlFileSpecs) {
|
||||||
|
const htmlPath = path.join(scriptDir, spec.name);
|
||||||
|
const backupPath = htmlPath + '.orig';
|
||||||
try {
|
try {
|
||||||
if (!fs.existsSync(htmlPath)) {
|
if (!fs.existsSync(htmlPath)) {
|
||||||
logToFile(`[OBSERVER] ${htmlFileName} not found — skipping`);
|
logToFile(`[OBSERVER] ${spec.name} not found — skipping`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let html = fs.readFileSync(htmlPath, 'utf8');
|
let html = fs.readFileSync(htmlPath, 'utf8');
|
||||||
// SAFETY: Refuse to patch if file is empty or suspiciously small
|
// ── BACKUP: Save original before first-ever patch ──
|
||||||
// (race condition: another extension instance may be mid-write)
|
// Only backup if the file looks valid AND hasn't been backed up yet.
|
||||||
if (html.length < 500 || !html.includes('<!DOCTYPE html>')) {
|
if (!fs.existsSync(backupPath)
|
||||||
logToFile(`[OBSERVER] ${htmlFileName} appears corrupt or empty (${html.length} bytes) — SKIPPING to prevent further damage`);
|
&& html.length >= 500
|
||||||
|
&& html.includes('<!DOCTYPE html>')
|
||||||
|
&& html.includes(spec.requiredMarker)) {
|
||||||
|
fs.writeFileSync(backupPath, html, 'utf8');
|
||||||
|
logToFile(`[OBSERVER] ${spec.name} backed up to .orig (${html.length} bytes)`);
|
||||||
|
}
|
||||||
|
// ── SAFETY: Refuse to patch if file is corrupt, empty, or wrong type ──
|
||||||
|
// Race condition: another extension instance may be mid-write (0-byte).
|
||||||
|
// Wrong type: restored from the other HTML file (different CSS/JS refs).
|
||||||
|
const isCorrupt = html.length < 500 || !html.includes('<!DOCTYPE html>');
|
||||||
|
const isWrongType = !isCorrupt && !html.includes(spec.requiredMarker);
|
||||||
|
if (isCorrupt || isWrongType) {
|
||||||
|
const reason = isCorrupt
|
||||||
|
? `corrupt/empty (${html.length} bytes)`
|
||||||
|
: `wrong type (missing ${spec.requiredMarker})`;
|
||||||
|
logToFile(`[OBSERVER] ${spec.name} detected ${reason}`);
|
||||||
|
// Try to restore from backup
|
||||||
|
if (fs.existsSync(backupPath)) {
|
||||||
|
const backup = fs.readFileSync(backupPath, 'utf8');
|
||||||
|
if (backup.length >= 500
|
||||||
|
&& backup.includes('<!DOCTYPE html>')
|
||||||
|
&& backup.includes(spec.requiredMarker)) {
|
||||||
|
fs.writeFileSync(htmlPath, backup, 'utf8');
|
||||||
|
html = backup;
|
||||||
|
logToFile(`[OBSERVER] ${spec.name} RESTORED from .orig backup (${backup.length} bytes) ✅`);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
logToFile(`[OBSERVER] ${spec.name} .orig backup also invalid — SKIPPING`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
logToFile(`[OBSERVER] ${spec.name} no .orig backup available — SKIPPING to prevent further damage`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
// CRITICAL: Patch CSP to allow inline scripts.
|
// CRITICAL: Patch CSP to allow inline scripts.
|
||||||
// Default CSP has script-src 'self' 'unsafe-eval' blob: — NO 'unsafe-inline'.
|
// Default CSP has script-src 'self' 'unsafe-eval' blob: — NO 'unsafe-inline'.
|
||||||
// Without 'unsafe-inline', all inline <script> tags are silently blocked.
|
// Without 'unsafe-inline', all inline <script> tags are silently blocked.
|
||||||
if (html.includes('script-src') && !html.match(/script-src[^;]*'unsafe-inline'/)) {
|
if (html.includes('script-src') && !html.match(/script-src[^;]*'unsafe-inline'/)) {
|
||||||
html = html.replace(/(script-src\s[^;]*?)('self')/, "$1$2\n\t\t\t\t\t'unsafe-inline'");
|
html = html.replace(/(script-src\s[^;]*?)('self')/, "$1$2\n\t\t\t\t\t'unsafe-inline'");
|
||||||
logToFile(`[OBSERVER] ${htmlFileName} CSP patched: added 'unsafe-inline' to script-src`);
|
logToFile(`[OBSERVER] ${spec.name} CSP patched: added 'unsafe-inline' to script-src`);
|
||||||
}
|
}
|
||||||
// Remove old external script tag if present (legacy, cannot be served)
|
// Remove old external script tag if present (legacy, cannot be served)
|
||||||
const extMarkerStart = '<!-- AG SDK [variet-gravity-bridge] -->';
|
const extMarkerStart = '<!-- AG SDK [variet-gravity-bridge] -->';
|
||||||
@@ -480,7 +529,7 @@ async function setupApprovalObserver() {
|
|||||||
'[\\s\\S]*?' +
|
'[\\s\\S]*?' +
|
||||||
extMarkerEnd.replace(/[[\]]/g, '\\$&') + '\\n?');
|
extMarkerEnd.replace(/[[\]]/g, '\\$&') + '\\n?');
|
||||||
html = html.replace(extRe, '');
|
html = html.replace(extRe, '');
|
||||||
logToFile(`[OBSERVER] removed external script tag from ${htmlFileName}`);
|
logToFile(`[OBSERVER] removed external script tag from ${spec.name}`);
|
||||||
}
|
}
|
||||||
// Insert or update inline script
|
// Insert or update inline script
|
||||||
const inlineMarkerStart = '<!-- AG SDK INLINE [variet-gravity-bridge] -->';
|
const inlineMarkerStart = '<!-- AG SDK INLINE [variet-gravity-bridge] -->';
|
||||||
@@ -490,21 +539,21 @@ async function setupApprovalObserver() {
|
|||||||
'[\\s\\S]*?' +
|
'[\\s\\S]*?' +
|
||||||
inlineMarkerEnd.replace(/[[\]]/g, '\\$&'));
|
inlineMarkerEnd.replace(/[[\]]/g, '\\$&'));
|
||||||
html = html.replace(re, `${inlineMarkerStart}\n<script>\n${combinedScript}\n</script>\n${inlineMarkerEnd}`);
|
html = html.replace(re, `${inlineMarkerStart}\n<script>\n${combinedScript}\n</script>\n${inlineMarkerEnd}`);
|
||||||
logToFile(`[OBSERVER] ${htmlFileName} inline script UPDATED`);
|
logToFile(`[OBSERVER] ${spec.name} inline script UPDATED`);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
html = html.replace('</html>', `\n${inlineMarkerStart}\n<script>\n${combinedScript}\n</script>\n${inlineMarkerEnd}\n</html>`);
|
html = html.replace('</html>', `\n${inlineMarkerStart}\n<script>\n${combinedScript}\n</script>\n${inlineMarkerEnd}\n</html>`);
|
||||||
logToFile(`[OBSERVER] ${htmlFileName} inline script INSERTED`);
|
logToFile(`[OBSERVER] ${spec.name} inline script INSERTED`);
|
||||||
}
|
}
|
||||||
// SAFETY: Final validation before write — never write empty or invalid HTML
|
// SAFETY: Final validation before write
|
||||||
if (html.length < 500 || !html.includes('<!DOCTYPE html>')) {
|
if (html.length < 500 || !html.includes('<!DOCTYPE html>') || !html.includes(spec.requiredMarker)) {
|
||||||
logToFile(`[OBSERVER] ${htmlFileName} WOULD BE CORRUPT after patching (${html.length} bytes) — ABORTING write`);
|
logToFile(`[OBSERVER] ${spec.name} WOULD BE CORRUPT after patching (${html.length} bytes) — ABORTING write`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
fs.writeFileSync(htmlPath, html, 'utf8');
|
fs.writeFileSync(htmlPath, html, 'utf8');
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
logToFile(`[OBSERVER] ${htmlFileName} patch error: ${e.message}`);
|
logToFile(`[OBSERVER] ${spec.name} patch error: ${e.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user