diff --git a/.plans/rebuild-from-scratch/PLAN.md b/.plans/rebuild-from-scratch/PLAN.md new file mode 100644 index 0000000..87ef03b --- /dev/null +++ b/.plans/rebuild-from-scratch/PLAN.md @@ -0,0 +1,547 @@ +# 하나랑 대시보드 Rebuild From Scratch Implementation Plan + +> **For Hermes:** 이 문서는 하랑이 planner role 산출물이다. 승인 전 구현/배포 금지. 승인 후 나랑이 builder 또는 Codex가 task 단위로 실행하고, 다랑이 reviewer가 spec/code quality를 검증한다. + +**Goal:** 기존 하나랑 대시보드를 “자기야가 4자매가 작업하는 과정을 실시간으로 보는 서비스”로 처음부터 다시 설계하고 구현한다. 메인 화면의 핵심은 이랑이/하랑이/나랑이/다랑이가 PLAN → BUILD → REVIEW → DEPLOY 흐름에서 무엇을 하고 있는지 live timeline/board/scene으로 보여주는 것이다. + +**Architecture:** 기존 `hanarang-dashboard` repo는 유지하되, 제품/정보구조/UI를 새로 잡는다. Frontend는 Next.js 16 + styled-components를 유지하고, Backend는 NestJS + Prisma/MariaDB + Socket.IO를 유지한다. 메인 서비스는 `4 Sisters Live Workboard`이며, v0는 mock event stream으로 “작업 과정이 보이는 화면”을 먼저 완성한다. v1에서 NestJS Socket.IO event stream과 workflow state API를 붙인다. 기존 코드는 삭제보다 `legacy` 보존/점진 교체를 우선한다. + +**Tech Stack:** Next.js 16, React 19, styled-components, NestJS 11, Prisma 7, MariaDB, Socket.IO, Hermes role skills, Proxmox read-only API. + +--- + +## 0. 배경 + +기존 대시보드는 다음 전제를 갖고 있었다. + +- 하랑/나랑/다랑/이랑이 각각 독립 gateway로 상시 동작 +- 4자매 서버 상태를 곧 agent 상태로 봄 +- OpenClaw/Lobster workflow 흔적이 UI에 남아 있음 +- 실시간 office/metaphor와 운영 패널이 섞여 있음 + +현재 운영 방향은 바뀌었다. + +- 자기야의 기본 진입점은 이랑이 하나 +- 하랑=planner, 나랑=builder, 다랑=reviewer는 role skill로 호출 +- 하랑/나랑/다랑 gateway는 나중에 standby 전환 가능 +- Proxmox 서버 상태와 role runnable 상태를 분리해서 봐야 함 +- 하네스 엔지니어링 원칙: PLAN → 승인 → 구현 → QA → 배포 게이트 + +그래서 단순 리팩토링보다 “처음부터 다시 만드는” 수준의 IA/UI 재설계가 맞다. + +--- + +## 1. 제품 방향 + +### 한 줄 정의 + +하나랑 대시보드는 자기야가 **4자매가 작업하는 과정을 실시간으로 보는 서비스**다. 단순 관제센터가 아니라, 하랑이의 기획, 나랑이의 구현, 다랑이의 리뷰, 이랑이의 인프라/배포 판단이 어떤 순서로 흘러가는지 메인 화면에서 바로 보여준다. + +### 핵심 사용자 + +- 자기야: 최종 사용자/승인자 +- 이랑이: 메인 비서, 오케스트레이터, 인프라 책임자 +- 하랑이 role: planner +- 나랑이 role: builder +- 다랑이 role: reviewer + +### 새 핵심 은유 + +기존 “정적인 관제 카드 모음”이 아니라 **4자매 작업실을 실시간으로 보는 화면**이 메인이다. + +```text +4 Sisters Live Workboard + ├─ Live Work Scene: 지금 누가 어떤 작업 중인지 + ├─ Workflow Timeline: PLAN → BUILD → REVIEW → DEPLOY 이벤트 흐름 + ├─ Active Task Focus: 현재 선택된 작업의 상세 상태 + ├─ Sister Activity Lanes: 하랑/나랑/다랑/이랑 역할별 lane + └─ Infra/Approval Side Panel: 막힌 이유, 승인 필요, 인프라 위험 +``` + +이랑이는 여전히 단일 진입점이지만, 화면의 주인공은 “이랑이 혼자”가 아니라 **4자매가 함께 일하는 과정**이다. 하랑/나랑/다랑이 gateway로 상시 떠 있지 않아도, role execution/event log를 통해 작업 과정이 보여야 한다. + +--- + +## 2. Scope + +### 포함 — v0 + +- 메인 화면을 `4 Sisters Live Workboard`로 설계 +- 하랑/나랑/다랑/이랑 activity lane +- PLAN → BUILD → REVIEW → DEPLOY live timeline +- 현재 작업 focus panel +- approval/blocker side panel +- mock event stream으로 실시간처럼 흐르는 UI +- 새 AppShell/Navigation +- 4자매 role status 카드 +- Proxmox monitoring link/panel placeholder +- 프로젝트 list placeholder +- mobile-first layout +- mock/static data로 UI 완성 +- 기존 route와 충돌하지 않는 방식으로 새 route 구성 + +### 포함 — v1 + +- Backend API 계약 연결 +- role agent health endpoint 연결 +- Proxmox v0 live inventory 연결 +- project/workflow API 연결 +- Socket.IO live update 일부 적용 + +### 포함 — v2 + +- 기존 legacy 화면 정리 +- 인증/권한 흐름 재검토 +- 실제 action gate UI +- deployment/QA report UI + +### 제외 — 당장 하지 않음 + +- 기존 DB schema 삭제 +- 기존 production deploy +- 하랑/나랑/다랑 gateway 중지 +- Proxmox write action +- DB migration +- 도메인/Nginx 변경 + +--- + +## 3. Product IA + +### Primary Navigation + +1. **Live** — 4자매 작업 과정 실시간 메인 화면 +2. **Harness** — PLAN/Build/Review/Deploy 흐름 상세 +3. **Projects** — repo/project/sprint/task +4. **Infra** — Proxmox/server/service health +5. **Agents** — 이랑이 + 하랑/나랑/다랑 role health +6. **Settings** — 토큰/환경/연동 상태. secret 값 표시 금지 + +### Live 메인 화면 + +메인 화면은 대시보드의 핵심 서비스다. + +상단: +- 현재 active workflow 이름 +- live/snapshot/mock/fallback source badge +- 전체 phase: Planning / Building / Reviewing / Waiting Approval / Deploying +- pending approvals / blockers + +중앙: +- **4 Sisters Live Workboard** + - 하랑 lane: 요구사항 정리, PLAN 작성, handoff + - 나랑 lane: 구현 task, test/build, diff 생성 + - 다랑 lane: spec review, code review, QA 결과 + - 이랑 lane: 인프라 확인, 배포 게이트, 위험 차단 +- 각 lane에는 현재 상태, 마지막 이벤트, 진행 중 작업, 다음 handoff를 표시한다. + +우측: +- Active Task Focus + - 지금 선택한 이벤트/작업 상세 + - 관련 파일/plan/QA report 링크 + - tool call 또는 verification command 상태 + +하단: +- Workflow Timeline + - event stream 형태 + - PLAN_CREATED, BUILD_STARTED, TEST_PASSED, REVIEW_REQUESTED, QA_FAILED, APPROVAL_NEEDED 같은 이벤트 +- Infra/Approval compact panel + +### Harness 화면 + +Live 화면보다 상세한 작업 단위 board다. + +- workflow list +- stage별 task cards +- approval gates +- QA report links +- build/test logs + +### Agents 화면 + +agent 상태를 4축으로 표시한다. + +| 항목 | 의미 | +|---|---| +| role | erang-main, harang-planner, narang-builder, darang-reviewer | +| mode | main, role-skill, profile, gateway | +| runnable | 현재 호출 가능한지 | +| gateway | active, standby, failed, unknown | +| server | LXC running 여부 | + +### Infra 화면 + +처음엔 Proxmox monitoring PLAN과 연결한다. + +- `/infra/proxmox` +- live-only v0 +- DB 없음 +- write action 없음 + +--- + +## 4. Design Principles + +1. **운영 판단이 먼저** + - 예쁜 오피스보다 “지금 위험한가?”를 먼저 보여준다. +2. **상태의 출처를 숨기지 않음** + - live / snapshot / mock / fallback 구분. +3. **상시 gateway와 role runnable을 분리** + - gateway standby는 장애가 아니다. +4. **모바일 우선** + - 자기야 iPhone에서 빠르게 봐야 함. +5. **과한 애니메이션 금지** + - 상태 파악 방해 금지. +6. **하네스 원칙 내장** + - PLAN 승인 없이는 Build가 시작되지 않는 UI. + +--- + +## 5. Repo Strategy + +### 기본 전략 + +기존 repo 유지: + +```text +/home/erang/hanarang-dashboard +``` + +새 구현은 기존 route를 바로 갈아엎지 않고, feature branch에서 새 구조를 만든다. + +추천 branch: + +```bash +git checkout -b feature/rebuild-control-center +``` + +### 파일 구조 제안 + +Frontend: + +```text +frontend/app/(control)/page.tsx +frontend/app/(control)/harness/page.tsx +frontend/app/(control)/projects/page.tsx +frontend/app/(control)/infra/page.tsx +frontend/app/(control)/infra/proxmox/page.tsx +frontend/app/(control)/agents/page.tsx +frontend/app/(control)/settings/page.tsx + +frontend/components/control/AppFrame.tsx +frontend/components/control/TopStatusBar.tsx +frontend/components/control/NavigationRail.tsx +frontend/components/control/StatusCard.tsx +frontend/components/control/SourceBadge.tsx + +frontend/components/harness/HarnessBoard.tsx +frontend/components/harness/PipelineStageCard.tsx +frontend/components/harness/ApprovalQueue.tsx + +frontend/components/agents/AgentRoleCard.tsx +frontend/components/agents/AgentRoleMap.tsx + +frontend/components/infra/InfraSummaryPanel.tsx +frontend/components/infra/ProxmoxSummaryCard.tsx + +frontend/lib/control/mock-data.ts +frontend/lib/control/types.ts +frontend/lib/control/api.ts +``` + +Backend v0/v1: + +```text +backend/src/infra/agents/agent-role-health.controller.ts +backend/src/infra/agents/agent-role-health.service.ts +backend/src/infra/proxmox/* +backend/src/control/control.controller.ts +backend/src/control/control.service.ts +``` + +--- + +## 6. Tasks + +### Task 1: 현재 구조 조사 + +**Objective:** 새 대시보드를 얹을 수 있는 실제 route/component/API 구조를 확인한다. + +**Files:** +- Read: `frontend/app/**` +- Read: `frontend/components/**` +- Read: `frontend/AGENTS.md` +- Read: `backend/src/app.module.ts` +- Read: `backend/src/dashboard/**` +- Read: `backend/src/events/**` + +**Commands:** + +```bash +cd /home/erang/hanarang-dashboard +find frontend/app -maxdepth 4 -type f | sort +find frontend/components -maxdepth 3 -type f | sort +find backend/src -maxdepth 3 -type f | sort +``` + +**Verification:** +- Next.js route 구조 확인 +- styled-components 사용 패턴 확인 +- 기존 API/gateway 패턴 확인 + +--- + +### Task 2: 새 제품 스펙 작성 + +**Objective:** 기존 PRD를 새 운영 구조에 맞게 대체한다. + +**Files:** +- Create: `.plans/rebuild-from-scratch/product-spec.md` +- Modify: `docs/product-specs/openclaw-office-dashboard-prd.md` 또는 새 파일 생성 + +**Acceptance criteria:** +- 이랑이 단일 진입점 명시 +- 하랑/나랑/다랑 role agent 모델 명시 +- gateway standby != down 명시 +- Proxmox monitoring 연결 명시 + +**Verification:** +- 다랑이 review에서 scope ambiguity 없음 + +--- + +### Task 3: Design system 초안 + +**Objective:** 새 dashboard의 UI 토큰과 컴포넌트 원칙을 정한다. + +**Files:** +- Create: `.plans/rebuild-from-scratch/design-system.md` +- Create: `frontend/components/control/README.md` + +**Design tokens:** +- background: dark infra control center +- status colors: ok/warning/critical/standby/unknown +- density: desktop 3-column, mobile 1-column +- source badges: live/snapshot/mock/fallback + +**Verification:** +- 모든 상태가 색상만이 아니라 text label도 가짐 + +--- + +### Task 4: Frontend live workboard skeleton + +**Objective:** mock event stream만으로 4자매가 작업하는 과정이 메인 화면에서 실시간처럼 보이게 만든다. + +**Files:** +- Create: `frontend/app/(control)/page.tsx` +- Create: `frontend/components/control/AppFrame.tsx` +- Create: `frontend/components/control/TopStatusBar.tsx` +- Create: `frontend/components/control/NavigationRail.tsx` +- Create: `frontend/components/live/LiveWorkboard.tsx` +- Create: `frontend/components/live/SisterActivityLane.tsx` +- Create: `frontend/components/live/WorkflowTimeline.tsx` +- Create: `frontend/components/live/ActiveTaskFocus.tsx` +- Create: `frontend/lib/control/types.ts` +- Create: `frontend/lib/control/mock-events.ts` + +**Rules:** +- 기존 `frontend/app/page.tsx`를 바로 삭제하지 않는다. +- 새 route가 충돌하면 `/control` 아래에 먼저 만든다. +- mock data는 `source: 'mock'`을 반드시 표시한다. +- 메인 화면의 첫 impression은 “4자매가 일하고 있다”여야 한다. 인프라/프로젝트 카드는 보조 패널로 둔다. + +**Verification:** + +```bash +cd frontend +npm run lint +npm run build +``` + +--- + +### Task 5: Harness detail board UI + +**Objective:** 메인 Live Workboard에서 선택한 workflow를 더 자세히 보는 상세 board를 만든다. + +**Files:** +- Create: `frontend/components/harness/HarnessBoard.tsx` +- Create: `frontend/components/harness/PipelineStageCard.tsx` +- Create: `frontend/components/harness/ApprovalQueue.tsx` + +**Acceptance criteria:** +- PLAN_REQUIRED, BUILDING, REVIEWING, WAITING_APPROVAL, DEPLOY_READY 상태 표시 +- 각 단계에 responsible role 표시 +- 승인 대기 항목이 명확함 +- Live 화면의 event와 같은 workflow id로 연결됨 + +**Verification:** +- mock pipeline 3개 렌더링 +- 모바일 1열 전환 + +--- + +### Task 6: Agent role map UI + +**Objective:** 이랑이 중심 role agent 상태를 표시한다. + +**Files:** +- Create: `frontend/components/agents/AgentRoleMap.tsx` +- Create: `frontend/components/agents/AgentRoleCard.tsx` + +**Acceptance criteria:** +- erang-main = active main +- harang-planner/narang-builder/darang-reviewer = role-skill runnable +- gateway active/standby/failed와 runnable 분리 + +--- + +### Task 7: Infra summary placeholder + +**Objective:** Proxmox monitoring v0와 연결될 홈 요약 영역을 만든다. + +**Files:** +- Create: `frontend/components/infra/InfraSummaryPanel.tsx` +- Create: `frontend/components/infra/ProxmoxSummaryCard.tsx` + +**Acceptance criteria:** +- live/snapshot/mock source badge +- critical 우선 정렬 +- `/infra/proxmox`로 이동 link + +--- + +### Task 8: Backend live workflow summary API + +**Objective:** frontend mock event stream을 backend live/snapshot API 계약으로 전환할 수 있게 한다. + +**Files:** +- Create: `backend/src/control/control.module.ts` +- Create: `backend/src/control/control.controller.ts` +- Create: `backend/src/control/control.service.ts` +- Create: `backend/src/control/workflow-events.gateway.ts` +- Modify: `backend/src/app.module.ts` + +**Endpoints:** + +```text +GET /control/summary +GET /control/workflows/active +WS /control/events +``` + +**Response:** + +```ts +type ControlSummary = { + activeWorkflow: WorkflowSummary | null; + sisters: SisterActivityLaneState[]; + events: WorkflowEvent[]; + infra: InfraSummary; + approvals: ApprovalItem[]; + source: 'live' | 'snapshot' | 'mock' | 'fallback'; + updatedAt: string; +}; +``` + +**Verification:** + +```bash +cd backend +npm test -- control +npm run build +``` + +--- + +### Task 9: API client 연결 + +**Objective:** frontend가 `/control/summary`를 읽고 실패 시 fallback mock을 쓴다. + +**Files:** +- Create: `frontend/lib/control/api.ts` +- Modify: `frontend/app/(control)/page.tsx` + +**Acceptance criteria:** +- API 실패 시 fallback 표시 +- fallback임을 UI에 명확히 표시 +- loading/error/empty 상태 있음 + +--- + +### Task 10: QA 문서 작성 + +**Objective:** 다랑이가 검증할 QA checklist를 만든다. + +**Files:** +- Create: `.plans/rebuild-from-scratch/qa-checklist.md` + +**Checks:** +- iPhone viewport +- source badge 정확성 +- gateway standby != down +- mock/fallback 구분 +- PLAN approval gate 표시 +- build/lint pass + +--- + +## 7. Approval Gates + +### Gate A — Rebuild plan approval + +지금 단계. 자기야가 이 PLAN을 승인해야 구현 시작 가능. + +### Gate B — Frontend v0 implementation + +허용: +- frontend route/component 추가 +- mock/static data +- no DB migration +- no deploy + +### Gate C — Backend API v0 + +허용: +- read-only summary endpoint +- mock/fallback service +- no DB migration + +### Gate D — Proxmox integration + +별도 Proxmox monitoring PLAN과 연결. + +### Gate E — Deploy + +L3. 별도 승인 필요. + +--- + +## 8. Rollback Plan + +- feature branch에서 작업한다. +- 기존 `main`과 production route는 건드리지 않는다. +- 새 route가 문제면 branch 폐기 또는 route 제거. +- 배포 전까지 기존 사이트 영향 없음. + +--- + +## 9. Open Questions + +1. 이름을 계속 `하나랑 대시보드`로 둘지, `하나랑 Control Center`로 바꿀지 +2. 첫 화면을 `/`로 교체할지, `/control`에서 먼저 검증할지 +3. 기존 오피스/등축 UI를 완전히 버릴지, 나중에 `Agents` 화면의 보조 시각화로 남길지 +4. 로그인/권한을 유지할지, 자기야 개인망 전용으로 단순화할지 + +## 10. 이랑이 추천 + +- 이름: `하나랑 Live Workboard` 또는 `하나랑 Control Center` +- 메인 서비스: 4자매 작업 과정 실시간 화면 +- 구현 시작 route: `/control` 또는 `/live` +- 기존 `/`는 당장 유지 +- v0는 mock event stream으로 “작업 과정이 보이는 화면”부터 완성 +- 이후 backend `/control/summary`, `/control/events` 연결 +- 마지막에 `/`를 새 live 화면으로 교체 + +이 방식이면 자기야가 원하는 “4자매가 일하는 걸 실시간으로 보는 메인 서비스”를 먼저 만들고, 인프라/프로젝트 관제는 그 옆에 붙일 수 있어. diff --git a/.plans/rebuild-from-scratch/builder-report-v0.md b/.plans/rebuild-from-scratch/builder-report-v0.md new file mode 100644 index 0000000..a11c9f3 --- /dev/null +++ b/.plans/rebuild-from-scratch/builder-report-v0.md @@ -0,0 +1,57 @@ +# Narang Builder Report — Live Workboard v0 + +## Scope Executed + +- Plan: `.plans/rebuild-from-scratch/PLAN.md` +- Task: Frontend live workboard skeleton +- Scope: mock event stream 기반 4자매 작업 과정 메인 화면 구현 + +## Files Changed + +- `frontend/app/page.tsx` + - 기존 사무실 메인 화면을 Live Workboard 메인 화면으로 교체 +- `frontend/app/control/page.tsx` + - `/control` route에서도 같은 Live Workboard 확인 가능 +- `frontend/components/common/Sidebar.tsx` + - nav에 `Live`, `Control` 항목 추가 +- `frontend/components/live/LiveWorkboard.tsx` + - 메인 Live Workboard container + - phase strip, 4자매 lane, active focus, timeline, v0 scope note 구성 +- `frontend/components/live/SisterActivityLane.tsx` + - 하랑/나랑/다랑/이랑 activity lane card +- `frontend/components/live/WorkflowTimeline.tsx` + - mock event stream timeline +- `frontend/components/live/ActiveTaskFocus.tsx` + - 선택 이벤트 상세와 approval/risk panel +- `frontend/lib/control/types.ts` + - Live Workboard 타입 정의 +- `frontend/lib/control/mock-events.ts` + - v0 mock workflow/sister/event/approval data + +## Verification + +- `npm run lint -- components/live app/control app/page.tsx lib/control components/common/Sidebar.tsx`: PASS +- `npm run build`: PASS +- `curl -I http://localhost:3004/`: HTTP 200 during dev server smoke test +- Browser accessibility snapshot: `/`에 Live Workboard, 4자매 lane, Workflow Timeline, Active Task Focus 확인 + +## Known Existing Issues + +- `npm run lint` 전체 실행은 기존 파일들의 React 19 lint rule 위반 때문에 FAIL. +- 새로 추가/수정한 Live Workboard 관련 파일만 대상으로 한 lint는 PASS. +- 기존 오류 위치 예시: + - `components/office-room/SisterDetailPanel.tsx` + - `components/rails/FileViewerModal.tsx` + - `components/rails/SubTaskDetailDrawer.tsx` + - `components/rails/TransitionsTimeline.tsx` + +## Risks / Notes + +- 현재 데이터는 `source: mock`으로 명확히 표시됨. +- 아직 real Socket.IO event stream은 연결하지 않음. +- production deploy는 수행하지 않음. +- DB/Prisma/Nginx/systemd 변경 없음. + +## Ready for Darang Review + +yes diff --git a/.plans/rebuild-from-scratch/darang-review-v0.md b/.plans/rebuild-from-scratch/darang-review-v0.md new file mode 100644 index 0000000..8554343 --- /dev/null +++ b/.plans/rebuild-from-scratch/darang-review-v0.md @@ -0,0 +1,59 @@ +# Darang Review Report — Live Workboard v0 + +## Verdict + +PASS WITH IMPORTANT NOTES + +## Spec Compliance + +- [x] 메인 화면의 핵심을 4자매 작업 과정 실시간 시각화로 변경했다. +- [x] 하랑/나랑/다랑/이랑 activity lane을 구현했다. +- [x] Workflow Timeline을 구현했다. +- [x] Active Task Focus를 구현했다. +- [x] Approval / Risk side panel을 구현했다. +- [x] `source: mock · v0`를 UI에 명확히 표시했다. +- [x] `/` 메인 화면과 `/control` route에서 Live Workboard를 볼 수 있다. +- [x] DB migration, 배포, 인프라 변경 없이 frontend v0 범위만 수행했다. + +## Critical Issues + +없음. + +## Important Issues + +1. 전체 `npm run lint`는 기존 legacy 파일 오류 때문에 실패한다. + - 새 파일/수정 파일 대상 lint는 PASS. + - build는 PASS. +2. 브라우저 vision 검증은 인증 상태 차이 때문에 로그인 화면을 보기도 했다. + - accessibility snapshot과 curl smoke에서는 `/` route가 정상 응답하고 Live Workboard 구조가 확인됐다. + - 실제 운영 계정 로그인 후 시각 QA가 한 번 더 필요하다. + +## Minor Issues + +- `/control`과 `/`가 같은 화면을 보여준다. v1에서 `/control`을 상세 관제 route로 분리할지 결정 필요. +- mock event 자동 회전은 데모용이다. 실제 event stream 연결 시 선택 이벤트 유지 정책을 다시 정해야 한다. + +## Verification Checked + +- `npm run lint -- components/live app/control app/page.tsx lib/control components/common/Sidebar.tsx`: PASS +- `npm run build`: PASS +- `curl -I http://localhost:3004/`: PASS, HTTP 200 +- browser snapshot: PASS, Live Workboard 주요 섹션 확인 + +## Deployment Risk + +Low for local/frontend code. + +이유: +- frontend-only 변경 +- DB/인프라/배포 없음 +- 기존 office 컴포넌트 삭제 없음 +- production 반영은 아직 별도 승인 필요 + +## Required Fixes Before Proceeding + +없음. + +## Recommended Next Step + +v1 전에 먼저 자기야가 화면 방향을 확인한다. 방향이 맞으면 다음 task는 backend event contract (`GET /control/summary`, `WS /control/events`) 설계/구현이다. diff --git a/frontend/app/control/page.tsx b/frontend/app/control/page.tsx new file mode 100644 index 0000000..77083a0 --- /dev/null +++ b/frontend/app/control/page.tsx @@ -0,0 +1,7 @@ +'use client'; + +import LiveWorkboard from '@/components/live/LiveWorkboard'; + +export default function ControlPage() { + return ; +} diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index 61098e5..3b46ed9 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -1,103 +1,7 @@ 'use client'; -import React, { useEffect, useState } from 'react'; -import styled from 'styled-components'; -import { API_URL } from '@/lib/config'; -import OfficeRoom from '@/components/office-room/OfficeRoom'; -import ActivePipelineStrip from '@/components/dashboard/ActivePipelineStrip'; - -const PageRoot = styled.div` - display: flex; - flex-direction: column; - gap: var(--space-xl); -`; - -const PageHeader = styled.header` - display: flex; - flex-direction: column; - gap: 4px; -`; - -const PageTitle = styled.h1` - font-size: 22px; - font-weight: 700; - margin: 0; - color: var(--text-primary); -`; - -const PageSub = styled.p` - font-size: 12px; - color: var(--text-secondary); - margin: 0; - font-family: var(--font-mono); -`; - -type Stage = 'plan' | 'implement' | 'review' | 'deploy' | null; - -const STATE_TO_STAGE: Record = { - planning: 'plan', - implementing: 'implement', - reviewing: 'review', - deploying: 'deploy', -}; +import LiveWorkboard from '@/components/live/LiveWorkboard'; export default function HomePage() { - const [activeStage, setActiveStage] = useState(null); - - // Poll the rails pipelines list every 4s and pick the most recently - // updated non-terminal pipeline. Map its currentState to a stage so the - // matching sister card pulses. - useEffect(() => { - let cancelled = false; - const load = async () => { - try { - const res = await fetch(`${API_URL}/api/rails/pipelines?limit=10`, { - credentials: 'include', - }); - if (!res.ok) return; - const data = (await res.json()) as { - pipelines?: Array<{ currentState: string; updatedAt: string }>; - }; - if (cancelled) return; - const live = (data.pipelines ?? []).filter((p) => - ['planning', 'implementing', 'reviewing', 'deploying'].includes( - p.currentState, - ), - ); - if (live.length === 0) { - setActiveStage(null); - return; - } - // Most recently updated wins - const sorted = live.slice().sort((a, b) => - (b.updatedAt ?? '').localeCompare(a.updatedAt ?? ''), - ); - const top = sorted[0]; - if (top) setActiveStage(STATE_TO_STAGE[top.currentState] ?? null); - } catch { - // ignore - } - }; - void load(); - const t = setInterval(load, 4000); - return () => { - cancelled = true; - clearInterval(t); - }; - }, []); - - return ( - - - 🏢 사무실 - - 4 자매가 일하는 가상 사무실 — 클릭으로 상세 정보, /rails 에서 파이프라인 모니터링 - - - - - - - - ); + return ; } diff --git a/frontend/components/common/Sidebar.tsx b/frontend/components/common/Sidebar.tsx index 163a3ab..0728f91 100644 --- a/frontend/components/common/Sidebar.tsx +++ b/frontend/components/common/Sidebar.tsx @@ -7,7 +7,8 @@ import { usePathname } from 'next/navigation'; import { useAuth } from '@/lib/AuthContext'; const NAV_ITEMS = [ - { href: '/', label: '🏢 사무실' }, + { href: '/', label: '🟠 Live' }, + { href: '/control', label: '🧭 Control' }, { href: '/rails', label: '🚦 레일' }, { href: '/settings', label: '⚙️ 설정' }, ]; diff --git a/frontend/components/live/ActiveTaskFocus.tsx b/frontend/components/live/ActiveTaskFocus.tsx new file mode 100644 index 0000000..2407491 --- /dev/null +++ b/frontend/components/live/ActiveTaskFocus.tsx @@ -0,0 +1,136 @@ +'use client'; + +import styled from 'styled-components'; +import type { ApprovalItem, WorkflowEvent } from '@/lib/control/types'; + +const Panel = styled.aside` + display: grid; + gap: 14px; +`; + +const Card = styled.section` + border: 1px solid var(--border-color); + border-radius: 22px; + padding: 18px; + background: linear-gradient(180deg, rgba(255,255,255,0.045), rgba(255,255,255,0.018)); +`; + +const Title = styled.h2` + margin: 0 0 12px; + font-size: 15px; +`; + +const Eyebrow = styled.div` + font-family: var(--font-mono); + font-size: 10px; + color: var(--text-secondary); + text-transform: uppercase; + margin-bottom: 6px; +`; + +const EventTitle = styled.h3` + margin: 0; + font-size: 20px; + line-height: 1.2; +`; + +const Detail = styled.p` + margin: 12px 0 0; + color: var(--text-secondary); + font-size: 13px; +`; + +const MetaGrid = styled.div` + display: grid; + grid-template-columns: 1fr 1fr; + gap: 8px; + margin-top: 16px; +`; + +const Meta = styled.div` + border: 1px solid rgba(255,255,255,0.08); + border-radius: 14px; + padding: 10px; + background: rgba(0,0,0,0.18); +`; + +const MetaValue = styled.div` + font-family: var(--font-mono); + font-size: 11px; + color: var(--text-primary); + overflow-wrap: anywhere; +`; + +const ApprovalList = styled.div` + display: grid; + gap: 8px; +`; + +const ApprovalRow = styled.div<{ $status: ApprovalItem['status'] }>` + display: grid; + grid-template-columns: 42px 1fr auto; + gap: 8px; + align-items: center; + border: 1px solid ${({ $status }) => { + if ($status === 'blocked') return 'rgba(255, 112, 112, 0.42)'; + if ($status === 'waiting') return 'rgba(255, 204, 102, 0.38)'; + return 'rgba(139, 211, 255, 0.28)'; + }}; + border-radius: 14px; + padding: 10px; + background: rgba(0,0,0,0.14); + font-size: 12px; +`; + +const Risk = styled.span` + font-family: var(--font-mono); + color: var(--text-secondary); +`; + +const Status = styled.span` + font-family: var(--font-mono); + font-size: 10px; + color: var(--text-secondary); +`; + +export default function ActiveTaskFocus({ + selectedEvent, + approvals, +}: { + selectedEvent: WorkflowEvent; + approvals: ApprovalItem[]; +}) { + return ( + + + Active Task Focus + {selectedEvent.sister} · {selectedEvent.type} + {selectedEvent.title} + {selectedEvent.detail} + + + workflow + {selectedEvent.workflowId} + + + source + {selectedEvent.source} + + + + + + Approval / Risk + + {approvals.map((item) => ( + + {item.risk} + {item.title} + {item.status} + + ))} + + + + ); +} diff --git a/frontend/components/live/LiveWorkboard.tsx b/frontend/components/live/LiveWorkboard.tsx new file mode 100644 index 0000000..35f47a8 --- /dev/null +++ b/frontend/components/live/LiveWorkboard.tsx @@ -0,0 +1,223 @@ +'use client'; + +import { useEffect, useMemo, useState } from 'react'; +import styled from 'styled-components'; +import SisterActivityLane from './SisterActivityLane'; +import WorkflowTimeline from './WorkflowTimeline'; +import ActiveTaskFocus from './ActiveTaskFocus'; +import { mockLiveWorkboard, rotateEvents } from '@/lib/control/mock-events'; + +const Root = styled.div` + display: flex; + flex-direction: column; + gap: var(--space-lg); +`; + +const Hero = styled.header` + border: 1px solid rgba(255,255,255,0.12); + border-radius: 28px; + padding: 24px; + background: + radial-gradient(circle at 16% 18%, rgba(255,143,112,0.18), transparent 34%), + radial-gradient(circle at 82% 12%, rgba(139,211,255,0.16), transparent 28%), + linear-gradient(135deg, rgba(255,255,255,0.075), rgba(255,255,255,0.02)); +`; + +const HeroTop = styled.div` + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 16px; + + @media (max-width: 767px) { + flex-direction: column; + } +`; + +const TitleBlock = styled.div` + display: grid; + gap: 8px; +`; + +const Label = styled.div` + font-family: var(--font-mono); + color: var(--text-secondary); + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.08em; +`; + +const Title = styled.h1` + margin: 0; + font-size: clamp(30px, 5vw, 58px); + line-height: 0.95; + letter-spacing: -0.055em; +`; + +const Summary = styled.p` + margin: 0; + max-width: 760px; + color: var(--text-secondary); + font-size: 14px; +`; + +const SourceBadge = styled.div` + display: inline-flex; + align-items: center; + gap: 8px; + border: 1px solid rgba(255, 204, 102, 0.35); + border-radius: 999px; + padding: 8px 12px; + color: #ffcc66; + font-family: var(--font-mono); + font-size: 11px; + white-space: nowrap; +`; + +const Dot = styled.span` + width: 7px; + height: 7px; + border-radius: 50%; + background: #ffcc66; + box-shadow: 0 0 18px rgba(255, 204, 102, 0.95); +`; + +const PhaseGrid = styled.div` + display: grid; + grid-template-columns: repeat(5, minmax(0, 1fr)); + gap: 8px; + margin-top: 22px; + + @media (max-width: 767px) { + grid-template-columns: 1fr; + } +`; + +const PhaseCell = styled.div<{ $active: boolean }>` + border: 1px solid ${({ $active }) => ($active ? 'rgba(255,255,255,0.36)' : 'rgba(255,255,255,0.09)')}; + border-radius: 14px; + padding: 10px; + color: ${({ $active }) => ($active ? 'var(--text-primary)' : 'var(--text-secondary)')}; + background: ${({ $active }) => ($active ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.16)')}; + font-family: var(--font-mono); + font-size: 11px; +`; + +const MainGrid = styled.div` + display: grid; + grid-template-columns: minmax(0, 1fr) 360px; + gap: var(--space-lg); + + @media (max-width: 1100px) { + grid-template-columns: 1fr; + } +`; + +const LanesGrid = styled.section` + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: var(--space-md); + + @media (max-width: 900px) { + grid-template-columns: 1fr; + } +`; + +const BottomGrid = styled.div` + display: grid; + grid-template-columns: minmax(0, 1.35fr) minmax(280px, 0.65fr); + gap: var(--space-lg); + + @media (max-width: 1100px) { + grid-template-columns: 1fr; + } +`; + +const NoteCard = styled.section` + border: 1px solid var(--border-color); + border-radius: 22px; + padding: 18px; + background: rgba(255,255,255,0.025); +`; + +const NoteTitle = styled.h2` + margin: 0 0 8px; + font-size: 15px; +`; + +const NoteText = styled.p` + margin: 0; + color: var(--text-secondary); + font-size: 13px; +`; + +const phases = [ + ['planning', 'PLAN'], + ['building', 'BUILD'], + ['reviewing', 'REVIEW'], + ['waiting_approval', 'APPROVAL'], + ['deploying', 'DEPLOY'], +] as const; + +export default function LiveWorkboard() { + const [tick, setTick] = useState(0); + const [selectedId, setSelectedId] = useState(mockLiveWorkboard.events[0]?.id ?? ''); + + const events = useMemo(() => rotateEvents(mockLiveWorkboard.events, tick), [tick]); + const selected = events.find((event) => event.id === selectedId) ?? events[0]; + + useEffect(() => { + const timer = window.setInterval(() => { + setTick((value) => value + 1); + }, 4200); + return () => window.clearInterval(timer); + }, []); + + return ( + + + + + + 지금 4자매가 어떻게 일하는지 + {mockLiveWorkboard.workflow.summary} + + + source: {mockLiveWorkboard.source} · v0 + + + + + {phases.map(([phase, label]) => ( + + {label} + + ))} + + + + + + {mockLiveWorkboard.sisters.map((lane) => ( + + ))} + + + + + + setSelectedId(event.id)} + /> + + v0 범위 + + 이 화면은 아직 mock event stream이야. 목적은 먼저 “4자매가 작업하는 과정이 보이는 경험”을 검증하는 것. 다음 단계에서 Socket.IO live event와 `/control/summary`를 붙이면 돼. + + + + + ); +} diff --git a/frontend/components/live/SisterActivityLane.tsx b/frontend/components/live/SisterActivityLane.tsx new file mode 100644 index 0000000..080cc82 --- /dev/null +++ b/frontend/components/live/SisterActivityLane.tsx @@ -0,0 +1,149 @@ +'use client'; + +import styled, { keyframes } from 'styled-components'; +import type { SisterLaneState } from '@/lib/control/types'; + +const pulse = keyframes` + 0%, 100% { box-shadow: 0 0 0 rgba(255, 255, 255, 0); } + 50% { box-shadow: 0 0 24px rgba(255, 255, 255, 0.08); } +`; + +const LaneCard = styled.article<{ $color: string; $active: boolean }>` + position: relative; + min-height: 258px; + border: 1px solid color-mix(in srgb, ${({ $color }) => $color} 42%, var(--border-color)); + border-radius: 22px; + padding: 18px; + overflow: hidden; + background: + radial-gradient(circle at top right, color-mix(in srgb, ${({ $color }) => $color} 18%, transparent), transparent 34%), + linear-gradient(180deg, rgba(255,255,255,0.045), rgba(255,255,255,0.015)); + animation: ${({ $active }) => ($active ? pulse : 'none')} 2.8s ease-in-out infinite; +`; + +const TopRow = styled.div` + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; + margin-bottom: 18px; +`; + +const Identity = styled.div` + display: flex; + flex-direction: column; + gap: 4px; +`; + +const Name = styled.h3` + margin: 0; + font-size: 18px; + letter-spacing: -0.02em; +`; + +const Role = styled.span` + font-family: var(--font-mono); + font-size: 11px; + color: var(--text-secondary); + text-transform: uppercase; +`; + +const StatusPill = styled.span<{ $color: string }>` + border: 1px solid color-mix(in srgb, ${({ $color }) => $color} 56%, var(--border-color)); + color: ${({ $color }) => $color}; + border-radius: 999px; + padding: 5px 9px; + font-family: var(--font-mono); + font-size: 10px; + background: rgba(0, 0, 0, 0.22); +`; + +const TaskBlock = styled.div` + display: grid; + gap: 12px; +`; + +const Label = styled.div` + font-size: 10px; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--text-secondary); + font-family: var(--font-mono); + margin-bottom: 4px; +`; + +const Body = styled.p` + margin: 0; + font-size: 14px; + color: var(--text-primary); +`; + +const Muted = styled.p` + margin: 0; + font-size: 12px; + color: var(--text-secondary); +`; + +const ProgressTrack = styled.div` + height: 7px; + border-radius: 999px; + background: rgba(255, 255, 255, 0.08); + overflow: hidden; + margin-top: 16px; +`; + +const ProgressFill = styled.div<{ $progress: number; $color: string }>` + width: ${({ $progress }) => $progress}%; + height: 100%; + border-radius: inherit; + background: linear-gradient(90deg, ${({ $color }) => $color}, color-mix(in srgb, ${({ $color }) => $color} 54%, white)); +`; + +const MetaRow = styled.div` + display: flex; + justify-content: space-between; + gap: 8px; + margin-top: 10px; + font-family: var(--font-mono); + font-size: 10px; + color: var(--text-secondary); +`; + +export default function SisterActivityLane({ lane }: { lane: SisterLaneState }) { + const active = lane.status === 'working' || lane.status === 'thinking' || lane.status === 'reviewing'; + + return ( + + + + {lane.name} + {lane.role} · {lane.mode} + + {lane.status} + + + +
+ + {lane.currentTask} +
+
+ + {lane.lastEvent} +
+
+ + {lane.nextHandoff} +
+
+ + + + + + {lane.phase} + {lane.progress}% + +
+ ); +} diff --git a/frontend/components/live/WorkflowTimeline.tsx b/frontend/components/live/WorkflowTimeline.tsx new file mode 100644 index 0000000..f1ec236 --- /dev/null +++ b/frontend/components/live/WorkflowTimeline.tsx @@ -0,0 +1,123 @@ +'use client'; + +import styled from 'styled-components'; +import type { WorkflowEvent } from '@/lib/control/types'; + +const TimelineRoot = styled.section` + border: 1px solid var(--border-color); + border-radius: 22px; + background: rgba(255, 255, 255, 0.025); + padding: 18px; +`; + +const Header = styled.div` + display: flex; + justify-content: space-between; + gap: 12px; + align-items: center; + margin-bottom: 14px; +`; + +const Title = styled.h2` + font-size: 15px; + margin: 0; +`; + +const LiveBadge = styled.span` + font-family: var(--font-mono); + font-size: 10px; + color: #ffcc66; + border: 1px solid rgba(255, 204, 102, 0.35); + border-radius: 999px; + padding: 4px 8px; +`; + +const EventList = styled.ol` + list-style: none; + display: grid; + gap: 10px; +`; + +const EventItem = styled.li<{ $selected: boolean }>` + display: grid; + grid-template-columns: 54px 1fr; + gap: 12px; + padding: 12px; + border-radius: 16px; + border: 1px solid ${({ $selected }) => ($selected ? 'rgba(255,255,255,0.34)' : 'rgba(255,255,255,0.08)')}; + background: ${({ $selected }) => ($selected ? 'rgba(255,255,255,0.07)' : 'rgba(0,0,0,0.16)')}; + cursor: pointer; + transition: border-color .15s ease, background .15s ease; + + &:hover { + border-color: rgba(255,255,255,0.26); + } +`; + +const EventTime = styled.div` + font-family: var(--font-mono); + font-size: 11px; + color: var(--text-secondary); +`; + +const EventBody = styled.div` + min-width: 0; +`; + +const EventTitle = styled.div` + display: flex; + justify-content: space-between; + gap: 8px; + font-size: 13px; + font-weight: 700; +`; + +const EventDetail = styled.p` + margin: 5px 0 0; + color: var(--text-secondary); + font-size: 12px; +`; + +const TypeBadge = styled.span` + flex-shrink: 0; + font-family: var(--font-mono); + font-size: 9px; + color: var(--text-secondary); +`; + +export default function WorkflowTimeline({ + events, + selectedId, + onSelect, +}: { + events: WorkflowEvent[]; + selectedId: string; + onSelect: (event: WorkflowEvent) => void; +}) { + return ( + +
+ Workflow Timeline + mock stream +
+ + {events.map((event) => ( + onSelect(event)} + > + {event.at} + + + {event.title} + {event.type} + + {event.detail} + + + ))} + +
+ ); +} diff --git a/frontend/lib/control/mock-events.ts b/frontend/lib/control/mock-events.ts new file mode 100644 index 0000000..c7bbeb4 --- /dev/null +++ b/frontend/lib/control/mock-events.ts @@ -0,0 +1,146 @@ +import type { LiveWorkboardState, WorkflowEvent } from './types'; + +export const mockLiveWorkboard: LiveWorkboardState = { + source: 'mock', + workflow: { + id: 'wf-hanarang-live-workboard-v0', + title: '하나랑 Live Workboard v0', + phase: 'building', + summary: '4자매가 작업하는 과정을 메인 화면에서 실시간으로 보여주는 첫 버전', + planPath: '.plans/rebuild-from-scratch/PLAN.md', + updatedAt: '방금 전', + source: 'mock', + }, + sisters: [ + { + id: 'harang', + name: '하랑이', + role: 'Planner', + color: '#8bd3ff', + phase: 'planning', + status: 'waiting', + currentTask: 'PLAN.md 승인 범위 정리 완료', + lastEvent: '나랑이에게 v0 구현 handoff 전달', + nextHandoff: '구현 중 scope 변경 생기면 플랜 보강', + progress: 100, + mode: 'role-skill', + }, + { + id: 'narang', + name: '나랑이', + role: 'Builder', + color: '#ffcc66', + phase: 'building', + status: 'working', + currentTask: 'Live Workboard mock UI 구현', + lastEvent: 'Sister activity lane 컴포넌트 생성', + nextHandoff: 'lint/build 결과를 다랑이에게 전달', + progress: 58, + mode: 'role-skill', + }, + { + id: 'darang', + name: '다랑이', + role: 'Reviewer', + color: '#c6a5ff', + phase: 'reviewing', + status: 'idle', + currentTask: '구현 완료 대기', + lastEvent: 'QA checklist 준비', + nextHandoff: 'spec compliance review 시작', + progress: 20, + mode: 'role-skill', + }, + { + id: 'erang', + name: '이랑이', + role: 'Infra / Orchestrator', + color: '#ff8f70', + phase: 'waiting_approval', + status: 'thinking', + currentTask: '승인 게이트와 배포 위험 분리', + lastEvent: 'DB/배포 변경 없음 확인', + nextHandoff: '다랑이 PASS 후 자기야에게 배포 승인 요청', + progress: 42, + mode: 'main', + }, + ], + approvals: [ + { + id: 'approval-v0-local', + title: 'v0 로컬 UI 구현', + risk: 'L2', + status: 'not-required', + }, + { + id: 'approval-deploy', + title: 'production 배포', + risk: 'L3', + status: 'waiting', + }, + { + id: 'approval-proxmox-write', + title: 'Proxmox write API / restart action', + risk: 'L4', + status: 'blocked', + }, + ], + events: [ + { + id: 'evt-001', + at: '13:04', + sister: 'harang', + type: 'PLAN_CREATED', + title: '재구축 PLAN 작성', + detail: '메인 서비스를 4 Sisters Live Workboard로 재정의', + workflowId: 'wf-hanarang-live-workboard-v0', + source: 'mock', + }, + { + id: 'evt-002', + at: '13:09', + sister: 'erang', + type: 'APPROVAL_NEEDED', + title: 'Gate A 승인 확인', + detail: '자기야가 “그거 먼저 만들어보자”로 v0 구현 승인', + workflowId: 'wf-hanarang-live-workboard-v0', + source: 'mock', + }, + { + id: 'evt-003', + at: '13:12', + sister: 'narang', + type: 'BUILD_STARTED', + title: 'Frontend v0 구현 시작', + detail: 'mock event stream 기반 lane/timeline/focus UI 생성', + workflowId: 'wf-hanarang-live-workboard-v0', + source: 'mock', + }, + { + id: 'evt-004', + at: '13:15', + sister: 'narang', + type: 'FILES_CHANGED', + title: 'Live Workboard 컴포넌트 추가', + detail: 'SisterActivityLane, WorkflowTimeline, ActiveTaskFocus', + workflowId: 'wf-hanarang-live-workboard-v0', + source: 'mock', + }, + { + id: 'evt-005', + at: 'next', + sister: 'darang', + type: 'REVIEW_REQUESTED', + title: '다랑이 리뷰 예정', + detail: 'lint/build 결과 확인 후 spec compliance review', + workflowId: 'wf-hanarang-live-workboard-v0', + source: 'mock', + }, + ], +}; + +export function rotateEvents(events: WorkflowEvent[], tick: number): WorkflowEvent[] { + if (events.length === 0) return events; + const offset = tick % events.length; + return [...events.slice(offset), ...events.slice(0, offset)]; +} diff --git a/frontend/lib/control/types.ts b/frontend/lib/control/types.ts new file mode 100644 index 0000000..da030f9 --- /dev/null +++ b/frontend/lib/control/types.ts @@ -0,0 +1,75 @@ +export type SourceKind = 'live' | 'snapshot' | 'mock' | 'fallback'; + +export type SisterId = 'harang' | 'narang' | 'darang' | 'erang'; + +export type WorkflowPhase = + | 'planning' + | 'building' + | 'reviewing' + | 'waiting_approval' + | 'deploying' + | 'blocked' + | 'done'; + +export type WorkflowEventType = + | 'PLAN_CREATED' + | 'HANDOFF_SENT' + | 'BUILD_STARTED' + | 'FILES_CHANGED' + | 'TEST_RUNNING' + | 'TEST_PASSED' + | 'REVIEW_REQUESTED' + | 'QA_BLOCKED' + | 'APPROVAL_NEEDED' + | 'INFRA_CHECKED' + | 'DEPLOY_READY'; + +export type SisterLaneState = { + id: SisterId; + name: string; + role: string; + color: string; + phase: WorkflowPhase; + status: 'idle' | 'thinking' | 'working' | 'reviewing' | 'blocked' | 'waiting'; + currentTask: string; + lastEvent: string; + nextHandoff: string; + progress: number; + mode: 'main' | 'role-skill' | 'gateway' | 'standby'; +}; + +export type WorkflowEvent = { + id: string; + at: string; + sister: SisterId; + type: WorkflowEventType; + title: string; + detail: string; + workflowId: string; + source: SourceKind; +}; + +export type ActiveWorkflow = { + id: string; + title: string; + phase: WorkflowPhase; + summary: string; + planPath: string; + updatedAt: string; + source: SourceKind; +}; + +export type ApprovalItem = { + id: string; + title: string; + risk: 'L1' | 'L2' | 'L3' | 'L4'; + status: 'waiting' | 'not-required' | 'blocked'; +}; + +export type LiveWorkboardState = { + workflow: ActiveWorkflow; + sisters: SisterLaneState[]; + events: WorkflowEvent[]; + approvals: ApprovalItem[]; + source: SourceKind; +};