bigwolfe commited on
Commit
6cdb404
Β·
1 Parent(s): 58b4b49

start up script, mcp working, input sanitization over mcp

Browse files
.gitignore CHANGED
@@ -43,3 +43,7 @@ build/
43
  # Runtime data
44
  data/
45
 
 
 
 
 
 
43
  # Runtime data
44
  data/
45
 
46
+ # Development script artifacts
47
+ .backend.pid
48
+ .frontend.pid
49
+
ai-notes/mcp-tools-test-report.md ADDED
@@ -0,0 +1,206 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # MCP Tools Test Report
2
+
3
+ ## Overview
4
+ Comprehensive testing of all MCP obsidian-docs tools exposed by the server.
5
+
6
+ ## Test Results
7
+
8
+ ### βœ… `list_notes` - **PASSING**
9
+ **Functionality**: Lists all notes in the vault
10
+ **Test Results**:
11
+ - Successfully listed 10 notes from the vault
12
+ - Returns array with: `path`, `title`, `last_modified` timestamp
13
+ - Works with optional `folder` parameter (tested with `null` and empty string)
14
+ - **Status**: βœ… Working correctly
15
+
16
+ **Sample Output**:
17
+ ```json
18
+ [
19
+ {"path":"API Documentation.md","title":"API Documentation 123","last_modified":"2025-11-17T01:17:52.721411+00:00"},
20
+ {"path":"test-from-cursor-over-mcp.md","title":"test from cursor over mcp","last_modified":"2025-11-17T02:16:14.156806+00:00"}
21
+ ]
22
+ ```
23
+
24
+ ---
25
+
26
+ ### βœ… `read_note` - **PASSING**
27
+ **Functionality**: Reads a note with metadata and body
28
+ **Test Results**:
29
+ - Successfully read existing notes
30
+ - Returns: `path`, `title`, `metadata` (created, updated, title), and `body`
31
+ - Properly handles deleted notes (returns "Note not found" error)
32
+ - **Status**: βœ… Working correctly
33
+
34
+ **Sample Output**:
35
+ ```json
36
+ {
37
+ "path":"test-from-cursor-over-mcp.md",
38
+ "title":"test from cursor over mcp",
39
+ "metadata":{
40
+ "created":"2025-11-17T02:13:39+00:00",
41
+ "title":"test from cursor over mcp",
42
+ "updated":"2025-11-17T02:16:14+00:00"
43
+ },
44
+ "body":"# test from cursor over mcp\n\nThis is a test note created from Cursor using the MCP server.\n\n**Updated**: This note has been updated to test the write_note functionality.\n\nNow referencing [[API Documentation]] to test backlinks."
45
+ }
46
+ ```
47
+
48
+ **Error Handling**:
49
+ - βœ… Returns proper error for non-existent notes: "Note not found: test-delete-me.md"
50
+
51
+ ---
52
+
53
+ ### βœ… `write_note` - **PASSING**
54
+ **Functionality**: Creates or updates notes with optional metadata
55
+ **Test Results**:
56
+ - Successfully created new notes
57
+ - Successfully updated existing notes (preserves creation timestamp, updates modified timestamp)
58
+ - Supports optional `title` parameter
59
+ - Supports optional `metadata` parameter (not fully tested with tags)
60
+ - Automatically updates frontmatter timestamps
61
+ - **Status**: βœ… Working correctly
62
+
63
+ **Test Cases**:
64
+ 1. βœ… Created new note: `test-from-cursor-over-mcp.md`
65
+ 2. βœ… Updated existing note: Modified `test-from-cursor-over-mcp.md` and confirmed timestamp updated
66
+ 3. βœ… Created note for deletion testing: `test-delete-me.md`
67
+
68
+ **Sample Output**:
69
+ ```json
70
+ {"status":"ok","path":"test-from-cursor-over-mcp.md"}
71
+ ```
72
+
73
+ ---
74
+
75
+ ### βœ… `delete_note` - **PASSING**
76
+ **Functionality**: Deletes a note from the vault
77
+ **Test Results**:
78
+ - Successfully deleted test note `test-delete-me.md`
79
+ - Returns status confirmation: `{"status":"ok"}`
80
+ - Deleted note removed from vault (verified by attempting to read it)
81
+ - **Status**: βœ… Working correctly
82
+
83
+ **Test Case**:
84
+ 1. βœ… Created `test-delete-me.md`
85
+ 2. βœ… Verified it exists by reading it
86
+ 3. βœ… Deleted it successfully
87
+ 4. βœ… Confirmed deletion by attempting to read (returned "Note not found" error)
88
+
89
+ ---
90
+
91
+ ### βœ… `search_notes` - **MOSTLY PASSING**
92
+ **Functionality**: Full-text search with snippets and recency-aware scoring
93
+ **Test Results**:
94
+ - Successfully searches notes with proper queries
95
+ - Returns results with highlighted snippets using `<mark>` tags
96
+ - Supports `limit` parameter (tested with 5 and 10)
97
+ - Returns: `path`, `title`, `snippet` (with markdown highlights)
98
+ - **Status**: ⚠️ Working but has SQL syntax issues with special characters
99
+
100
+ **Test Cases**:
101
+ 1. βœ… Search "test cursor" - Found matching note with highlights
102
+ 2. βœ… Search "API" - Found multiple notes with proper highlighting
103
+ 3. ❌ Search with special characters (apostrophe) - **Error**: `fts5: syntax error near "'"`
104
+
105
+ **Sample Output**:
106
+ ```json
107
+ [
108
+ {
109
+ "path":"test-from-cursor-over-mcp.md",
110
+ "title":"test from cursor over mcp",
111
+ "snippet":"# <mark>test</mark> from <mark>cursor</mark> over mcp\n\nThis is a <mark>test</mark> note created from <mark>Cursor</mark> using the MCP server."
112
+ }
113
+ ]
114
+ ```
115
+
116
+ **Known Issue**:
117
+ - ❌ SQL syntax error when search query contains special characters like apostrophes
118
+ - **Recommendation**: Implement input sanitization/escaping for search queries
119
+
120
+ ---
121
+
122
+ ### βœ… `get_backlinks` - **PASSING**
123
+ **Functionality**: Lists notes that reference the target note via wikilinks
124
+ **Test Results**:
125
+ - Successfully retrieved backlinks for existing notes
126
+ - Returns array of notes that reference the target
127
+ - Includes: `path` and `title` for each backlink
128
+ - **Status**: βœ… Working correctly
129
+
130
+ **Test Cases**:
131
+ 1. βœ… Retrieved backlinks for "API Documentation.md" - Found 6 notes referencing it
132
+ 2. βœ… Confirmed backlinks include the test note we created that references it
133
+
134
+ **Sample Output**:
135
+ ```json
136
+ [
137
+ {"path":"test-from-cursor-over-mcp.md","title":"test from cursor over mcp"},
138
+ {"path":"Architecture Overview.md","title":"Architecture Overview"},
139
+ {"path":"FAQ.md","title":"FAQ"}
140
+ ]
141
+ ```
142
+
143
+ ---
144
+
145
+ ### βœ… `get_tags` - **PASSING**
146
+ **Functionality**: Lists all tags and their associated note counts
147
+ **Test Results**:
148
+ - Successfully retrieved all tags from the vault
149
+ - Returns array with: `tag` name and `count` of notes using that tag
150
+ - **Status**: βœ… Working correctly
151
+
152
+ **Sample Output**:
153
+ ```json
154
+ [
155
+ {"tag":"guide","count":2},
156
+ {"tag":"agents","count":1},
157
+ {"tag":"ai","count":1},
158
+ {"tag":"architecture","count":1},
159
+ {"tag":"mcp","count":1}
160
+ ]
161
+ ```
162
+
163
+ ---
164
+
165
+ ## Summary
166
+
167
+ ### Tools Tested: 7
168
+ ### Passing: 6
169
+ ### Passing with Issues: 1
170
+
171
+ | Tool | Status | Notes |
172
+ |------|--------|-------|
173
+ | `list_notes` | βœ… PASS | Works perfectly |
174
+ | `read_note` | βœ… PASS | Works perfectly, proper error handling |
175
+ | `write_note` | βœ… PASS | Works perfectly, updates timestamps correctly |
176
+ | `delete_note` | βœ… PASS | Works perfectly, proper cleanup |
177
+ | `search_notes` | ⚠️ PASS (with issues) | SQL syntax error with special characters |
178
+ | `get_backlinks` | βœ… PASS | Works perfectly |
179
+ | `get_tags` | βœ… PASS | Works perfectly |
180
+
181
+ ## Issues Found
182
+
183
+ ### 1. Search Query SQL Syntax Error
184
+ **Severity**: Medium
185
+ **Description**: The `search_notes` tool fails when search queries contain special characters like apostrophes
186
+ **Error Message**: `fts5: syntax error near "'"`
187
+ **Recommendation**: Implement proper SQL escaping/sanitization for FTS5 queries in the backend
188
+
189
+ ## Recommendations
190
+
191
+ 1. **Fix SQL injection vulnerability** in search functionality
192
+ 2. **Add input validation** for all tool parameters
193
+ 3. **Consider adding**:
194
+ - Error codes for better error handling
195
+ - Rate limiting information
196
+ - Validation for path parameters (no '..' or '\\' as documented)
197
+
198
+ ## Test Notes Created/Modified
199
+
200
+ - βœ… `test-from-cursor-over-mcp.md` - Created and updated
201
+ - βœ… `test-delete-me.md` - Created and deleted (for deletion testing)
202
+
203
+ ## Conclusion
204
+
205
+ Overall, the MCP server is working well with 6 out of 7 tools functioning perfectly. The only issue is with the search functionality when handling special characters, which should be addressed for production use.
206
+
backend/src/services/indexer.py CHANGED
@@ -12,6 +12,7 @@ from .database import DatabaseService
12
  from .vault import VaultNote
