diff --git a/AGENTS.md b/AGENTS.md index ac82795..03c4fa9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -111,6 +111,83 @@ Four Docker Compose services: 3. Verify with `get_screenshot()` 4. Then implement the code changes +## GitHub Project Task Workflow + +When the user says **"do tasks"**, **"pick up tasks"**, **"work on ready tasks"**, or similar — follow this workflow: + +### 1. Parse tasks from Ready column + +```bash +# Get all items in Ready status +gh project item-list 2 --owner @me --format json | python3 -c " +import json, sys +data = json.load(sys.stdin) +for item in data.get('items', []): + if item.get('status') == 'Ready': + print(f\"#{item.get('number', '?')} — {item['title']} (Size: {item.get('size', '?')}, Priority: {item.get('priority', '?')})\")" +``` + +Present the list to the user and ask which tasks to implement (or implement all if user says so). + +### 2. Plan implementation + +Enter plan mode. For each task: +- Read the GitHub issue body (`gh issue view `) +- Identify affected files and dependencies between tasks +- Determine implementation order (simplest first, dependencies respected) + +### 3. Implement each task + +For each task being worked on, **first** move it to "In progress": + +```bash +# Move to "In progress" BEFORE starting implementation +gh project item-edit --project-id PVT_kwHOAcEVs84BR7UH --id \ + --field-id PVTSSF_lAHOAcEVs84BR7UHzg_nCMQ --single-select-option-id e56656c8 +``` + +Then: +- Implement the changes following existing code patterns +- Add tests in `tests/test_mcp_tools.py` and/or `tests/test_ui_api.py` +- Run tests after each task: `.venv/bin/pytest tests/ -v -x` +- Update docs (`README.md`, `docs/tool-reference.md`, `AGENTS.md`) if needed + +### 4. Move to "In review" + +After implementation + tests pass, move the task to **In review** (NOT Done): + +```bash +# Move to "In review" +gh project item-edit --project-id PVT_kwHOAcEVs84BR7UH --id \ + --field-id PVTSSF_lAHOAcEVs84BR7UHzg_nCMQ --single-select-option-id 65241aaf +``` + +Do NOT close the issue yet. The user will verify and move to Done / close manually. + +### 5. Project board column reference + +| Column | Option ID | Meaning | +|--------|-----------|---------| +| Backlog | `bf802e7a` | Not prioritized yet | +| Ready | `83e6dfa4` | Prioritized, ready to pick up | +| In progress | `e56656c8` | Currently being worked on | +| In review | `65241aaf` | Implemented, needs user verification before Done | +| Done | `d0415fde` | Verified and complete | + +**Project ID:** `PVT_kwHOAcEVs84BR7UH` +**Status field ID:** `PVTSSF_lAHOAcEVs84BR7UHzg_nCMQ` + +### Getting item IDs + +```bash +# Find item IDs for specific issues +gh project item-list 2 --owner @me --format json | python3 -c " +import json, sys +data = json.load(sys.stdin) +for item in data.get('items', []): + print(f\"{item['id']} #{item.get('number', '?')} {item['title']} [{item.get('status', '?')}]\")" +``` + ## Common Tasks **Adding a new MCP tool:** @@ -165,6 +242,8 @@ Always work on **feature branches**, never commit directly to `main` or `multius # 1. Create branch from main git checkout -b feature/my-feature main +ONLY AFTER USER APPROVE + # 2. Make changes, commit git add git commit -m "Description of changes" @@ -183,7 +262,12 @@ gh pr create --base main --title "Short title" --body "..." ## Critical Rules -- **NEVER DROP THE DATABASE.** Do not run `DROP SCHEMA`, `DROP DATABASE`, `DROP TABLE` or any destructive SQL on the PRDforge PostgreSQL database. It contains user project data (sections, revisions, dependencies, comments) that cannot be recreated. For database restores, use `psql < backup.sql` directly — never drop-and-restore. Always ask the user before any destructive database operation. +- **BACKUP BEFORE ANY DATABASE CHANGES.** The PRDforge database is a live service storing user PRD projects, sections, revisions, comments, and dependencies. Before ANY operation that touches the database (schema migrations, ALTER TABLE, column type changes, docker compose down -v, volume operations), **create a backup first**: + ```bash + docker compose exec postgres pg_dump -U prdforge prdforge > backup_$(date +%Y%m%d_%H%M%S).sql + ``` + This is non-negotiable. Lost data cannot be recreated — PRD content is user-authored. +- **NEVER DROP THE DATABASE.** Do not run `DROP SCHEMA`, `DROP DATABASE`, `DROP TABLE`, `docker compose down -v`, or any destructive SQL/Docker command. For schema changes, use `ALTER TABLE` with `IF NOT EXISTS` guards. For restores, use `psql < backup.sql` — never drop-and-restore. Always ask the user before any destructive database operation. - **Never add AI/agent signatures to git commits.** No "Co-Authored-By: Claude", "Generated by AI", etc. - **Never install packages globally.** Always use `uvx` or a virtual environment (`.venv`). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..61872d9 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,129 @@ +# Contributing to PRDforge + +## Prerequisites + +- **Docker** (Docker Desktop or Docker Engine + Compose) +- **Python 3.11+** (for backend/MCP server development) +- **Node 22+** and **Yarn** (for frontend development) + +## Quick Start + +```bash +git clone && cd PRDforge +./install.sh +``` + +This starts all services (PostgreSQL, MCP server, API, Frontend, Redis) and seeds a sample project. + +## Local Development + +### Backend (API + MCP Server) + +Services run in Docker. Watch logs: + +```bash +docker compose logs -f python-api mcp-server +``` + +To iterate on Python code, the containers mount source directories — changes are picked up on restart: + +```bash +docker compose restart python-api mcp-server +``` + +### Frontend + +```bash +cd frontend +yarn install +yarn dev +``` + +Runs on http://localhost:3000 with hot reload. + +### Database + +PostgreSQL runs in Docker on port 5432 (or next available if taken). + +Connect directly: + +```bash +docker compose exec postgres psql -U prdforge -d prdforge +``` + +## Running Tests + +### Backend tests (MCP + API) + +```bash +# Requires running PostgreSQL (docker compose up postgres -d) +pytest tests/ -v +``` + +### Frontend tests + +```bash +cd frontend +yarn lint +yarn typecheck +yarn test --run +``` + +## Database Migrations + +SQL files live in `db/` with numbered prefixes (`01_init.sql`, `02_comments.sql`, etc.). + +**Pattern for new migrations:** +- Use `IF NOT EXISTS` / `DO $$ ... $$` blocks for idempotency +- Name: `NN_description.sql` where NN is the next number +- Test locally before committing + +## Adding MCP Tools + +All MCP tools are in `mcp_server/server.py`. Follow the existing pattern: + +```python +@mcp.tool( + annotations={"readOnlyHint": False, "destructiveHint": False, "idempotentHint": True, "openWorldHint": False} +) +async def prd_your_tool(project: str, ...) -> str: + """Docstring shown to Claude — keep it concise.""" + try: + pool = await get_pool() + pid = await resolve_project_id(pool, project) + if not pid: + return err(f"project '{project}' not found") + + # ... logic ... + + return ok({...}) + except Exception as e: + logger.error("prd_your_tool: %s", e) + return err(str(e)) +``` + +**Transaction rule:** Inside `conn.transaction()`, never `return err(...)` — always `raise ValueError(msg)`. Catch outside to return `err()`. This ensures auto-rollback on any validation failure. + +After adding a tool: +1. Add tests in `tests/test_mcp_tools.py` +2. Update tool count in `README.md` and `docs/tool-reference.md` +3. Add entry in `docs/tool-reference.md` tool index + +## Code Style + +- Python: standard library formatting, type hints where useful +- Frontend: ESLint + TypeScript strict mode (`yarn lint && yarn typecheck`) +- No AI/agent signatures in git commits (no "Co-Authored-By: Claude", "Generated by AI", etc.) + +## Commit Guidelines + +- Write clear, concise commit messages +- No AI-generated signatures or attributions +- One logical change per commit + +## Pull Request Process + +1. Create a feature branch from `main` +2. Ensure all tests pass (`pytest tests/ -v` and `cd frontend && yarn lint && yarn typecheck && yarn test --run`) +3. Update documentation if adding/changing tools +4. Open PR with description of changes diff --git a/README.md b/README.md index eb58370..8f1491a 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ![PRDforge Demo](demo.gif) -PRD Forge splits your product requirements into independently addressable sections stored in PostgreSQL, then gives Claude surgical read/write access through 31 MCP tools. The result: edits that used to burn ~15,000 tokens now cost 500-2,000 — an **85-95% reduction** in context per operation. +PRD Forge splits your product requirements into independently addressable sections stored in PostgreSQL, then gives Claude surgical read/write access through 34 MCP tools. The result: edits that used to burn ~15,000 tokens now cost 500-2,000 — an **85-95% reduction** in context per operation. ## The Problem @@ -22,7 +22,7 @@ Each section stores both its full **content** and a short **summary** (1-3 sente ## Features -- **31 MCP tools** — read, write, search, import/export, manage dependencies, track revisions, resolve comments +- **34 MCP tools** — read, write, search, import/export, manage dependencies, track revisions, resolve comments - **Multi-user auth** — Better Auth (email/password + Google OAuth), 5 roles (owner/admin/editor/commenter/viewer), org-scoped access control - **Real-time collaboration** — WebSocket presence, live section updates across clients - **Project templates** — start with Blank, SaaS MVP, Mobile App, or API Design — pre-built section structures with starter content @@ -52,7 +52,7 @@ Five Docker services: | Service | Stack | Port | Purpose | |---------|-------|------|---------| | **PostgreSQL 16** | 15+ tables, 2 views | 5432 | Source of truth | -| **MCP Server** | FastMCP / Python | 8080 | 31 tools for Claude | +| **MCP Server** | FastMCP / Python | 8080 | 34 tools for Claude | | **Python API** | FastAPI | 8088 | REST backend (projects, sections, chat, auth, audit) | | **Frontend** | Next.js 15, React 19, Tailwind v4, shadcn/ui, Better Auth | 3000 | Web UI | | **Redis 7** | — | 6379 | WebSocket token uniqueness, real-time pub/sub | @@ -165,7 +165,7 @@ Chat is an experimental feature, disabled by default. Enable per-project in **Se ## MCP Tools Reference -31 MCP tools across 10 groups: project management, section CRUD, dependencies, comments, context/search, revisions, import/export, batch operations, token stats, and settings. +34 MCP tools across 10 groups: project management, section CRUD, dependencies, comments, context/search, revisions, import/export, batch operations, token stats, and settings. See **[docs/tool-reference.md](docs/tool-reference.md)** for the full tool table and usage examples. @@ -205,7 +205,7 @@ PRDforge/ ├── docker-compose.yml ├── frontend/ # Next.js 15 app (React 19, Tailwind v4, shadcn/ui) ├── api/ # FastAPI backend (REST, chat, auth, WebSocket) -├── mcp_server/ # FastMCP server (31 tools, stdio + HTTP) +├── mcp_server/ # FastMCP server (34 tools, stdio + HTTP) ├── shared/ # Shared Python modules (settings, constants, templates) ├── db/ # PostgreSQL schema migrations (13 files) ├── tests/ # pytest test suite @@ -246,6 +246,10 @@ docker exec prdforge-postgres-1 pg_dump -U prdforge prdforge > backup.sql docker compose down -v && docker compose up -d ``` +## Contributing + +See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, testing, and contribution guidelines. + ## License MIT diff --git a/TODO.md b/TODO.md deleted file mode 100644 index 2bf6d03..0000000 --- a/TODO.md +++ /dev/null @@ -1,91 +0,0 @@ -# PRDforge TODO - -## High Impact, Moderate Effort – Do Next - -- [ ] Agent personas for chat — per-project `system_prompt` in settings + per-section `agent_prompt` override. Ship 5-6 presets (PRD Architect, Technical Reviewer, UX/Design, QA Strategist, Executive Summary). Users pick preset or write custom. Passed as system message to the chat provider. Section-level prompt overrides project-level when chatting in that section context. *(Base system prompt exists in `_chat_system_prompt`, needs UI + per-project/section override)* - -## Medium Impact, Lower Effort – Quick Wins - -- [ ] CONTRIBUTING.md — how to run tests locally, code style expectations, PR process -- [x] GitHub repo metadata — description, topics/tags (`mcp`, `claude`, `prd`, `product-requirements`, `ai-tools`, `developer-tools`, `mcp-server`, `nextjs`, `fastapi`, `docker`) -- [ ] GitHub repo website URL — set homepage once public site is ready -- [ ] `prd_diff_sections` tool — unified diff between two revisions of a section, avoids loading both and diffing manually -- [ ] UI Playwright tests - -## Medium Impact, Higher Effort – Plan For These - -- [ ] Webhooks / notifications — POST to a configured URL on section update for Slack/Discord/pipeline integration -- [ ] Export to Google Docs / Notion — push assembled PRD back to where stakeholders read it (Notion API is more reasonable than Google Docs) -- [ ] Embeddings-based dependency suggestions — store embeddings in pgvector, compute cosine similarity to catch semantic relationships that FTS keyword overlap misses -- [ ] Version tagging / snapshots — `prd_tag_version` tool that snapshots all current section revision numbers under a named tag for milestone tracking -- [ ] Google OAuth provider — Better Auth config ready, needs Google Cloud Console credentials - -## Lower Priority – Worth Tracking - -- [ ] `prd_import_url` tool — fetch public Google Docs, GitHub markdown, or raw URLs and import as sections. Private docs deferred (each provider needs its own OAuth — not worth the complexity for an import tool). Public-only version is ~2h of work. -- [ ] Section ordering visualization — Gantt-style view based on `blocks` dependency type for timeline sections -- [ ] Prometheus `/metrics` endpoint — request counts, latency histograms, DB connection pool stats -- [ ] claude.ai MCP marketplace listing — be ready when Anthropic opens MCP server discovery -- [ ] Replace heuristic markdown import parser with `markdown-it-py` AST parser -- [ ] Support `### ` (h3) splitting in import for nested section hierarchies -- [ ] Add `prd_merge_sections` tool (combine two sections into one) -- [ ] Add `prd_reorder_sections` tool (bulk sort_order update) -- [x] ~~Export as PDF via headless browser~~ Export as PDF via browser print dialog -- [ ] MCP auth for remote Claude clients (SSH tunnel or authenticated ingress) -- [ ] move to tasks github projects -- [ ] add Playwright preview autoupdate (CI for PR + agents.md for agent instructions on which sections to observe) - -## Done - -- [x] Demo GIF/video recording script (Playwright-based, `scripts/record_demo.py`) -- [x] Infrastructure files (.gitignore, .env.example, docker-compose.yml, Dockerfiles, requirements) -- [x] Database schema with composite FK cross-project guard -- [x] Seed data (SnapHabit, 12 sections, 12 dependencies) -- [x] MCP server — 31 tools with atomic revision-before-update -- [x] Web UI — dark theme SPA with vendored marked.js -- [x] Test suite (test_mcp_tools.py, test_ui_api.py) -- [x] README.md with architecture diagrams -- [x] AGENTS.md with agent instructions -- [x] Claude Desktop stdio config (working) -- [x] Claude Code HTTP config -- [x] Pencil UI mockup design -- [x] One-command install script (`install.sh`) -- [x] Tag multi-selector dropdown with colored chips and search -- [x] Interactive force-directed dependency graph in main panel -- [x] `prd_list_comments` tool for context-efficient comment scanning -- [x] Editable comments (PATCH endpoint + inline edit UI) -- [x] Graph node popups with section summary, click-to-open -- [x] Full test suite fixed (70 unit tests + 9 smoke tests passing) -- [x] Concurrent revision writes verified (SELECT FOR UPDATE correctness) -- [x] MCP transport path verified (`/mcp/` streamable-http working) -- [x] `test_smoke.py` — CI contract tests (MCP liveness, DB readiness, UI endpoints, seed data) -- [x] Container images pinned by digest (postgres:16-alpine, python:3.11-slim) -- [x] Google Fonts vendored locally (Inter, JetBrains Mono in ui/static/fonts/) -- [x] Light theme / dark-light toggle in UI nav rail -- [x] Docker build pipeline (GitHub Actions → ghcr.io) + `docker-compose.prod.yml` + install.sh pull support -- [x] Token savings dashboard — Grafana-style with recharts, area/bar/pie/gauge, session-based honest savings math, section heatmap -- [x] Health checks in docker-compose — all services (postgres, mcp-server, redis) have healthchecks with depends_on conditions -- [x] Multi-arch Docker builds — `linux/amd64,linux/arm64` in GH Actions -- [x] Multi-user auth — Better Auth (email/password), closed sign-up, first-user bootstrap -- [x] RBAC — project_members with 5 roles (owner/admin/editor/commenter/viewer), permission middleware -- [x] Next.js frontend — React 19, Tailwind v4, shadcn/ui, replaces server-rendered HTML -- [x] Real-time WebSocket — presence tracking, event broadcasting via Redis pub/sub -- [x] Optimistic locking — expected_revision on section PATCH, 409 Conflict with details -- [x] MCP activity tracking — 12 mutating tools logged to mcp_activity table -- [x] Chat with streaming — SSE, tool calls display, selection context, file attachments, stop button -- [x] Audit events table — project + user indexed -- [x] Password reset flow — admin-generated tokens, no email -- [x] Member management — add/remove/change role via API + UI -- [x] Dependency graph — dual view (force-directed SVG graph + list with type badges), click popup, drag nodes, curved colored arrows -- [x] Structured error handling — 9 error codes, frontend error boundaries -- [x] Comments — text selection anchoring, resolve/reopen toggle, edit/delete, existing comment detection -- [x] Section status management — clickable dropdown to change status -- [x] Multi-user chat threads — per-project + per-section, chat_type column -- [x] Org-level encrypted API key — Fernet/AES-256 -- [x] Wider chat section — 40% viewport width, min 480px, max 700px -- [x] Honest token savings math — section_access_log with session-based dedup, coverage fractions (full/summary/snippet), 30-min session windowing -- [x] Section templates — SaaS MVP, Mobile App project templates with seeded sections and dependencies -- [x] Notes accordion — per-section collapsible notes with inline editing -- [x] README + Playwright demo preview — `scripts/record_demo.py` rewritten, demo.gif regenerated -- [x] Redis jti uniqueness for WS tokens — SET NX EX replay protection in websocket handler -- [x] Security hardening — member endpoint auth, ws-token auth from session, schema-qualified `to_regclass`, default secret warnings diff --git a/api/app.py b/api/app.py index c8f5f3c..b081d2a 100644 --- a/api/app.py +++ b/api/app.py @@ -1855,8 +1855,14 @@ async def get_token_stats(slug: str): best_session = max((s["savings_pct"] for s in sessions), default=0) avg_sections_per_session = round(sum(s["sections_touched"] for s in sessions) / session_count, 1) if session_count > 0 else 0 total_unique_loaded = sum(s["unique_loaded_words"] for s in sessions) - total_loaded_tokens = int(total_unique_loaded * 1.3) - total_saved_tokens = max(0, full_doc_tokens * session_count - total_loaded_tokens) + # Use cumulative per-operation totals from token_estimates + cumulative = await pool.fetchrow(""" + SELECT COALESCE(SUM(full_doc_tokens), 0) AS total_full, + COALESCE(SUM(loaded_tokens), 0) AS total_loaded + FROM token_estimates WHERE project_id = $1 + """, pid) + total_loaded_tokens = cumulative["total_loaded"] + total_saved_tokens = max(0, cumulative["total_full"] - total_loaded_tokens) # Section heatmap — how often each section is accessed heatmap = await pool.fetch(""" @@ -2425,8 +2431,10 @@ async def list_project_members(slug: str, request: Request): if not proj: return JSONResponse({"error": f"project '{slug}' not found"}, 404) rows = await pool.fetch(""" - SELECT pm.id, pm.user_id, pm.role, pm.created_at, pm.updated_at + SELECT pm.id, pm.user_id, pm.role, pm.created_at, pm.updated_at, + u.name, u.email FROM project_members pm + LEFT JOIN "user" u ON u.id = pm.user_id WHERE pm.project_id = $1 ORDER BY pm.created_at """, proj["id"]) diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index b3a6dbf..27f1272 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -84,7 +84,7 @@ services: frontend: image: ghcr.io/tommass/prdforge-frontend:${PRDFORGE_VERSION:-latest} ports: - - "3000:3000" + - "127.0.0.1:3000:3000" # use a reverse proxy for external access environment: NEXT_PUBLIC_API_URL: "" depends_on: diff --git a/docker-compose.yml b/docker-compose.yml index b0d060a..3d4b6c2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -95,7 +95,7 @@ services: context: . dockerfile: frontend/Dockerfile ports: - - "3000:3000" + - "127.0.0.1:3000:3000" environment: DATABASE_URL: postgresql://${POSTGRES_USER:-prdforge}:${POSTGRES_PASSWORD:-prdforge}@postgres:5432/${POSTGRES_DB:-prdforge} BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET:-dev-better-auth-secret-change-in-production} diff --git a/docs/tool-reference.md b/docs/tool-reference.md index 7c0a418..c30bdd8 100644 --- a/docs/tool-reference.md +++ b/docs/tool-reference.md @@ -8,7 +8,7 @@ ## Overview -PRDforge exposes **32 MCP tools** for reading, writing, searching, and managing PRD sections. Tools are designed to minimize context window usage — prefer lightweight tools (`prd_list_sections`, `prd_get_overview`) for navigation and reserve full reads (`prd_read_section`) for editing. +PRDforge exposes **34 MCP tools** for reading, writing, searching, and managing PRD sections. Tools are designed to minimize context window usage — prefer lightweight tools (`prd_list_sections`, `prd_get_overview`) for navigation and reserve full reads (`prd_read_section`) for editing. ## Table of Contents @@ -32,7 +32,9 @@ PRDforge exposes **32 MCP tools** for reading, writing, searching, and managing | `prd_update_section` | Section | Update fields, auto-revision on content change, atomic comment resolve | — | | `prd_delete_section` | Section | Delete section (warns about dependencies) | — | | `prd_move_section` | Section | Change sort_order or parent section | — | +| `prd_reorder_sections` | Section | Reorder sections by slug list (unlisted keep relative order) | — | | `prd_duplicate_section` | Section | Copy section with new slug | — | +| `prd_merge_sections` | Section | Merge source into target (content, deps, comments, children) | — | | `prd_add_dependency` | Deps | Add/update dependency link (idempotent) | — | | `prd_remove_dependency` | Deps | Remove a dependency link | — | | `prd_suggest_dependencies` | Deps | Auto-suggest deps via content similarity (TF-IDF) | 200 | @@ -52,10 +54,11 @@ PRDforge exposes **32 MCP tools** for reading, writing, searching, and managing | `prd_rollback_section` | Revision | Rollback to a previous revision (current saved as backup) | — | | `prd_export_markdown` | Export | Export full document as assembled markdown | 15000+ | | `prd_import_markdown` | Import | Import from markdown (configurable heading level or delimiter) | — | +| `prd_import_url` | Import | Import markdown from URL (SSRF-protected, Google Docs/GitHub support) | — | | `prd_bulk_status` | Batch | Update status for multiple sections at once | — | **Read tools** (return data, consume tokens): 14 tools -**Write tools** (mutate data, logged to `mcp_activity`): 12 tools +**Write tools** (mutate data, logged to `mcp_activity`): 14 tools **Hybrid tools** (read + compute): 6 tools (search, suggest, stats, changelog, export, import) --- diff --git a/frontend/src/app/api/orgs/[slug]/api-key/route.ts b/frontend/src/app/api/orgs/[slug]/api-key/route.ts index ba5a741..38c548a 100644 --- a/frontend/src/app/api/orgs/[slug]/api-key/route.ts +++ b/frontend/src/app/api/orgs/[slug]/api-key/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; import { prisma } from "@/lib/db"; +import { requireOrgAdmin } from "@/lib/require-org-admin"; import crypto from "crypto"; const ENCRYPTION_SECRET = process.env.API_KEY_ENCRYPTION_SECRET || "dev-encryption-key-change-in-prod"; @@ -18,6 +19,10 @@ export async function PUT( { params }: { params: Promise<{ slug: string }> } ) { const { slug } = await params; + + const check = await requireOrgAdmin(slug); + if ("error" in check) return check.error; + const body = await req.json(); const apiKey = body.api_key?.trim(); diff --git a/frontend/src/app/api/orgs/[slug]/members/[id]/reset-password/route.ts b/frontend/src/app/api/orgs/[slug]/members/[id]/reset-password/route.ts index 0b76639..fc6b545 100644 --- a/frontend/src/app/api/orgs/[slug]/members/[id]/reset-password/route.ts +++ b/frontend/src/app/api/orgs/[slug]/members/[id]/reset-password/route.ts @@ -1,12 +1,16 @@ import { NextRequest, NextResponse } from "next/server"; import { prisma } from "@/lib/db"; +import { requireOrgAdmin } from "@/lib/require-org-admin"; import crypto from "crypto"; export async function POST( req: NextRequest, { params }: { params: Promise<{ slug: string; id: string }> } ) { - const { id: userId } = await params; + const { slug, id: userId } = await params; + + const check = await requireOrgAdmin(slug); + if ("error" in check) return check.error; // Generate a secure reset token const token = crypto.randomBytes(32).toString("hex"); diff --git a/frontend/src/app/api/orgs/[slug]/members/create/route.ts b/frontend/src/app/api/orgs/[slug]/members/create/route.ts index a554c4a..4665bba 100644 --- a/frontend/src/app/api/orgs/[slug]/members/create/route.ts +++ b/frontend/src/app/api/orgs/[slug]/members/create/route.ts @@ -1,11 +1,16 @@ import { NextRequest, NextResponse } from "next/server"; import { auth } from "@/lib/auth"; +import { requireOrgAdmin } from "@/lib/require-org-admin"; export async function POST( req: NextRequest, { params }: { params: Promise<{ slug: string }> } ) { const { slug } = await params; + + const check = await requireOrgAdmin(slug); + if ("error" in check) return check.error; + const body = await req.json(); const { name, email, password, role } = body; diff --git a/frontend/src/app/projects/[slug]/page.tsx b/frontend/src/app/projects/[slug]/page.tsx index e85dbf0..b0ca119 100644 --- a/frontend/src/app/projects/[slug]/page.tsx +++ b/frontend/src/app/projects/[slug]/page.tsx @@ -233,7 +233,12 @@ export default function ProjectDetailPage() { ]); while (true) { - const { done, value } = await reader.read(); + // Timeout: if no data for 2 minutes, break out + const readPromise = reader.read(); + const timeout = new Promise<{ done: true; value: undefined }>((resolve) => + setTimeout(() => resolve({ done: true, value: undefined }), 120_000) + ); + const { done, value } = await Promise.race([readPromise, timeout]); if (done) break; buffer += decoder.decode(value, { stream: true }); @@ -279,6 +284,14 @@ export default function ProjectDetailPage() { setStreamStatus("approval"); streamStatusRef.current = "approval"; // Will be handled after stream ends via metadata + } else if (eventType === "error") { + // Backend error — show to user + const errMsg = parsed.error || "Chat stream failed"; + setChatMessages((prev) => + prev.map((m) => + m.id === assistantId ? { ...m, content: `Error: ${errMsg}` } : m + ) + ); } else if (eventType === "done" && parsed.message) { // Final message — use the server's complete content + tool events assistantContent = parsed.message.content; @@ -400,6 +413,13 @@ export default function ProjectDetailPage() { ); } else if (eventType === "tool" && parsed.name) { setStreamTools((prev) => [...prev, parsed.name]); + } else if (eventType === "error") { + const errMsg = parsed.error || "Chat stream failed"; + setChatMessages((prev) => + prev.map((m) => + m.id === assistantId ? { ...m, content: `Error: ${errMsg}` } : m + ) + ); } else if (eventType === "done" && parsed.message) { assistantContent = parsed.message.content; const meta = parsed.message.metadata || {}; diff --git a/frontend/src/app/projects/[slug]/settings/page.tsx b/frontend/src/app/projects/[slug]/settings/page.tsx index 250e9af..d35af74 100644 --- a/frontend/src/app/projects/[slug]/settings/page.tsx +++ b/frontend/src/app/projects/[slug]/settings/page.tsx @@ -22,10 +22,22 @@ import { SelectValue, } from "@/components/ui/select"; import { LoadingOverlay } from "@/components/loading-overlay"; +import { MemberManager } from "@/components/member-manager"; const PROVIDERS = ["claude_cli", "anthropic_api"] as const; const MODELS = ["sonnet", "opus", "haiku"] as const; +const ORG_SLUG = "default"; + +interface Member { + id: string; + user_id: string; + role: string; + name?: string; + email?: string; + created_at: string; +} + interface Settings { claude_comment_replies: boolean; chat_enabled: boolean; @@ -54,15 +66,18 @@ export default function SettingsPage() { const [loggingIn, setLoggingIn] = useState(false); const [showCodeInput, setShowCodeInput] = useState(false); const [loginCode, setLoginCode] = useState(""); + const [members, setMembers] = useState([]); useEffect(() => { Promise.all([ fetch(`/api/projects/${slug}/settings`).then((r) => r.json()), fetch("/api/chat/provider-status").then((r) => r.json()), + fetch(`/api/projects/${slug}/members`).then((r) => r.ok ? r.json() : []), ]) - .then(([s, p]) => { + .then(([s, p, m]) => { setSettings(s); setProviderStatus(p); + setMembers(m); }) .catch(console.error) .finally(() => setLoading(false)); @@ -170,6 +185,52 @@ export default function SettingsPage() { } }; + const refreshMembers = async () => { + const res = await fetch(`/api/projects/${slug}/members`); + if (res.ok) setMembers(await res.json()); + }; + + const handleAddMember = async (userId: string, role: string) => { + const res = await fetch(`/api/projects/${slug}/members`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ user_id: userId, role }), + }); + if (!res.ok) { + const data = await res.json(); + toast.error(data.error || "Failed to add member"); + throw new Error("Failed to add member"); + } + toast.success("Member added"); + await refreshMembers(); + }; + + const handleRemoveMember = async (userId: string) => { + const res = await fetch(`/api/projects/${slug}/members/${userId}`, { + method: "DELETE", + }); + if (!res.ok) { + toast.error("Failed to remove member"); + return; + } + toast.success("Member removed"); + await refreshMembers(); + }; + + const handleChangeRole = async (userId: string, role: string) => { + const res = await fetch(`/api/projects/${slug}/members`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ user_id: userId, role }), + }); + if (!res.ok) { + toast.error("Failed to change role"); + return; + } + toast.success("Role updated"); + await refreshMembers(); + }; + if (loading) { return (
@@ -434,6 +495,23 @@ export default function SettingsPage() { + {/* Members */} + + + Members + + + + + +
+ +
+ +
+ {tab === "existing" ? ( +
+ + setUserId(e.target.value)} + className="mt-1.5" + /> +
+ ) : ( + <> +
+ + setNewName(e.target.value)} + className="mt-1.5" + /> +
+
+ + setNewEmail(e.target.value)} + className="mt-1.5" + /> +
+
+ + setNewPassword(e.target.value)} + className="mt-1.5" + /> +
+ + )}
+