feat(reader): Gitea client + config + board orchestrator

closes board-viewer-gitea-reader

- src/gitea.ts: HTTP client (getFile, getLatestCommitIso, rawUrl); DI'd fetch
- src/reader.ts: readBoard(client, repos) → TaskRecord[]
- src/config.ts: TOML config loader with board_viewer_repos whitelist
- 25 tests total (9 parser incl. 3 real-repo fixtures, 7 gitea, 5 reader, 4 config)
- emoji contract 🔴🟡🔵🟢 (open/in_progress/paused/blocked/done)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
vitya
2026-05-22 12:48:37 +03:00
parent 1c7e372abf
commit d093693795
14 changed files with 2543 additions and 15 deletions

56
src/reader.ts Normal file
View File

@@ -0,0 +1,56 @@
import type { GiteaClient } from './gitea.ts';
import { parseStatus, type Status, type StatusEmoji } from './parser.ts';
export interface RepoRef {
owner: string;
repo: string;
}
export interface TaskRecord {
slug: string;
project: string;
project_owner: string;
status: Status;
status_emoji: StatusEmoji;
title: string;
where_stopped: string | null;
next_action: string | null;
blocker: string | null;
branch: string | null;
last_commit_iso: string | null;
raw_url: string;
}
export async function readBoard(
client: GiteaClient,
repos: ReadonlyArray<RepoRef>,
): Promise<TaskRecord[]> {
const records: TaskRecord[] = [];
for (const { owner, repo } of repos) {
const status = await client.getFile(owner, repo, '.tasks/STATUS.md');
if (status === null) continue;
const blocks = parseStatus(status);
for (const block of blocks) {
const taskPath = `.tasks/${block.slug}.md`;
const last_commit_iso = await client.getLatestCommitIso(owner, repo, taskPath);
records.push({
slug: block.slug,
project: repo,
project_owner: owner,
status: block.status,
status_emoji: block.status_emoji,
title: block.title,
where_stopped: block.where_stopped,
next_action: block.next_action,
blocker: block.blocker,
branch: block.branch,
last_commit_iso,
raw_url: client.rawUrl(owner, repo, taskPath),
});
}
}
return records;
}