Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
74 lines
3.0 KiB
JavaScript
74 lines
3.0 KiB
JavaScript
'use strict';
|
|
// 사이트 메타데이터/명령 이력/토큰 사용량을 data/sites/<id>.json 에 저장하는 단순 JSON 저장소.
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const DATA_DIR = process.env.DATA_DIR || path.join(__dirname, '..', 'data', 'sites');
|
|
fs.mkdirSync(DATA_DIR, { recursive: true });
|
|
|
|
const ID_RE = /^[a-f0-9]{32}$/;
|
|
|
|
function fileOf(id) { return path.join(DATA_DIR, `${id}.json`); }
|
|
|
|
function emptyTotals() {
|
|
return { input: 0, output: 0, cacheCreate: 0, cacheRead: 0, thinking: 0, total: 0, costUsd: 0, chargeKrw: 0, commands: 0 };
|
|
}
|
|
|
|
function recomputeTotals(site) {
|
|
const t = emptyTotals();
|
|
for (const c of site.commands) {
|
|
if (!c.usage) continue;
|
|
t.input += c.usage.input; t.output += c.usage.output;
|
|
t.cacheCreate += c.usage.cacheCreate; t.cacheRead += c.usage.cacheRead;
|
|
t.thinking += c.usage.thinking || 0; t.total += c.usage.total;
|
|
t.costUsd += c.costUsd || 0; t.chargeKrw += c.chargeKrw || 0; t.commands += 1;
|
|
}
|
|
site.totals = t;
|
|
return site;
|
|
}
|
|
|
|
// v1.6.31 (요구 10): 원자적 쓰기(임시 파일 + fsync + rename) + 직전 정상본 .bak 보존. 읽기 실패는 손상으로 기록해 관리자 운영 복구 화면에 보인다(빈 값으로 숨기지 않음)
|
|
const js = require('./jsonstore');
|
|
function save(site) {
|
|
recomputeTotals(site);
|
|
site.updatedAt = new Date().toISOString();
|
|
js.writeJsonAtomic(fileOf(site.id), JSON.stringify(site, null, 2), { mode: 0o600, keepBackup: true });
|
|
return site;
|
|
}
|
|
/** 사이트 기록 읽기. 없으면 null. 깨졌으면 .bak 으로 복구를 시도하고 손상 목록에 남긴다(복구도 안 되면 null — 화면에는 "손상" 으로 보인다) */
|
|
function load(id) {
|
|
if (!ID_RE.test(id)) return null;
|
|
return js.readJson(fileOf(id), null, {});
|
|
}
|
|
/** 손상된 사이트 파일 목록 (운영 복구 화면) */
|
|
function health() { return js.health().filter((h) => h.file.startsWith(DATA_DIR)); }
|
|
|
|
function list(ownerId) {
|
|
return fs.readdirSync(DATA_DIR)
|
|
.filter((f) => f.endsWith('.json'))
|
|
.map((f) => load(f.slice(0, -5)))
|
|
.filter(Boolean)
|
|
.filter((s) => !ownerId || s.ownerId === ownerId)
|
|
.sort((a, b) => (b.createdAt || '').localeCompare(a.createdAt || ''))
|
|
// v1.2.0: 상단 사이트 선택 상자가 "생성 중 / 완성 / 가져옴·복사" 로 묶어 보여 주도록 mode·plan 단계도 함께 준다
|
|
.map((s) => ({ id: s.id, title: s.title, createdAt: s.createdAt, status: s.status, totals: s.totals, commands: s.commands.length, mode: s.mode || null, stage: (s.plan && s.plan.stage) || null }));
|
|
}
|
|
|
|
function create(fields) {
|
|
const site = {
|
|
id: fields.id,
|
|
createdAt: new Date().toISOString(),
|
|
ownerId: fields.ownerId || null, ownerName: fields.ownerName || '',
|
|
title: fields.title, purpose: fields.purpose, content: fields.content,
|
|
design: fields.design, extra: fields.extra,
|
|
git: { seq: 0, changes: [] },
|
|
sessionId: fields.sessionId,
|
|
status: 'idle',
|
|
commands: [],
|
|
totals: emptyTotals(),
|
|
};
|
|
return save(site);
|
|
}
|
|
|
|
module.exports = { health, ID_RE, create, save, load, list, DATA_DIR };
|