chore: D22 落地——docs/SDD/wiki/CLAUDE.md 進 repo(Gitea private 預設全 push)

頂層 D22 決策(leo 2026-07-03 拍板):推什麼由開發環境歸屬決定,
Gitea private=除機敏值/build 產物/.github 外全 push。
解 T1.5 卡點:雲端工人 clone 拿得到 credential-store-migration.md,可就地改寫 SDD。
機敏掃描兩輪通過(新增 189 檔約 2.1MB,node_modules/dist/wasm 照舊排除)。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
uncle6me-web
2026-07-03 07:13:15 +08:00
parent c830150da1
commit 5d00e71275
190 changed files with 39486 additions and 14 deletions
+300
View File
@@ -0,0 +1,300 @@
'use client';
// Mira repo 總管 + 工作台
// SDD: polaris/mira/.agents/specs/mira-app/design.md §3.9.1.2 + §5.2 v2.1
// 對應 task: 階段 10-B
// GitHub = repo 全清單 SSOTclone 到 Hetzner = 激活進工作態
// 資料源:mira daemon GET /projects(轉發 AI-Meka GitHub 掃描器)
import { useEffect, useMemo, useState } from 'react';
import '../mira.css';
// mira daemonnginx mira.uncle6.me/mira/ → 容器)
const DAEMON = process.env.NEXT_PUBLIC_MIRA_DAEMON ?? 'https://mira.uncle6.me/mira';
const API_BASE = process.env.NEXT_PUBLIC_API_BASE ?? 'https://cypher.arcrun.dev';
const KBDB_BASE = 'https://kbdb.finally.click'; // 既有技術債(同 feed/wiki),KI-3 未解前沿用
export type RepoSummary = {
name: string;
full_name: string;
cloned: boolean;
has_sdd: boolean;
archived: boolean;
fork: boolean;
total: number;
done: number;
in_progress: number;
ai_count: number;
leo_count: number;
blocked_count: number;
last_activity: string | null;
};
// project_detector 拆出的 todo blocktype=note, source=ai-project-detector
// G1:一篇河道筆記可拆多條 todo,各自掛 suggested-repodesign.md §3.7.5.7
type Todo = {
id: string;
content: string;
suggested_repo: string; // repo name 或 "new"
raw_id: string; // 溯源回河道原 raw(raw: tag),給「來自河道」連結
};
type Filter = 'sdd' | 'all';
// 從 tags_json 解析 detector 打的 tag
function parseTodoTags(tagsJson: string | null): { isTodo: boolean; repo: string; rawId: string } {
let tags: string[] = [];
try { tags = JSON.parse(tagsJson || '[]'); } catch { /* */ }
const isTodo = tags.includes('is_todo:true');
const repoTag = tags.find(t => t.startsWith('suggested-repo:'));
const repo = repoTag ? repoTag.slice('suggested-repo:'.length) : '';
const rawTag = tags.find(t => t.startsWith('raw:'));
const rawId = rawTag ? rawTag.slice('raw:'.length) : '';
return { isTodo, repo, rawId };
}
export default function ProjectsPage() {
const [repos, setRepos] = useState<RepoSummary[] | null>(null);
const [todos, setTodos] = useState<Todo[]>([]);
const [error, setError] = useState<string | null>(null);
const [filter, setFilter] = useState<Filter>('sdd');
const [cloning, setCloning] = useState<string | null>(null);
const [newRepo, setNewRepo] = useState('');
const load = () => {
setError(null);
fetch(`${DAEMON}/projects`)
.then(async r => {
if (!r.ok) throw new Error(`daemon 回 ${r.status}`);
return r.json() as Promise<{ repos: RepoSummary[] }>;
})
.then(d => setRepos(d.repos ?? []))
.catch(e => setError(e instanceof Error ? e.message : String(e)));
void loadTodos();
};
// 撈 project_detector 拆出的 todo blocksource=ai-project-detectorG1 一篇拆多條)
// client 過濾 is_todo:trueKI-3 tag query bug 故用 source filter 而非 tag query
const loadTodos = async () => {
try {
const me = await fetch(`${API_BASE}/me`, { credentials: 'include' }).then(r => r.ok ? r.json() : null);
if (!me?.api_key) return;
const res = await fetch(`${KBDB_BASE}/blocks?type=note&source=ai-project-detector&limit=300`, {
headers: { Authorization: `Bearer ${me.api_key}` },
});
if (!res.ok) return;
const data = await res.json() as { blocks?: Array<{ id: string; content: string; tags_json: string | null }> };
const list: Todo[] = [];
for (const b of data.blocks ?? []) {
const { isTodo, repo, rawId } = parseTodoTags(b.tags_json);
if (isTodo) list.push({ id: b.id, content: b.content, suggested_repo: repo || 'new', raw_id: rawId });
}
setTodos(list);
} catch { /* todos best-effort */ }
};
useEffect(load, []);
const clone = async (fullName: string) => {
setCloning(fullName);
setError(null);
try {
const r = await fetch(`${DAEMON}/projects/clone`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ repo: fullName }),
});
if (!r.ok) throw new Error(`clone 失敗 ${r.status}`);
setNewRepo('');
load();
} catch (e) {
setError(e instanceof Error ? e.message : String(e));
} finally {
setCloning(null);
}
};
const addRepo = () => {
const v = newRepo.trim();
if (!v) return;
// 允許輸入 "name" 或 "owner/name"
const full = v.includes('/') ? v : `richblack/${v}`;
clone(full);
};
const shown = useMemo(() => {
if (!repos) return [];
const arr = repos.filter(r => {
if (filter === 'sdd') return r.has_sdd;
return true;
});
// 有進度等你處理的優先,其次已 clone,其次最近活動
arr.sort((a, b) =>
b.leo_count - a.leo_count ||
Number(b.cloned) - Number(a.cloned) ||
(b.last_activity ?? '').localeCompare(a.last_activity ?? ''),
);
return arr;
}, [repos, filter]);
// 待辦按 suggested_repo 分組(outlinerRepo 父 → todo 子)
const todosByRepo = useMemo(() => {
const m = new Map<string, Todo[]>();
for (const t of todos) {
const key = t.suggested_repo || 'new';
if (!m.has(key)) m.set(key, []);
m.get(key)!.push(t);
}
return m;
}, [todos]);
const newTodos = todosByRepo.get('new') ?? [];
return (
<main className="mira-page">
<div className="mira-content">
<header className="mira-proj-header">
<h1 className="mira-proj-title">📋 </h1>
<p className="mira-proj-sub"> clone repo · 🤖 Mira / 👤 </p>
<div className="mira-proj-controls">
<div className="mira-proj-sort">
{([['sdd', '有進度'], ['all', '全部']] as [Filter, string][]).map(([k, label]) => (
<button
key={k}
className={`mira-proj-sort-btn${filter === k ? ' is-active' : ''}`}
onClick={() => setFilter(k)}
>
{label}
</button>
))}
</div>
<div className="mira-proj-add">
<input
className="mira-proj-add-input"
placeholder="加 reponame 或 owner/name"
value={newRepo}
onChange={e => setNewRepo(e.target.value)}
onKeyDown={e => { if (e.key === 'Enter') addRepo(); }}
/>
<button className="mira-proj-add-btn" onClick={addRepo} disabled={!!cloning || !newRepo.trim()}>
{cloning ? 'clone 中…' : '↓ clone'}
</button>
</div>
</div>
</header>
{error && (
<div className="mira-card mira-proj-error">
<strong> repo </strong>{error}
<div className="mira-proj-error-hint">
mira daemon{DAEMON}/projects daemon endpoint 10-A
</div>
</div>
)}
{!error && repos === null && <div className="mira-proj-loading"></div>}
{!error && repos?.length === 0 && (
<div className="mira-card mira-proj-empty">GitHub repo</div>
)}
<div className="mira-proj-grid">
{shown.map(r => (
<RepoCard
key={r.full_name}
r={r}
cloning={cloning === r.full_name}
onClone={() => clone(r.full_name)}
todos={todosByRepo.get(r.name) ?? []}
/>
))}
</div>
{newTodos.length > 0 && (
<div className="mira-card mira-proj-new-group">
<div className="mira-proj-new-title">💡 {newTodos.length}</div>
<ul className="mira-proj-todo-list">
{newTodos.map(t => (
<li key={t.id} className="mira-proj-todo-item">
<span className="mira-proj-todo-dot">·</span> {t.content}
{t.raw_id && (
<a className="mira-proj-todo-src" href={`/mira/feed#raw=${t.raw_id}`}> </a>
)}
</li>
))}
</ul>
</div>
)}
</div>
</main>
);
}
function pct(r: RepoSummary): number {
return r.total === 0 ? 0 : Math.round((r.done / r.total) * 100);
}
function relTime(iso: string | null): string {
if (!iso) return '—';
const t = new Date(iso).getTime();
if (Number.isNaN(t)) return '—';
const days = Math.floor((Date.now() - t) / 86400000);
if (days <= 0) return '今天';
if (days === 1) return '昨天';
if (days < 30) return `${days} 天前`;
return `${Math.floor(days / 30)} 個月前`;
}
function RepoCard({ r, cloning, onClone, todos }: { r: RepoSummary; cloning: boolean; onClone: () => void; todos: Todo[] }) {
const percent = pct(r);
return (
<div className="mira-card mira-proj-card">
<div className="mira-proj-card-top">
<span className="mira-proj-name">
{r.name}
{r.fork && <span className="mira-proj-tag">fork</span>}
{r.archived && <span className="mira-proj-tag">archived</span>}
</span>
<span className="mira-proj-time mira-en">{relTime(r.last_activity)}</span>
</div>
<div className="mira-proj-path mira-en">{r.full_name}</div>
{/* 河道偵測到、建議整進此專案的待辦(outliner 子項)*/}
{todos.length > 0 && (
<ul className="mira-proj-todo-list">
{todos.map(t => (
<li key={t.id} className="mira-proj-todo-item">
<span className="mira-proj-todo-dot">·</span> {t.content}
{t.raw_id && (
<a className="mira-proj-todo-src" href={`/mira/feed#raw=${t.raw_id}`}> </a>
)}
</li>
))}
</ul>
)}
{!r.cloned ? (
<div className="mira-proj-uncloned">
<span className="mira-proj-uncloned-hint"> clone Hetzner</span>
<button className="mira-proj-clone-btn" onClick={onClone} disabled={cloning}>
{cloning ? 'clone 中…' : '↓ clone 進工作態'}
</button>
</div>
) : !r.has_sdd ? (
<div className="mira-proj-nosdd"> clone · SDD .agents/specs</div>
) : (
<>
<div className="mira-proj-bar">
<div className="mira-proj-bar-fill" style={{ width: `${percent}%` }} />
</div>
<div className="mira-proj-stats">
<span className="mira-proj-pct mira-en">{percent}%</span>
<span className="mira-en">{r.done}/{r.total}</span>
{r.leo_count > 0 && <span className="mira-proj-chip mira-chip-leo">👤 {r.leo_count} </span>}
{r.ai_count > 0 && <span className="mira-proj-chip mira-chip-ai">🤖 {r.ai_count} Mira</span>}
{r.blocked_count > 0 && <span className="mira-proj-chip mira-chip-blocked"> {r.blocked_count}</span>}
</div>
</>
)}
</div>
);
}