bigwolfe
logging and polish
a5a2c66
# Implementation Tasks: Multi-Tenant Obsidian-Like Docs Viewer
**Feature Branch**: `001-obsidian-docs-viewer`
**Created**: 2025-11-15
**Status**: Ready for Implementation
## Implementation Strategy
**MVP = User Story 1 (AI Agent Writes) + User Story 2 (Human Reads UI)**
The MVP delivers immediate value:
- AI agents (via MCP STDIO) can create and maintain documentation
- Humans can browse, search, and read documentation in the web UI
- Full-text search, wikilinks, tags, and backlinks work end-to-end
**Post-MVP enhancements**:
- User Story 3: Human editing with version conflict detection
- User Story 4: Multi-tenant HF OAuth for production deployment
- User Story 5: Advanced search ranking and index health monitoring
**Progress Update (2025-11-16)**:
- Phase 1 setup complete except T017 (create `data/vaults/`) and T021 (`.env.example`); T024 remains as an outstanding run-step (schema init script prepared but not executed).
- Phase 2 foundational backend/models/types complete.
- Phase 3 MCP backend + prompt tightening complete; see `backend/src/mcp/server.py` for the updated tool contracts.
---
## Phase 1: Setup
**Goal**: Initialize project structure, dependencies, and database schema.
- [x] [T001] Create project directory structure at /home/wolfe/Projects/Document-MCP
- [x] [T002] [P] Create backend/src/models/ directory and __init__.py
- [x] [T003] [P] Create backend/src/services/ directory and __init__.py
- [x] [T004] [P] Create backend/src/api/routes/ directory and __init__.py
- [x] [T005] [P] Create backend/src/api/middleware/ directory and __init__.py
- [x] [T006] [P] Create backend/src/mcp/ directory and __init__.py
- [x] [T007] [P] Create backend/tests/unit/ directory and __init__.py
- [x] [T008] [P] Create backend/tests/integration/ directory and __init__.py
- [x] [T009] [P] Create backend/tests/contract/ directory and __init__.py
- [x] [T010] [P] Create frontend/src/components/ui/ directory
- [x] [T011] [P] Create frontend/src/pages/ directory
- [x] [T012] [P] Create frontend/src/services/ directory
- [x] [T013] [P] Create frontend/src/lib/ directory
- [x] [T014] [P] Create frontend/src/types/ directory
- [x] [T015] [P] Create frontend/tests/unit/ directory
- [x] [T016] [P] Create frontend/tests/e2e/ directory
- [ ] [T017] [P] Create data/vaults/ directory for runtime vault storage
- [x] [T018] Create backend/pyproject.toml with dependencies: fastapi, fastmcp, python-frontmatter, pyjwt, huggingface_hub, uvicorn
- [x] [T019] Create frontend/package.json with dependencies: react, vite, typescript, shadcn/ui, react-markdown
- [x] [T020] Create frontend/vite.config.ts with proxy to backend API
- [ ] [T021] Create .env.example with JWT_SECRET_KEY, HF_OAUTH_CLIENT_ID, HF_OAUTH_CLIENT_SECRET, VAULT_BASE_PATH
- [x] [T022] Create .gitignore to exclude data/, .env, node_modules/, __pycache__, dist/
- [x] [T023] Create backend/src/services/database.py with SQLite initialization DDL from data-model.md
- [ ] [T024] Execute SQLite schema initialization (note_metadata, note_fts, note_tags, note_links, index_health tables)
---
## Phase 2: Foundational
**Goal**: Build core infrastructure required by all user stories.
- [x] [T025] Create backend/src/services/config.py to load env vars: JWT_SECRET_KEY, VAULT_BASE_PATH, HF_OAUTH_CLIENT_ID, HF_OAUTH_CLIENT_SECRET
- [x] [T026] [P] Create backend/src/models/user.py with User and HFProfile Pydantic models from data-model.md
- [x] [T027] [P] Create backend/src/models/note.py with Note, NoteMetadata, NoteCreate, NoteUpdate, NoteSummary Pydantic models from data-model.md
- [x] [T028] [P] Create backend/src/models/index.py with Wikilink, Tag, IndexHealth Pydantic models from data-model.md
- [x] [T029] [P] Create backend/src/models/search.py with SearchResult, SearchRequest Pydantic models from data-model.md
- [x] [T030] [P] Create backend/src/models/auth.py with TokenResponse, JWTPayload Pydantic models from data-model.md
- [x] [T031] [P] Create frontend/src/types/user.ts with User and HFProfile TypeScript types from data-model.md
- [x] [T032] [P] Create frontend/src/types/note.ts with Note, NoteMetadata, NoteSummary, NoteCreateRequest, NoteUpdateRequest TypeScript types from data-model.md
- [x] [T033] [P] Create frontend/src/types/search.ts with SearchResult, Tag, IndexHealth TypeScript types from data-model.md
- [x] [T034] [P] Create frontend/src/types/auth.ts with TokenResponse, APIError TypeScript types from data-model.md
- [x] [T035] Create backend/src/services/vault.py with VaultService class: path validation, sanitization (sanitize_path function from data-model.md), vault directory initialization
- [x] [T036] Create backend/src/services/auth.py with AuthService class: JWT creation (create_jwt), validation (validate_jwt), placeholder for HF OAuth
- [x] [T037] Create backend/src/api/middleware/auth_middleware.py with extract_user_id_from_jwt function to validate Authorization: Bearer header
- [x] [T038] Create backend/src/api/middleware/error_handlers.py with FastAPI exception handlers for 400, 401, 403, 404, 409, 413, 500 from http-api.yaml
---
## Phase 3: User Story 1 - AI Agent Writes (P1)
**Goal**: Enable AI agents to write/update docs via MCP STDIO, with automatic indexing.
- [x] [T039] [US1] Create backend/src/services/vault.py VaultService.read_note method: read file, parse frontmatter with python-frontmatter, extract title (priority: frontmatter > H1 > filename stem)
- [x] [T040] [US1] Create backend/src/services/vault.py VaultService.write_note method: validate path/content, create parent dirs, write frontmatter + body, return absolute path
- [x] [T041] [US1] Create backend/src/services/vault.py VaultService.delete_note method: validate path, remove file, handle FileNotFoundError
- [x] [T042] [US1] Create backend/src/services/vault.py VaultService.list_notes method: walk vault tree, filter by folder param, return paths and titles
- [x] [T043] [US1] Create backend/src/services/indexer.py IndexerService class with db connection management
- [x] [T044] [US1] Create backend/src/services/indexer.py IndexerService.index_note method: delete old rows for (user_id, note_path), insert into note_metadata, note_fts, note_tags, note_links
- [x] [T045] [US1] Create backend/src/services/indexer.py IndexerService.extract_wikilinks method: regex pattern \[\[([^\]]+)\]\] to extract link_text from body
- [x] [T046] [US1] Create backend/src/services/indexer.py IndexerService.resolve_wikilinks method: normalize slug (data-model.md algorithm), match against normalized_title_slug and normalized_path_slug, prefer same-folder, update is_resolved
- [x] [T047] [US1] Create backend/src/services/indexer.py IndexerService.increment_version method: get current version or default to 1, increment, return new version
- [x] [T048] [US1] Create backend/src/services/indexer.py IndexerService.update_index_health method: update note_count, last_incremental_update timestamp
- [x] [T049] [US1] Create backend/src/services/indexer.py IndexerService.delete_note_index method: delete rows from all index tables, update backlinks to set is_resolved=false
- [x] [T050] [US1] Create backend/src/mcp/server.py FastMCP server initialization with name="obsidian-docs-viewer"
- [x] [T051] [US1] Create backend/src/mcp/server.py list_notes MCP tool: call VaultService.list_notes, return [{path, title, last_modified}]
- [x] [T052] [US1] Create backend/src/mcp/server.py read_note MCP tool: call VaultService.read_note, return {path, title, metadata, body}
- [x] [T053] [US1] Create backend/src/mcp/server.py write_note MCP tool: call VaultService.write_note, then IndexerService.index_note, return {status: "ok", path}
- [x] [T054] [US1] Create backend/src/mcp/server.py delete_note MCP tool: call VaultService.delete_note, then IndexerService.delete_note_index, return {status: "ok"}
- [x] [T055] [US1] Create backend/src/mcp/server.py search_notes MCP tool: query note_fts with bm25 ranking (3.0 title weight, 1.0 body weight), add recency bonus, return [{path, title, snippet}]
- [x] [T056] [US1] Create backend/src/mcp/server.py get_backlinks MCP tool: query note_links WHERE target_path=?, join note_metadata, return [{path, title}]
- [x] [T057] [US1] Create backend/src/mcp/server.py get_tags MCP tool: query note_tags GROUP BY tag, return [{tag, count}]
- [x] [T058] [US1] Create backend/src/mcp/server.py STDIO transport mode: if __name__ == "__main__", run FastMCP with stdio transport for local development
- [x] [T059] [US1] Add recency bonus calculation to search_notes: +1.0 for updated in last 7 days, +0.5 for last 30 days, 0 otherwise
---
## Phase 4: User Story 2 - Human Reads UI (P1)
**Goal**: Web UI for browsing, searching, and reading notes with wikilinks and backlinks.
- [x] [T060] [US2] Create backend/src/api/routes/notes.py with GET /api/notes endpoint: call VaultService.list_notes, return NoteSummary[] from http-api.yaml
- [x] [T061] [US2] Create backend/src/api/routes/notes.py with GET /api/notes/{path} endpoint: URL-decode path, call VaultService.read_note, return Note from http-api.yaml
- [x] [T062] [US2] Create backend/src/api/routes/search.py with GET /api/search endpoint: call IndexerService search with query param, return SearchResult[] from http-api.yaml
- [x] [T063] [US2] Create backend/src/api/routes/search.py with GET /api/backlinks/{path} endpoint: URL-decode path, query note_links, return BacklinkResult[] from http-api.yaml
- [x] [T064] [US2] Create backend/src/api/routes/search.py with GET /api/tags endpoint: query note_tags, return Tag[] from http-api.yaml
- [x] [T065] [US2] Create backend/src/api/main.py FastAPI app with CORS middleware, mount routes, include error handlers
- [x] [T066] [US2] Create frontend/src/services/api.ts API client with fetch wrapper: add Authorization: Bearer header, handle JSON responses, throw APIError on non-200
- [x] [T067] [US2] Create frontend/src/services/api.ts listNotes function: GET /api/notes?folder=, return NoteSummary[]
- [x] [T068] [US2] Create frontend/src/services/api.ts getNote function: GET /api/notes/{encodeURIComponent(path)}, return Note
- [x] [T069] [US2] Create frontend/src/services/api.ts searchNotes function: GET /api/search?q=, return SearchResult[]
- [x] [T070] [US2] Create frontend/src/services/api.ts getBacklinks function: GET /api/backlinks/{encodeURIComponent(path)}, return BacklinkResult[]
- [x] [T071] [US2] Create frontend/src/services/api.ts getTags function: GET /api/tags, return Tag[]
- [x] [T072] [US2] Create frontend/src/lib/wikilink.ts with extractWikilinks function: regex /\[\[([^\]]+)\]\]/g
- [x] [T073] [US2] Create frontend/src/lib/wikilink.ts with normalizeSlug function: lowercase, replace spaces/underscores with dash, strip non-alphanumeric
- [x] [T074] [US2] Create frontend/src/lib/markdown.tsx with react-markdown config: code highlighting, wikilink custom renderer
- [x] [T075] [US2] Initialize shadcn/ui in frontend/: run npx shadcn@latest init, select default theme
- [x] [T076] [US2] Install shadcn/ui components: ScrollArea, Button, Input, Card, Badge, Resizable, Collapsible, Dialog, Alert, Textarea, Dropdown-Menu, Avatar, Command, Tooltip, Popover
- [x] [T077] [US2] Create frontend/src/components/DirectoryTree.tsx: recursive tree view with collapsible folders, leaf items for notes, onClick handler to load note
- [x] [T078] [US2] Create frontend/src/components/NoteViewer.tsx: render note title, metadata (tags as badges, timestamps), react-markdown body with wikilink links, backlinks section in footer
- [x] [T079] [US2] Create frontend/src/components/SearchBar.tsx: Input with debounced onChange (300ms), dropdown results with onClick to navigate to note
- [x] [T080] [US2] Create frontend/src/pages/MainApp.tsx: two-pane layout (left: DirectoryTree + SearchBar in ScrollArea, right: NoteViewer), state management for selected note path
- [x] [T081] [US2] Add wikilink click handler in NoteViewer: onClick [[link]] β†’ normalizeSlug β†’ API lookup β†’ navigate to resolved note
- [x] [T082] [US2] Add broken wikilink styling in NoteViewer: render unresolved [[links]] with distinct color/style
- [x] [T083] [US2] Create frontend/src/pages/MainApp.tsx useEffect to load directory tree on mount: call listNotes()
- [x] [T084] [US2] Create frontend/src/pages/MainApp.tsx useEffect to load note when path changes: call getNote(path) and getBacklinks(path)
---
## Phase 5: User Story 3 - Human Edits UI (P2)
**Goal**: Split-pane editor with optimistic concurrency protection.
- [x] [T085] [US3] Create backend/src/api/routes/notes.py with PUT /api/notes/{path} endpoint: URL-decode path, validate request body (NoteUpdate), check if_version, call VaultService.write_note, return NoteResponse from http-api.yaml
- [x] [T086] [US3] Add optimistic concurrency check in IndexerService.increment_version: if if_version provided and != current version, raise ConflictError
- [x] [T087] [US3] Create ConflictError exception in backend/src/models/errors.py, map to 409 Conflict in error_handlers.py
- [x] [T088] [US3] Create frontend/src/services/api.ts updateNote function: PUT /api/notes/{encodeURIComponent(path)} with {title?, metadata?, body, if_version?}, handle 409 response
- [x] [T089] [US3] Create frontend/src/components/NoteEditor.tsx: split-pane layout (left: textarea for markdown source, right: live preview with react-markdown)
- [x] [T090] [US3] Create frontend/src/components/NoteEditor.tsx with Save button: onClick β†’ call updateNote with if_version from initial note load, handle success β†’ switch to read mode
- [x] [T091] [US3] Create frontend/src/components/NoteEditor.tsx with Cancel button: onClick β†’ discard changes, switch to read mode
- [x] [T092] [US3] Add 409 Conflict error handling in NoteEditor: display alert "Note changed since you opened it, please reload before saving"
- [x] [T093] [US3] Add Edit button to NoteViewer: onClick β†’ switch main pane to NoteEditor mode, pass current note version
- [x] [T094] [US3] Update frontend/src/pages/MainApp.tsx to toggle between NoteViewer and NoteEditor based on edit mode state
---
## Phase 6: User Story 4 - Multi-Tenant OAuth (P2)
**Goal**: HF OAuth login, per-user vaults, JWT tokens for API and MCP HTTP.
- [ ] [T095] [US4] Create backend/src/services/auth.py HF OAuth integration: use huggingface_hub.attach_huggingface_oauth and parse_huggingface_oauth helpers
- [ ] [T096] [US4] Create backend/src/api/routes/auth.py with GET /auth/login endpoint: redirect to HF OAuth authorize URL
- [ ] [T097] [US4] Create backend/src/api/routes/auth.py with GET /auth/callback endpoint: parse_huggingface_oauth, map HF username to user_id, create vault if new user, set session cookie
- [ ] [T098] [US4] Create backend/src/api/routes/auth.py with POST /api/tokens endpoint: validate authenticated user, call AuthService.create_jwt, return TokenResponse from http-api.yaml
- [ ] [T099] [US4] Create backend/src/api/routes/auth.py with GET /api/me endpoint: validate Bearer token, return User from http-api.yaml
- [ ] [T100] [US4] Update backend/src/api/middleware/auth_middleware.py to extract user_id from JWT sub claim, attach to request.state.user_id
- [ ] [T101] [US4] Update backend/src/services/vault.py to scope all operations by user_id: vault path = VAULT_BASE_PATH / user_id
- [ ] [T102] [US4] Update backend/src/services/indexer.py to scope all queries by user_id: WHERE user_id = ?
- [ ] [T103] [US4] Initialize vault and index on first user login: create vault dir, insert initial index_health row
- [ ] [T104] [US4] Create backend/src/mcp/server.py HTTP transport mode: FastMCP with http transport, BearerAuth validation, extract user_id from JWT
- [x] [T105] [US4] Create frontend/src/services/auth.ts with login function: redirect to /auth/login
- [x] [T106] [US4] Create frontend/src/services/auth.ts with getCurrentUser function: GET /api/me, return User
- [x] [T107] [US4] Create frontend/src/services/auth.ts with getToken function: POST /api/tokens, return TokenResponse, store token in memory
- [x] [T108] [US4] Create frontend/src/pages/Login.tsx: "Sign in with Hugging Face" button β†’ onClick call auth.login()
- [x] [T109] [US4] Create frontend/src/pages/Settings.tsx: display user profile (user_id, HF avatar), API token with copy button for MCP config
- [x] [T110] [US4] Update frontend/src/pages/App.tsx to call getCurrentUser on mount, redirect to Login if 401
- [x] [T111] [US4] Update frontend/src/services/api.ts to include token from localStorage in Authorization header
---
## Phase 7: User Story 5 - Advanced Search (P3)
**Goal**: Enhanced search ranking and index health monitoring.
- [x] [T112] [US5] Update backend/src/services/indexer.py search_notes to calculate recency bonus: +1.0 for updated in last 7 days, +0.5 for last 30 days, 0 otherwise
- [x] [T113] [US5] Update backend/src/services/indexer.py search_notes to calculate final score: (3 * title_bm25) + (1 * body_bm25) + recency_bonus
- [x] [T114] [US5] Create backend/src/api/routes/index.py with GET /api/index/health endpoint: query index_health, return IndexHealth from http-api.yaml
- [x] [T115] [US5] Create backend/src/api/routes/index.py with POST /api/index/rebuild endpoint: call IndexerService.rebuild_index, return RebuildResponse from http-api.yaml
- [x] [T116] [US5] Create backend/src/services/indexer.py IndexerService.rebuild_index method: delete all user rows, walk vault, parse all notes, re-insert into all index tables, update index_health
- [x] [T117] [US5] Create frontend/src/services/api.ts getIndexHealth function: GET /api/index/health, return IndexHealth
- [x] [T118] [US5] Create frontend/src/services/api.ts rebuildIndex function: POST /api/index/rebuild, return RebuildResponse
- [x] [T119] [US5] Add index health indicator to frontend/src/pages/MainApp.tsx: display note count and last updated timestamp in footer
- [x] [T120] [US5] Add "Rebuild Index" button to frontend/src/pages/Settings.tsx: onClick β†’ call rebuildIndex, show progress/completion message
---
## Phase 8: Polish & Cross-Cutting
**Goal**: Documentation, configuration, logging, error handling improvements.
- [x] [T121] Create README.md with project overview, tech stack, local setup instructions (backend venv + npm install)
- [x] [T122] Add README.md section: "Running Backend" with uvicorn command for HTTP API and python -m backend.src.mcp.server for MCP STDIO
- [x] [T123] Add README.md section: "Running Frontend" with npm run dev command
- [x] [T124] Add README.md section: "MCP Client Configuration" with Claude Code/Desktop STDIO example from mcp-tools.json
- [ ] [T125] Add README.md section: "Deploying to Hugging Face Space" with environment variables and OAuth setup
- [x] [T126] Update .env.example with all variables: JWT_SECRET_KEY, VAULT_BASE_PATH, HF_OAUTH_CLIENT_ID, HF_OAUTH_CLIENT_SECRET, DATABASE_PATH
- [x] [T127] Add structured logging to backend/src/services/vault.py: log file operations with user_id, note_path, operation type
- [x] [T128] Add structured logging to backend/src/services/indexer.py: log index updates with user_id, note_path, duration_ms
- [x] [T129] Add structured logging to backend/src/mcp/server.py: log MCP tool calls with tool_name, user_id, duration_ms
- [x] [T130] Improve error messages in backend/src/api/middleware/error_handlers.py: include detail objects with field names and reasons
- [x] [T131] Add input validation to all HTTP API routes: validate path format, content size, required fields
- [x] [T132] Add input validation to all MCP tools: validate path format, content size via Pydantic models
- [x] [T133] Add rate limiting consideration to README.md: note potential need for per-user rate limits in production
- [x] [T134] Add performance optimization notes to README.md: FTS5 prefix indexes, SQLite WAL mode for concurrency
---
## Dependencies
### Story Completion Order
**Must complete in this order**:
1. **Phase 1** (Setup) β†’ **Phase 2** (Foundational) β†’ **Phase 3** (US1) + **Phase 4** (US2) in parallel
2. **Phase 5** (US3) depends on Phase 4 (needs HTTP API routes from US2)
3. **Phase 6** (US4) depends on Phase 2 (needs auth foundation) and Phase 3 (needs vault/indexer)
4. **Phase 7** (US5) depends on Phase 3 (needs indexer) and Phase 4 (needs HTTP API)
5. **Phase 8** (Polish) can run anytime after Phase 4 (MVP complete)
### Task Dependencies
**Critical path** (must be sequential):
- T023 (SQLite schema) β†’ T043 (IndexerService) β†’ T044 (index_note)
- T035 (VaultService foundation) β†’ T039 (read_note) β†’ T040 (write_note)
- T050 (FastMCP init) β†’ T051-T057 (MCP tools)
- T065 (FastAPI app) β†’ T060-T064 (HTTP routes)
**Parallelizable within phases** (marked with [P]):
- All directory creation tasks (T002-T017)
- All Pydantic model tasks (T026-T030)
- All TypeScript type tasks (T031-T034)
- All frontend component tasks within US2 (T077-T079)
---
## Parallel Execution Examples
### User Story 1 (AI Agent Writes) - Parallel Work
**Team A**: VaultService implementation (T039-T042)
**Team B**: IndexerService implementation (T043-T049)
**Team C**: MCP tools implementation (T051-T057)
After T050 (FastMCP init) completes, Team C can implement all 7 MCP tools in parallel since they're independent endpoints.
### User Story 2 (Human Reads UI) - Parallel Work
**Team A**: Backend HTTP API routes (T060-T064)
**Team B**: Frontend API client (T066-T071)
**Team C**: Frontend components (T077-T079)
After T065 (FastAPI app) and T075-T076 (shadcn/ui setup) complete, all three teams can work in parallel.
### User Story 4 (Multi-Tenant OAuth) - Parallel Work
**Team A**: Backend OAuth integration (T095-T104)
**Team B**: Frontend auth flow (T105-T111)
Both teams can work in parallel after Phase 2 (Foundational) completes.
---
## Summary
**Total Tasks**: 134
- **Phase 1 (Setup)**: 24 tasks
- **Phase 2 (Foundational)**: 14 tasks
- **Phase 3 (US1 - AI Agent Writes)**: 21 tasks
- **Phase 4 (US2 - Human Reads UI)**: 25 tasks
- **Phase 5 (US3 - Human Edits UI)**: 10 tasks
- **Phase 6 (US4 - Multi-Tenant OAuth)**: 17 tasks
- **Phase 7 (US5 - Advanced Search)**: 9 tasks
- **Phase 8 (Polish)**: 14 tasks
**MVP Tasks** (US1 + US2): 84 tasks (Phases 1-4)
**Post-MVP Tasks** (US3 + US4 + US5): 36 tasks (Phases 5-7)
**Polish Tasks**: 14 tasks (Phase 8)
**Estimated Effort**:
- MVP (US1 + US2): ~2-3 weeks (1-2 developers)
- Post-MVP (US3 + US4 + US5): ~1-2 weeks
- Polish: ~3-5 days
- **Total**: ~4-6 weeks for complete implementation
**Key Milestones**:
1. **Week 1**: Complete Phase 1-2 (Setup + Foundational)
2. **Week 2**: Complete Phase 3 (US1 - MCP STDIO working)
3. **Week 3**: Complete Phase 4 (US2 - Web UI working, MVP delivered)
4. **Week 4**: Complete Phase 5-6 (US3 editing + US4 multi-tenant)
5. **Week 5**: Complete Phase 7-8 (US5 advanced search + Polish)
**Next Steps**:
1. Review this task breakdown with stakeholders
2. Assign initial tasks (Phase 1 Setup) to team
3. Create GitHub issues from tasks using `/speckit.taskstoissues`
4. Begin implementation with T001 (project structure)