13
 
14
  WIKILINK_PATTERN = re.compile(r"\[\[([^\]]+)\]\]")
 
15
 
16
 
17
  def _utcnow_iso() -> str:
@@ -35,6 +36,45 @@ def normalize_tag(tag: str | None) -> str:
35
  return tag.strip().lower()
36
 
37
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  class IndexerService:
39
  """Manage SQLite-backed metadata, tags, search index, and link graph."""
40
 
@@ -241,6 +281,8 @@ class IndexerService:
241
  if not query or not query.strip():
242
  raise ValueError("Search query cannot be empty")
243
 
 
 
244
  conn = self.db_service.connect()
245
  try:
246
  rows = conn.execute(
@@ -257,7 +299,7 @@ class IndexerService:
257
  ORDER BY score DESC
258
  LIMIT ?
259
  """,
260
- (user_id, query, limit),
261
  ).fetchall()
262
  finally:
263
  conn.close()
 
12
  from .vault import VaultNote
13
 
14
  WIKILINK_PATTERN = re.compile(r"\[\[([^\]]+)\]\]")
15
+ WHITESPACE_RE = re.compile(r"\s+")
16
 
17
 
18
  def _utcnow_iso() -> str:
 
36
  return tag.strip().lower()
37
 
38
 
39
+ def _prepare_match_query(query: str) -> str:
40
+ """
41
+ Sanitize user-supplied query text for FTS5 MATCH usage.
42
+
43
+ - Splits on whitespace to keep simple keyword semantics.
44
+ - Wraps each token in double quotes to neutralize punctuation (e.g., apostrophes).
45
+ - Escapes embedded double quotes by doubling them.
46
+ - Preserves trailing '*' characters for prefix searches.
47
+ """
48
+ tokens = [token for token in WHITESPACE_RE.split(query or "") if token.strip()]
49
+ sanitized_terms: List[str] = []
50
+
51
+ for token in tokens:
52
+ cleaned = token.strip()
53
+ suffix = ""
54
+ while cleaned.endswith("*"):
55
+ suffix += "*"
56
+ cleaned = cleaned[:-1]
57
+
58
+ # Remove wrapping quotes if present; inner quotes are preserved/escaped below.
59
+ if cleaned.startswith('"') and cleaned.endswith('"') and len(cleaned) >= 2:
60
+ cleaned = cleaned[1:-1]
61
+ if cleaned.startswith("'") and cleaned.endswith("'") and len(cleaned) >= 2:
62
+ cleaned = cleaned[1:-1]
63
+
64
+ cleaned = cleaned.strip()
65
+ if not cleaned:
66
+ continue
67
+
68
+ escaped = cleaned.replace('"', '""')
69
+ term = f'"{escaped}"{suffix}'
70
+ sanitized_terms.append(term)
71
+
72
+ if not sanitized_terms:
73
+ raise ValueError("Search query must contain alphanumeric characters")
74
+
75
+ return " ".join(sanitized_terms)
76
+
77
+
78
  class IndexerService:
79
  """Manage SQLite-backed metadata, tags, search index, and link graph."""
80
 
 
281
  if not query or not query.strip():
282
  raise ValueError("Search query cannot be empty")
283
 
284
+ sanitized_query = _prepare_match_query(query)
285
+
286
  conn = self.db_service.connect()
287
  try:
288
  rows = conn.execute(
 
299
  ORDER BY score DESC
300
  LIMIT ?
301
  """,
302
+ (user_id, sanitized_query, limit),
303
  ).fetchall()
