Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
38b67fc
⚡ 병렬 외부 네트워크 호출을 위한 Promise.all 적용
seonghobae Aug 4, 2026
5a9dfaa
⚡ 병렬 외부 네트워크 호출을 위한 Promise.all 적용 및 CVE 패치
seonghobae Aug 4, 2026
a3c76d5
fix(review): bound job-status refresh concurrency to chunks of 5
seonghobae Aug 4, 2026
99ff6dd
ci: run one-shot attachment refresh hardening
seonghobae Aug 4, 2026
f5ecb72
ci: remove inactive one-shot repair workflow
seonghobae Aug 4, 2026
63a8f18
ci: validate and apply attachment refresh hardening
seonghobae Aug 4, 2026
bcdb27f
perf(attachments): add bounded status refresh engine
seonghobae Aug 4, 2026
d705f23
test(attachments): cover bounded refresh engine
seonghobae Aug 4, 2026
f8077e0
test(clearfolio): cover status cancellation signal
seonghobae Aug 4, 2026
f455f99
test(api): cover attachment status refresh contract
seonghobae Aug 4, 2026
0d6b439
feat(clearfolio): make status lookups abortable
seonghobae Aug 4, 2026
ff15fca
test(attachments): wire refresh coverage and API regressions
seonghobae Aug 4, 2026
0cc3809
docs(changelog): record bounded attachment refresh
seonghobae Aug 4, 2026
ee4930b
ci: make attachment repair marker-based
seonghobae Aug 4, 2026
62da5d0
Restore scorecard-analysis.yml for branch protection rule
seonghobae Aug 4, 2026
8ebd8cd
docs(attachments): document refresh worker
seonghobae Aug 4, 2026
9f37f79
ci: make attachment repair atomic and marker-based
seonghobae Aug 4, 2026
8fdffb1
ci: patch attachment repair browser setup
seonghobae Aug 4, 2026
a0e53d3
test(e2e): install Chromium for cloud validation
seonghobae Aug 4, 2026
0caf4eb
ci: remove browser bootstrap workflow
seonghobae Aug 4, 2026
0e3cd25
perf(attachments): bound status refresh and remove N+1 queries
github-actions[bot] Aug 4, 2026
4f6565f
chore: remove unrelated scorecard workflow
seonghobae Aug 4, 2026
dddfd60
chore: keep attachment change focused
seonghobae Aug 4, 2026
bb45a51
fix(attachments): enforce request-wide refresh budget
seonghobae Aug 4, 2026
f46c36a
fix(clearfolio): preserve status on HTTP failures
seonghobae Aug 4, 2026
d0a1b44
test(attachments): prove deadline and failure categories
seonghobae Aug 4, 2026
4f1dfe1
test(clearfolio): enforce status endpoint HTTP contract
seonghobae Aug 4, 2026
609c78c
test(attachments): use behavior-only list assertions
seonghobae Aug 4, 2026
73603cd
test(coverage): include Clearfolio status paths
seonghobae Aug 4, 2026
e883548
ci: repair exact-head coverage evidence for PR 419
seonghobae Aug 4, 2026
e34b138
ci: stage PR 419 review repair script
seonghobae Aug 4, 2026
230bafa
ci: arm ready-event repair for PR 419
seonghobae Aug 4, 2026
afeb3c0
fix(ci): make test coverage produce Istanbul evidence
seonghobae Aug 4, 2026
8da3b0e
test(ci): lock coverage-producing script contract
seonghobae Aug 4, 2026
64b7ed5
fix(ci): make coverage evidence deterministic
github-actions[bot] Aug 4, 2026
140bf95
test(ci): document coverage evidence contract
seonghobae Aug 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed


