feat(cockpit): quick wins — sync chip, cost budget guard, digest, recent commits (W3)
Four small, high-delight features that turn passive capture into glanceable signals.
- Sync chip on project cards: ahead/behind vs upstream + last-commit time, folded
into the existing concurrent per-repo metadata pass (git rev-list --count
--left-right @{u}...HEAD + git log -1 --format=%ct; no upstream → undefined, no route).
- Cost budget guard: COST_BUDGET_USD env (0 = off); a per-session one-shot latch
(Session.budgetNotified, cost is monotonic so never re-armed) fires a single push
on threshold crossing in manager.handleStatusLine; the already-broadcast telemetry
frame carries the warn (tg-cost-warn styling derived from costUsd>=budget via
/config/ui — no new ServerMessage). web-push title added to sw-push.js.
- "While you were away" digest: GET /digest?since= → {finished, needsInput, stuck,
totalCostUsd, sessions[]} aggregate over manager.list(); FE banner on reconnect.
- Recent commits per project: src/http/git-log.ts (NUL-delimited git log → CommitInfo[]),
GET /projects/log?path= (isValidGitDir), textContent-inert render in project detail.
All git via execFile (no shell) + validated cwd; new routes read-only; commit
messages rendered via textContent. Verified: typecheck + build:web clean, 1904 pass
at --test-timeout=30000 (two default-5s failures are slow-sandbox real-subprocess
timeout flakes — the known ring-buffer test + a new real-git-clone sync test — not
logic regressions).
This commit is contained in:
@@ -40,6 +40,8 @@ import { listSessions } from './http/history.js'
|
||||
import { buildProjects, buildProjectDetail } from './http/projects.js'
|
||||
import { openInEditor, openFileInEditor } from './http/editor.js'
|
||||
import { getDiff, isPlausibleRev } from './http/diff.js'
|
||||
import { getGitLog } from './http/git-log.js'
|
||||
import { buildDigest, clampSince } from './http/digest.js'
|
||||
import { getPrStatus } from './http/gh.js'
|
||||
import { parseStatusLine } from './http/statusline.js'
|
||||
import { createWorktree } from './http/worktrees.js'
|
||||
@@ -326,6 +328,15 @@ export function startServer(cfg: Config): { close(): Promise<void> } {
|
||||
res.json(manager.list())
|
||||
})
|
||||
|
||||
// ── W3 quick-wins (c): "while you were away" reconnect digest (read-only) ──
|
||||
// A pure read-side aggregate over manager.list() + in-memory telemetry/status;
|
||||
// no Origin guard (same threat model as /live-sessions). `?since=<epochMs>` is
|
||||
// the client's last-seen watermark (bad/absent → 0 = "everything is new").
|
||||
app.get('/digest', (req, res) => {
|
||||
const since = clampSince(req.query['since'])
|
||||
res.json(buildDigest(manager.list(), since))
|
||||
})
|
||||
|
||||
// Projects (v0.6 Project Manager) — discovery-only; no Origin guard (read-only, like /live-sessions).
|
||||
app.get('/projects', async (_req, res) => {
|
||||
try {
|
||||
@@ -820,6 +831,30 @@ export function startServer(cfg: Config): { close(): Promise<void> } {
|
||||
}
|
||||
})
|
||||
|
||||
// ── W3 quick-wins (d): read-only recent-commit log (no Origin guard) ──────
|
||||
// Same three-prong path validation (isValidGitDir, SEC-H7) as /projects/diff.
|
||||
// `?n=<int>` is clamped to [1, GIT_LOG_MAX] inside getGitLog; `path` missing →
|
||||
// 400, non-git dir → 404, git failure → best-effort empty (getGitLog) → 200.
|
||||
app.get('/projects/log', async (req, res) => {
|
||||
const target = req.query['path']
|
||||
if (typeof target !== 'string' || target === '') {
|
||||
res.status(400).json({ error: 'path query parameter is required' })
|
||||
return
|
||||
}
|
||||
if (!(await isValidGitDir(target))) {
|
||||
res.status(404).json({ error: 'project not found' }) // SEC-H7 three-prong
|
||||
return
|
||||
}
|
||||
const rawN = req.query['n']
|
||||
const n = typeof rawN === 'string' ? Number.parseInt(rawN, 10) : undefined
|
||||
try {
|
||||
res.json(await getGitLog(target, { n, timeoutMs: cfg.diffTimeoutMs }))
|
||||
} catch (err) {
|
||||
console.error('[server] /projects/log failed:', err instanceof Error ? err.message : String(err))
|
||||
res.status(500).json({ error: 'failed to read git log' })
|
||||
}
|
||||
})
|
||||
|
||||
// ── W3 read-only PR + CI status (no Origin guard; same threat model as /projects) ─
|
||||
// Out-of-band side-channel: spawns the host's `gh` CLI to read the current
|
||||
// branch's PR + statusCheckRollup. Unlike the local git side-channels, gh makes
|
||||
@@ -895,7 +930,12 @@ export function startServer(cfg: Config): { close(): Promise<void> } {
|
||||
|
||||
// ── GET /config/ui (review #4) — client-readable UI config (read-only) ────
|
||||
app.get('/config/ui', (_req, res) => {
|
||||
const uiConfig: UiConfig = { allowAutoMode: cfg.allowAutoMode }
|
||||
const uiConfig: UiConfig = {
|
||||
allowAutoMode: cfg.allowAutoMode,
|
||||
// W3(b): expose the cost budget only when set (>0) so the FE can derive
|
||||
// cost-overage warn styling; a non-secret number, safe over /config/ui.
|
||||
...(cfg.costBudgetUsd > 0 ? { costBudgetUsd: cfg.costBudgetUsd } : {}),
|
||||
}
|
||||
res.json(uiConfig)
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user