304
  finally:
305
  conn.close()
backend/tests/unit/test_indexer_search.py ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+
3
+ import pytest
4
+
5
+ from backend.src.services.database import DatabaseService
6
+ from backend.src.services.indexer import IndexerService
7
+
8
+
9
+ @pytest.fixture()
10
+ def indexer(tmp_path: Path) -> IndexerService:
11
+ db_path = tmp_path / "index.db"
12
+ db_service = DatabaseService(db_path)
13
+ db_service.initialize()
14
+ return IndexerService(db_service=db_service)
15
+
16
+
17
+ def _note(path: str, title: str, body: str) -> dict:
18
+ return {
19
+ "path": path,
20
+ "metadata": {"title": title},
21
+ "body": body,
22
+ }
23
+
24
+
25
+ def test_search_notes_handles_apostrophes(indexer: IndexerService) -> None:
26
+ indexer.index_note(
27
+ "local-dev",
28
+ _note(
29
+ "notes/obrien.md",
30
+ "O'Brien Authentication",
31
+ "Details about O'Brien's authentication flow.",
32
+ ),
33
+ )
34
+
35
+ results = indexer.search_notes("local-dev", "O'Brien")
36
+
37
+ assert results
38
+ assert results[0]["path"] == "notes/obrien.md"
39
+
40
+
41
+ def test_search_notes_preserves_prefix_queries(indexer: IndexerService) -> None:
42
+ indexer.index_note(
43
+ "local-dev",
44
+ _note(
45
+ "notes/auth.md",
46
+ "Authorization Overview",
47
+ "Prefix search should match auth prefix tokens.",
48
+ ),
49
+ )
50
+
51
+ results = indexer.search_notes("local-dev", "auth*")
52
+
53
+ assert results
54
+ assert results[0]["path"] == "notes/auth.md"
55
+
frontend/src/components/ui/label.tsx CHANGED
@@ -23,3 +23,4 @@ Label.displayName = LabelPrimitive.Root.displayName
23
 