- Attachment-list status refresh now removes the per-row database lookup,
uses a configurable bounded worker pool with abortable downstream timeouts,
preserves stale status after isolated failures, excludes internal conversion
identifiers from responses, and reports attempted, changed, failed, and
deferred refresh counters.
- 프로젝트 이름 입력 필드에 입력 예시(placeholder)를 추가하여 사용자 편의성을 개선했습니다.
- 데이터 테이블의 반복되는 액션 버튼에 컨텍스트 정보(작업명)를 포함한 명시적인 ARIA 레이블을 추가하고, 유효성 검사 에러를 폼 필드에 연결하여 접근성을 개선했습니다.
- `createGanttBarElement`, `renderGantt`, `buildWeekdayTimeline`에서 반복적으로 호출되던 `compareDateStrings`를 직접적인 문자열 비교 연산(`>=`, `<=`)으로 교체하여 O(N*D) 복잡도의 캐시 스레싱과 정규식 검사를 방지했습니다.
Expand Down
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 7 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,21 @@
},
"scripts": {
"check:python-docstrings": "node scripts/ci/static_coverage_evidence.mjs docstrings",
"coverage": "c8 --all --include=app.js --include=cloud-sync.js --include=scripts/ci/static_coverage_evidence.mjs --include=server/app.mjs --include=server/auth.mjs --reporter=json --reporter=json-summary npm run test:coverage",
"coverage": "npm run test:coverage",
"server": "node server/server.mjs",
"test:api": "node tests/api/auth-secret.test.mjs && node tests/api/smoke.mjs && node tests/api/ratelimit.test.mjs",
"test:unit": "node tests/unit/analytics.test.mjs && node tests/unit/cpm.test.mjs && node tests/unit/baseline-compare.test.mjs && node tests/unit/workload.test.mjs && node tests/unit/cost-evm.test.mjs && node tests/unit/msproject.test.mjs && node tests/unit/auth-password.test.mjs && node tests/unit/editor-unsaved.test.mjs && node tests/unit/static-coverage-evidence.test.mjs && node tests/unit/dep-types.test.mjs && node tests/unit/weekly-report.test.mjs && node tests/unit/clearfolio.test.mjs && node tests/unit/sprint-stats.test.mjs && node tests/unit/burndown.test.mjs && node tests/unit/pm-analysis.test.mjs && node tests/unit/cloud-sync-security.test.mjs",
"test:coverage": "node tests/unit/msproject.test.mjs && node tests/unit/auth-password.test.mjs && node tests/unit/editor-unsaved.test.mjs && node tests/unit/static-coverage-evidence.test.mjs && npm run test:api",
"test:api": "node tests/api/auth-secret.test.mjs && node tests/api/smoke.mjs && node tests/api/ratelimit.test.mjs && node tests/api/attachment-status.test.mjs",
"test:unit": "node tests/unit/analytics.test.mjs && node tests/unit/cpm.test.mjs && node tests/unit/baseline-compare.test.mjs && node tests/unit/workload.test.mjs && node tests/unit/cost-evm.test.mjs && node tests/unit/msproject.test.mjs && node tests/unit/auth-password.test.mjs && node tests/unit/editor-unsaved.test.mjs && node tests/unit/static-coverage-evidence.test.mjs && node tests/unit/dep-types.test.mjs && node tests/unit/weekly-report.test.mjs && node tests/unit/clearfolio.test.mjs && node tests/unit/sprint-stats.test.mjs && node tests/unit/burndown.test.mjs && node tests/unit/pm-analysis.test.mjs && node tests/unit/cloud-sync-security.test.mjs && node tests/unit/attachment-status.test.mjs && node tests/unit/clearfolio-status-signal.test.mjs && node tests/unit/coverage-script-contract.test.mjs",
"test:coverage": "c8 --all --include=app.js --include=cloud-sync.js --include=scripts/ci/static_coverage_evidence.mjs --include=server/attachment_status.mjs --include=server/app.mjs --include=server/auth.mjs --include=server/clearfolio.mjs --reporter=json --reporter=json-summary npm run test:coverage:cases",
"test:coverage:cases": "node tests/unit/coverage-script-contract.test.mjs && node tests/unit/attachment-status.test.mjs && node tests/unit/clearfolio-status-signal.test.mjs && node tests/unit/msproject.test.mjs && node tests/unit/auth-password.test.mjs && node tests/unit/editor-unsaved.test.mjs && node tests/unit/static-coverage-evidence.test.mjs && npm run test:api",
"test:e2e": "playwright test",
"test:e2e:headed": "playwright test --headed",
"test:e2e:cloud": "playwright test tests/e2e/cloud.spec.js",
"test:e2e:cloud": "playwright install chromium && playwright test tests/e2e/cloud.spec.js",
"test:fuzz": "playwright install chromium && playwright test tests/e2e/csv_formula_fuzz.spec.js",
"fuzz": "node --test tests/fuzz/*.mjs"
},
"dependencies": {
"@hono/node-server": "^2.0.12",
"hono": "^4.12.32"
"hono": "^4.13.0"
},
"devDependencies": {
"@playwright/test": "1.61.1",
Expand Down
79 changes: 57 additions & 22 deletions server/app.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { db, rowid } from './db.mjs';
import { hashPassword, verifyPassword, signToken, verifyToken, generateApiToken, hashApiToken } from './auth.mjs';
import { PLANS, planOf, orgUsage, wouldExceed, createCheckout } from './billing.mjs';
import { clearfolioMock, mockArtifact, submitJob, jobStatus, artifactUrl } from './clearfolio.mjs';
import { normalizeAttachmentStatusBudgetMs, normalizeAttachmentStatusConcurrency, normalizeAttachmentStatusTimeoutMs, refreshAttachmentStatuses } from './attachment_status.mjs';
import { chat as orchestratorChat } from './orchestrator.mjs';
import { computeEvm } from '../analytics.js'; // pure math, shared with the client

Expand Down Expand Up @@ -73,7 +74,20 @@ function projectAccess(userId, projectId) {
}

// --- observability: in-process counters + structured request log.
const metrics = { startedAt: new Date().toISOString(), requests: 0, s2xx: 0, s4xx: 0, s5xx: 0, signups: 0, projectsCreated: 0, webhookDeliveries: 0 };
const metrics = {
startedAt: new Date().toISOString(),
requests: 0,
s2xx: 0,
s4xx: 0,
s5xx: 0,
signups: 0,
projectsCreated: 0,
webhookDeliveries: 0,
attachmentStatusRefreshAttempted: 0,
attachmentStatusRefreshChanged: 0,
attachmentStatusRefreshFailed: 0,
attachmentStatusRefreshDeferred: 0,
};

// Outbound webhooks: POST signed JSON to each active hook subscribed to `event`.
// Fire-and-forget with a timeout, one retry on failure, and a recorded outcome
Expand Down Expand Up @@ -993,6 +1007,31 @@ app.post('/api/projects/:id/ai/brief', requireAuth, async (c) => {
// 갱신), 서명 아티팩트 열람(302), 삭제. 테넌트 = 조직, 브라우저에는 Clearfolio
// 자격이 절대 노출되지 않음.
const ATTACH_MAX_BYTES = 10 * 1024 * 1024;

const ATTACH_STATUS_CONCURRENCY = normalizeAttachmentStatusConcurrency(
process.env.SCOPEWEAVE_ATTACHMENT_STATUS_CONCURRENCY,
);
const ATTACH_STATUS_TIMEOUT_MS = normalizeAttachmentStatusTimeoutMs(
process.env.SCOPEWEAVE_ATTACHMENT_STATUS_TIMEOUT_MS,
);
const ATTACH_STATUS_BUDGET_MS = normalizeAttachmentStatusBudgetMs(
process.env.SCOPEWEAVE_ATTACHMENT_STATUS_BUDGET_MS,
);
const ATTACHMENT_LIST_COLUMNS = `a.id, a.task_id AS taskId, a.name, a.mime, a.size,
a.job_id AS jobId, a.status, a.created_at AS createdAt, u.email AS uploadedBy`;
const ATTACHMENT_LIST_FROM =
'FROM attachments a LEFT JOIN users u ON u.id = a.created_by';
const listAttachmentsStatement = db.prepare(
`SELECT ${ATTACHMENT_LIST_COLUMNS} ${ATTACHMENT_LIST_FROM}
WHERE a.project_id = ? ORDER BY a.id DESC`,
);
const listTaskAttachmentsStatement = db.prepare(
`SELECT ${ATTACHMENT_LIST_COLUMNS} ${ATTACHMENT_LIST_FROM}
WHERE a.project_id = ? AND a.task_id = ? ORDER BY a.id DESC`,
);
const updateAttachmentStatusStatement = db.prepare(
'UPDATE attachments SET status = ? WHERE id = ?',
);
app.post('/api/projects/:id/attachments', requireAuth, async (c) => {
const uid = c.get('user').sub;
const p = projectAccess(uid, c.req.param('id'));
Expand Down Expand Up @@ -1022,28 +1061,24 @@ app.get('/api/projects/:id/attachments', requireAuth, async (c) => {
const uid = c.get('user').sub;
const p = projectAccess(uid, c.req.param('id'));
if (!p) return c.json({ error: 'not found' }, 404);

const taskId = c.req.query('taskId');
const rows = (taskId
? db.prepare(`SELECT a.id, a.task_id AS taskId, a.name, a.mime, a.size, a.status, a.created_at AS createdAt, u.email AS uploadedBy
FROM attachments a LEFT JOIN users u ON u.id = a.created_by
WHERE a.project_id = ? AND a.task_id = ? ORDER BY a.id DESC`).all(p.id, taskId)
: db.prepare(`SELECT a.id, a.task_id AS taskId, a.name, a.mime, a.size, a.status, a.created_at AS createdAt, u.email AS uploadedBy
FROM attachments a LEFT JOIN users u ON u.id = a.created_by
WHERE a.project_id = ? ORDER BY a.id DESC`).all(p.id));
// PENDING 잡 상태 갱신(최선 노력)
for (const r of rows) {
if (r.status === 'PENDING' || r.status === 'RUNNING') {
try {
const jid = db.prepare('SELECT job_id FROM attachments WHERE id = ?').get(r.id).job_id;
const st = await jobStatus(p.org_id, uid, jid);
if (st !== r.status) {
db.prepare('UPDATE attachments SET status = ? WHERE id = ?').run(st, r.id);
r.status = st;
}
} catch { /* keep stale status */ }
}
}
return c.json({ attachments: rows });
const rows = taskId
? listTaskAttachmentsStatement.all(p.id, taskId)
: listAttachmentsStatement.all(p.id);
await refreshAttachmentStatuses(rows, {
orgId: p.org_id,
userId: uid,
jobStatus,
updateStatus: (status, attachmentId) =>
updateAttachmentStatusStatement.run(status, attachmentId),
concurrency: ATTACH_STATUS_CONCURRENCY,
timeoutMs: ATTACH_STATUS_TIMEOUT_MS,
budgetMs: ATTACH_STATUS_BUDGET_MS,
metrics,
});
const attachments = rows.map(({ jobId: _internalJobId, ...publicRow }) => publicRow);
return c.json({ attachments });
});

// 열람: 서명 아티팩트 URL로 302. 새 탭 열기용으로 ?token=도 허용(ics/stream 패턴).
Expand Down
Loading
Loading