Files
backnotprop__plannotator/apps/hook/dev-mock-api.ts
T
Michael Ramos a278fdaf77 feat: plan archive browser (#369)
* feat: plan archive browser with in-session sidebar tab (#362)

Add `plannotator archive` subcommand and archive sidebar tab for browsing
saved plan decisions from ~/.plannotator/plans/. Plans show approved/denied
badges and timestamps. In-session mode uses the linked doc overlay pattern
so users can reference old plans while reviewing a current one.

- New archive server (packages/server/archive.ts) following annotate pattern
- New ArchiveBrowser sidebar component, reusable in both contexts
- Archive listing/parsing functions in storage.ts (reads decision snapshots)
- Archive endpoints on plan server for in-session use (/api/archive/plans)
- Remove dead "Other Plans" UI, projectPlans state, /api/plan/history endpoint
- Fix resize handle touch area covering scrollbars in sidebar/main content
- Fix sidebar tab bar overflow when narrow

For provenance purposes, this commit was AI assisted.

* fix: code quality sweep for plan archive

- Remove `as any` cast: add "archive" to SessionInfo.mode union
- Replace inline import() type with proper import for ArchivedPlan
- Replace any[] with ArchivedPlan[] in fetch response types
- Fix infinite re-fetch when archive is empty (use hasFetched ref)
- Cache archive plan list in plan server (avoid re-scanning filesystem)
- Document ResizeHandle side prop behavior
- Remove redundant comment on Viewer archiveInfo prop

For provenance purposes, this commit was AI assisted.

* chore: remove dead marketing components

Step.astro and Landing.astro are unused — landing page inlines
step markup and pages use Base.astro directly.

For provenance purposes, this commit was AI assisted.

* fix: address code review findings for plan archive

- Path traversal: use resolve() + trailing separator guard (matches reference-handlers.ts)
- Thread customPath into in-session archive endpoints via query param
- Sort same-day archive entries by mtime instead of title
- Clear selectedArchiveFile on linked doc back to prevent badge leak
- Hide archive tab in annotate mode (server doesn't serve those endpoints)
- Add targetTab param to useLinkedDoc.open() to preserve calling sidebar tab
- Replace mutable render variable with index-based date grouping

For provenance purposes, this commit was AI assisted.

* refactor: collapse standalone archive server into plan server

Delete packages/server/archive.ts (187 lines) — nearly all duplicated
from the plan server. Add mode:"archive" option to startPlannotatorServer
instead. Fixes two bugs from code review:

- handleArchiveCopy now splits on "# Plan Feedback" marker instead of
  bare "---", preventing truncation at horizontal rules in plan content
- customPath support works in standalone archive mode (was only working
  in-session because the standalone server never received it)

For provenance purposes, this commit was AI assisted.

* refactor: extract useArchive hook from App.tsx

Move archive state (archiveMode, plans, selectedFile, isLoading) and
handlers (select, fetchPlans, done, copy) into a dedicated useArchive
hook. Reduces App.tsx by ~75 lines and makes the archive feature
self-contained.

For provenance purposes, this commit was AI assisted.

* feat: Pi archive parity + eliminate server duplication

Move runtime-agnostic storage, draft, and project functions from
packages/server/ to packages/shared/ — eliminating ~250 lines of
duplicated code in Pi's server.ts. Server package becomes thin
re-exports, preserving all existing import paths.

Add archive mode to Pi's plan review server (mode, routes, waitForDone)
and register /plannotator-archive command in the Pi extension. Consolidate
ArchivedPlan type to single definition in shared/storage.ts.

Simplify archive copy to include full content with feedback.

For provenance purposes, this commit was AI assisted.

* fix: drop -core suffix from Pi shared copies

The -core suffix broke cross-file imports — storage.ts imports from
./project which didn't resolve to project-core.ts. Using the original
filenames (no collision) lets relative imports work naturally.

For provenance purposes, this commit was AI assisted.

* fix: archive custom path bugs, disable sharing, update docs

- Normalize planDir via resolve() in getPlanDir() to handle relative
  paths and trailing slashes in the path traversal guard
- Re-fetch archive plans client-side with cookie-backed customPath
  so standalone archive respects the user's configured save location
- Disable sharing in archive mode (read-only viewer, no need)
- Remove dead /api/plan/history endpoint and listProjectPlans import
  from Pi extension
- Remove dead /api/plan/history mock from dev-mock-api
- Update CLAUDE.md and AGENTS.md: add archive flow, archive API
  endpoints, shared package structure, correct storage location,
  sidebar tab count, remove stale /api/plan/history references
- Update hook server docstring from four to five modes

For provenance purposes, this commit was AI assisted.

* fix: empty archive shows demo content, stale viewer after customPath fetch

- Clear demo markdown when archive opens with no plans (plan: "" was falsy,
  so setMarkdown was never called)
- Remove redundant fetchPlans() from archive init — server already sends
  archivePlans in initial response
- After fetchPlans() resolves with customPath results, auto-select and load
  the first plan into the viewer
- Remove dead listProjectPlans re-export from server barrel

For provenance purposes, this commit was AI assisted.

* refactor: gitignore Pi shared copies, add @generated headers

Pi's copied .ts files (storage, draft, project, feedback-templates,
review-core) are build artifacts generated from packages/shared/. They
looked like editable source files, leading to confusion about which file
to edit. Now gitignored like the HTML copies, with @generated headers
prepended by the build script.

For provenance purposes, this commit was AI assisted.

* fix: generate Pi shared copies in CI before tests

The Pi .ts copies are now gitignored build artifacts. CI needs to
generate them before running tests since server.test.ts transitively
imports them via server.ts.

For provenance purposes, this commit was AI assisted.

* fix: generate Pi shared copies in release pipeline test job

Same fix as test.yml — the Pi .ts copies are gitignored, so the test
job in the release pipeline also needs to generate them before bun test.

For provenance purposes, this commit was AI assisted.

* fix: use block scalar in CI workflow to avoid YAML parse error

The inline `run:` had a colon in the printf string that YAML
interpreted as a mapping key. Switch to `run: |` block scalar.

For provenance purposes, this commit was AI assisted.
2026-03-23 11:31:38 -07:00

245 lines
6.7 KiB
TypeScript

/**
* Vite plugin that mocks plannotator API endpoints for local development.
* Provides plan data with version history so the Versions tab works in dev mode.
*/
import type { Plugin } from 'vite';
// Version 1: earlier draft (shorter, missing sections)
const PLAN_V1 = `# Implementation Plan: Real-time Collaboration
## Overview
Add real-time collaboration features to the editor using WebSocket connections.
## Phase 1: Infrastructure
### WebSocket Server
Set up a WebSocket server to handle concurrent connections:
\`\`\`typescript
const server = new WebSocketServer({ port: 8080 });
server.on('connection', (socket) => {
const sessionId = generateSessionId();
sessions.set(sessionId, socket);
socket.on('message', (data) => {
broadcast(sessionId, data);
});
});
\`\`\`
### Client Connection
- Establish persistent connection on document load
- Implement reconnection logic with exponential backoff
- Handle offline state gracefully
### Database Schema
\`\`\`sql
CREATE TABLE documents (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
title VARCHAR(255) NOT NULL,
content JSONB NOT NULL DEFAULT '{}',
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
\`\`\`
## Phase 2: Operational Transforms
> The key insight is that we need to transform operations against concurrent operations to maintain consistency.
Key requirements:
- Transform insert against insert
- Transform insert against delete
- Transform delete against delete
## Pre-launch Checklist
- [ ] Infrastructure ready
- [ ] WebSocket server deployed
- [ ] Database migrations applied
- [ ] Security audit complete
- [ ] Documentation updated
---
**Target:** Ship MVP in next sprint
`;
// Version 2: expanded (added architecture diagram, more details)
const PLAN_V2 = `# Implementation Plan: Real-time Collaboration
## Overview
Add real-time collaboration features to the editor using WebSocket connections and operational transforms.
### Architecture
\`\`\`mermaid
flowchart LR
subgraph Client["Client Browser"]
UI[React UI] --> OT[OT Engine]
OT <--> WS[WebSocket Client]
end
subgraph Server["Backend"]
WSS[WebSocket Server] <--> OTS[OT Transform]
OTS <--> DB[(PostgreSQL)]
end
WS <--> WSS
\`\`\`
## Phase 1: Infrastructure
### WebSocket Server
Set up a WebSocket server to handle concurrent connections:
\`\`\`typescript
const server = new WebSocketServer({ port: 8080 });
server.on('connection', (socket, request) => {
const sessionId = generateSessionId();
sessions.set(sessionId, socket);
socket.on('message', (data) => {
broadcast(sessionId, data);
});
});
\`\`\`
### Client Connection
- Establish persistent connection on document load
- Initialize WebSocket with authentication token
- Set up heartbeat ping/pong every 30 seconds
- Implement reconnection logic with exponential backoff
- Start with 1 second delay
- Double delay on each retry (max 30 seconds)
- Handle offline state gracefully
- Queue local changes in IndexedDB
- Show offline indicator in UI
### Database Schema
\`\`\`sql
CREATE TABLE documents (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
title VARCHAR(255) NOT NULL,
content JSONB NOT NULL DEFAULT '{}',
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE TABLE collaborators (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
document_id UUID REFERENCES documents(id) ON DELETE CASCADE,
user_id UUID NOT NULL,
role VARCHAR(50) DEFAULT 'editor',
cursor_position JSONB,
last_seen_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX idx_collaborators_document ON collaborators(document_id);
\`\`\`
## Phase 2: Operational Transforms
> The key insight is that we need to transform operations against concurrent operations to maintain consistency.
Key requirements:
- Transform insert against insert
- Same position: use user ID for deterministic ordering
- Different positions: adjust offset of later operation
- Transform insert against delete
- Insert before delete: no change needed
- Insert inside deleted range: special handling required
- Transform delete against delete
- Non-overlapping: adjust positions
- Overlapping: merge or split operations
- Maintain cursor positions across transforms
## Phase 3: UI Updates
1. Show collaborator cursors in real-time
2. Display presence indicators
3. Add conflict resolution UI
4. Implement undo/redo stack per user
## Pre-launch Checklist
- [ ] Infrastructure ready
- [x] WebSocket server deployed
- [x] Database migrations applied
- [ ] Load balancer configured
- [ ] Security audit complete
- [x] Authentication flow reviewed
- [ ] Rate limiting implemented
- [x] Documentation updated
---
**Target:** Ship MVP in next sprint
`;
// Version 3 is the current PLAN_CONTENT from App.tsx (loaded by the editor itself)
// We don't duplicate it here — the editor already has it as the default state.
const now = Date.now();
const versions = [
{ version: 1, timestamp: new Date(now - 3600_000 * 2).toISOString() },
{ version: 2, timestamp: new Date(now - 3600_000).toISOString() },
{ version: 3, timestamp: new Date(now - 60_000).toISOString() },
];
const versionPlans: Record<number, string> = {
1: PLAN_V1,
2: PLAN_V2,
// Version 3 is the current plan — served via /api/plan
};
export function devMockApi(): Plugin {
return {
name: 'plannotator-dev-mock-api',
configureServer(server) {
server.middlewares.use((req, res, next) => {
if (req.url === '/api/plan') {
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({
plan: undefined, // Let editor use its own PLAN_CONTENT
origin: 'claude-code',
previousPlan: PLAN_V2,
versionInfo: { version: 3, totalVersions: 3, project: 'demo' },
sharingEnabled: true,
}));
return;
}
if (req.url === '/api/plan/versions') {
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({
project: 'demo',
slug: 'implementation-plan-real-time-collab',
versions,
}));
return;
}
if (req.url?.startsWith('/api/plan/version?')) {
const url = new URL(req.url, 'http://localhost');
const v = Number(url.searchParams.get('v'));
const plan = versionPlans[v];
if (plan) {
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ plan, version: v }));
} else {
res.statusCode = 404;
res.end(JSON.stringify({ error: 'Version not found' }));
}
return;
}
next();
});
},
};
}