24
  export { Label }
25
 
 
 
23
 
24
  export { Label }
25
 
26
+
frontend/src/pages/Settings.tsx CHANGED
@@ -203,13 +203,19 @@ export function Settings() {
203
  "obsidian-docs": {
204
  "command": "python",
205
  "args": ["-m", "backend.src.mcp.server"],
 
206
  "env": {
207
- "BEARER_TOKEN": "${apiToken || 'YOUR_TOKEN_HERE'}"
 
 
208
  }
209
  }
210
  }
211
  }`}
212
  </pre>
 
 
 
213
  </div>
214
  </CardContent>
215
  </Card>
 
203
  "obsidian-docs": {
204
  "command": "python",
205
  "args": ["-m", "backend.src.mcp.server"],
206
+ "cwd": "/path/to/Document-MCP",
207
  "env": {
208
+ "BEARER_TOKEN": "${apiToken || 'YOUR_TOKEN_HERE'}",
209
+ "FASTMCP_SHOW_CLI_BANNER": "false",
210
+ "PYTHONPATH": "/path/to/Document-MCP"
211
  }
212
  }
213
  }
214
  }`}
215
  </pre>
216
+ <p className="text-xs text-muted-foreground mt-2">
217
+ πŸ’‘ Replace <code className="bg-muted px-1 rounded">/path/to/Document-MCP</code> with your actual project path
218
+ </p>
219
  </div>
220
  </CardContent>
221
  </Card>
specs/001-obsidian-docs-viewer/data-model.md CHANGED
@@ -413,6 +413,8 @@ ORDER BY rank DESC
413
  LIMIT 50;
414
  ```
415
 
 
 
416
  ---
417
 
418
  #### note_tags
 
413
  LIMIT 50;
414
  ```
415
 
416
+ **Safety**: Incoming queries are tokenized and each token is wrapped in double quotes before being passed to `MATCH`, escaping embedded quotes and preserving trailing `*` for prefix searches. This prevents syntax errors from characters such as apostrophes while keeping simple keyword semantics.
417
+
418
  ---
419
 
420
  #### note_tags
start-dev.sh ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ # Development startup script for Document Viewer
3
+
4
+ # Colors for output
5
+ GREEN='\033[0;32m'
6
+ BLUE='\033[0;34m'
7
+ RED='\033[0;31m'
8
+ NC='\033[0m' # No Color
9
+
10
+ PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
+ BACKEND_DIR="$PROJECT_ROOT/backend"
12
+ FRONTEND_DIR="$PROJECT_ROOT/frontend"
13
+
14
+ # PID files
15
+ BACKEND_PID_FILE="$PROJECT_ROOT/.backend.pid"
16
+ FRONTEND_PID_FILE="$PROJECT_ROOT/.frontend.pid"
17
+
18
+ echo -e "${BLUE}Starting Document Viewer Development Environment${NC}"
19
+ echo "=================================================="
20
+
21
+ # Check if backend venv exists
22
+ if [ ! -d "$BACKEND_DIR/.venv" ]; then
23
+ echo -e "${RED}Error: Backend virtual environment not found${NC}"
24
+ echo "Run: cd backend && uv venv && uv pip install -e ."
25
+ exit 1
26
+ fi
27
+
28
+ # Check if frontend node_modules exists
29
+ if [ ! -d "$FRONTEND_DIR/node_modules" ]; then
30
+ echo -e "${RED}Error: Frontend dependencies not installed${NC}"
31
+ echo "Run: cd frontend && npm install"
32
+ exit 1
33
+ fi
34
+
35
+ # Start Backend
36
+ echo -e "${GREEN}Starting backend server...${NC}"
37
+ cd "$BACKEND_DIR"
38
+ JWT_SECRET_KEY="local-dev-secret-key-123" \
39
+ VAULT_BASE_PATH="$PROJECT_ROOT/data/vaults" \
40
+ .venv/bin/uvicorn src.api.main:app --host 0.0.0.0 --port 8000 --reload > "$PROJECT_ROOT/backend.log" 2>&1 &
41
+ BACKEND_PID=$!
42
+ echo $BACKEND_PID > "$BACKEND_PID_FILE"
43
+ echo -e "${GREEN}βœ“ Backend started (PID: $BACKEND_PID)${NC}"
44
+ echo " Logs: $PROJECT_ROOT/backend.log"
45
+ echo " URL: http://localhost:8000"
46
+
47
+ # Wait a moment for backend to start
48
+ sleep 2
49
+
50
+ # Start Frontend
51
+ echo -e "${GREEN}Starting frontend dev server...${NC}"
52
+ cd "$FRONTEND_DIR"
53
+ npm run dev > "$PROJECT_ROOT/frontend.log" 2>&1 &
54
+ FRONTEND_PID=$!
55
+ echo $FRONTEND_PID > "$FRONTEND_PID_FILE"
56
+ echo -e "${GREEN}βœ“ Frontend started (PID: $FRONTEND_PID)${NC}"
57
+ echo " Logs: $PROJECT_ROOT/frontend.log"
58
+ echo " URL: http://localhost:5173"
59
+
60
+ echo ""
61
+ echo -e "${BLUE}=================================================="
62
+ echo "Development servers are running!"
63
+ echo "=================================================="
64
+ echo -e "${NC}"
65
+ echo "Frontend: http://localhost:5173"
66
+ echo "Backend: http://localhost:8000"
67
+ echo ""
68
+ echo "To stop servers, run: ./stop-dev.sh"
69
+ echo "To view logs, run: tail -f backend.log frontend.log"
70
+ echo ""
71
+
status-dev.sh ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ # Check status of development servers for Document Viewer
3
+
4
+ # Colors for output
5
+ GREEN='\033[0;32m'
6
+ RED='\033[0;31m'
7
+ BLUE='\033[0;34m'
8
+ NC='\033[0m' # No Color
9
+
10
+ PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
+ BACKEND_PID_FILE="$PROJECT_ROOT/.backend.pid"
12
+ FRONTEND_PID_FILE="$PROJECT_ROOT/.frontend.pid"
13
+
14
+ echo -e "${BLUE}Document Viewer Development Status${NC}"
15
+ echo "=================================================="
16
+
17
+ # Check Backend
18
+ if [ -f "$BACKEND_PID_FILE" ]; then
19
+ BACKEND_PID=$(cat "$BACKEND_PID_FILE")
20
+ if ps -p $BACKEND_PID > /dev/null 2>&1; then
21
+ echo -e "${GREEN}βœ“ Backend: RUNNING${NC} (PID: $BACKEND_PID)"
22
+ echo " URL: http://localhost:8000"
23
+ else
24
+ echo -e "${RED}βœ— Backend: NOT RUNNING${NC} (stale PID: $BACKEND_PID)"
25
+ fi
26
+ else
27
+ echo -e "${RED}βœ— Backend: NOT RUNNING${NC}"
28
+ fi
29
+
30
+ # Check Frontend
31
+ if [ -f "$FRONTEND_PID_FILE" ]; then
32
+ FRONTEND_PID=$(cat "$FRONTEND_PID_FILE")
33
+ if ps -p $FRONTEND_PID > /dev/null 2>&1; then
34
+ echo -e "${GREEN}βœ“ Frontend: RUNNING${NC} (PID: $FRONTEND_PID)"
35
+ echo " URL: http://localhost:5173"
36
+ else
37
+ echo -e "${RED}βœ— Frontend: NOT RUNNING${NC} (stale PID: $FRONTEND_PID)"
38
+ fi
39
+ else
40
+ echo -e "${RED}βœ— Frontend: NOT RUNNING${NC}"
41
+ fi
42
+
43
+ echo ""
44
+ echo "Logs:"
45
+ echo " Backend: tail -f $PROJECT_ROOT/backend.log"
46
+ echo " Frontend: tail -f $PROJECT_ROOT/frontend.log"
47
+ echo ""
48
+
stop-dev.sh ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ # Stop development servers for Document Viewer
3
+
4
+ # Colors for output
5
+ RED='\033[0;31m'
6
+ GREEN='\033[0;32m'
7
+ NC='\033[0m' # No Color
8
+
9
+ PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
10
+ BACKEND_PID_FILE="$PROJECT_ROOT/.backend.pid"
11
+ FRONTEND_PID_FILE="$PROJECT_ROOT/.frontend.pid"
12
+
13
+ echo -e "${RED}Stopping Document Viewer Development Environment${NC}"
14
+ echo "=================================================="
15
+
16
+ # Stop Backend
17
+ if [ -f "$BACKEND_PID_FILE" ]; then
18
+ BACKEND_PID=$(cat "$BACKEND_PID_FILE")
19
+ if ps -p $BACKEND_PID > /dev/null 2>&1; then
20
+ echo "Stopping backend (PID: $BACKEND_PID)..."
21
+ kill $BACKEND_PID
22
+ rm "$BACKEND_PID_FILE"
23
+ echo -e "${GREEN}βœ“ Backend stopped${NC}"
24
+ else
25
+ echo "Backend process not found (PID: $BACKEND_PID)"
26
+ rm "$BACKEND_PID_FILE"
27
+ fi
28
+ else
29
+ echo "No backend PID file found"
30
+ fi
31
+
32
+ # Stop Frontend
33
+ if [ -f "$FRONTEND_PID_FILE" ]; then
34
+ FRONTEND_PID=$(cat "$FRONTEND_PID_FILE")
35
+ if ps -p $FRONTEND_PID > /dev/null 2>&1; then
36
+ echo "Stopping frontend (PID: $FRONTEND_PID)..."
37
+ kill $FRONTEND_PID
38
+ rm "$FRONTEND_PID_FILE"
39
+ echo -e "${GREEN}βœ“ Frontend stopped${NC}"
40
+ else
41
+ echo "Frontend process not found (PID: $FRONTEND_PID)"
42
+ rm "$FRONTEND_PID_FILE"
43
+ fi
44
+ else
45
+ echo "No frontend PID file found"
46
+ fi
47
+
48
+ echo ""
49
+ echo -e "${GREEN}Development servers stopped${NC}"
50
+