Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 81 additions & 31 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,54 +1,104 @@
# .github/workflows/ci.yml

name: CI

on:
push:
branches: [ "main" ]
branches:
- "main"
pull_request:
branches: [ "main" ]
branches:
- "main"
workflow_dispatch:

env:
NODE_VERSION: "20.x"
PYTHON_VERSION: "3.11"
NEXT_TELEMETRY_DISABLED: "1"
CI: "true"

jobs:
build-and-test:
name: Build and Test
backend-tests:
name: Backend Tests
runs-on: ubuntu-latest

steps:
# 1) Check out repository code
- name: Check out code
uses: actions/checkout@v3
- name: Check out repository
uses: actions/checkout@v4

# 2) Set up Python
- name: Set up Python
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: "3.9"
python-version: ${{ env.PYTHON_VERSION }}
cache: pip
cache-dependency-path: python_backend/requirements.txt

# 3) Install Python dependencies
- name: Install Python dependencies
- name: Install backend dependencies
run: |
python -m pip install --upgrade pip
pip install -r python_backend/requirements.txt
python -m venv python_backend/venv
python_backend/venv/bin/pip install --upgrade pip
python_backend/venv/bin/pip install -r python_backend/requirements.txt

# 4) Run Python unit tests
- name: Run Backend Tests
run: |
python -m pytest -q python_backend/tests
- name: Run backend unit tests
run: python_backend/venv/bin/pytest -q python_backend/tests

frontend-quality:
name: Frontend Quality Gate
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v4

# 5) Set up Node.js
- name: Set up Node.js
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: "20.x"
node-version: ${{ env.NODE_VERSION }}
cache: npm

# 6) Install Node dependencies (CI-friendly)
- name: Install Node Dependencies
- name: Install frontend dependencies
run: npm ci

# 7) Build the Next.js app
- name: Build Next.js
- name: Run ESLint
run: npm run lint

- name: Type-check with TypeScript
run: npx tsc --noEmit

- name: Build Next.js app
run: npm run build

# 8) Run the auto-tests (Python + Next.js)
- name: Run AutoTests
run: node scripts/autotest.js
integration-tests:
name: Integration AutoTests
runs-on: ubuntu-latest
needs:
- backend-tests
- frontend-quality
timeout-minutes: 25
steps:
- name: Check out repository
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
cache: pip
cache-dependency-path: python_backend/requirements.txt

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: npm

- name: Install backend dependencies
run: |
python -m venv python_backend/venv
python_backend/venv/bin/pip install --upgrade pip
python_backend/venv/bin/pip install -r python_backend/requirements.txt

- name: Install frontend dependencies
run: npm ci

- name: Run end-to-end AutoTests
env:
PORT: 3010
BACKEND_PORT: 5010
run: npm run test
20 changes: 17 additions & 3 deletions python_backend/utils/response_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,20 @@ def success_response(data=None, message=None, status_code=200):
return jsonify(response), status_code

def error_response(error, message="An error occurred", status_code=400):
"""Generates a standardized error JSON response."""
response = {"success": False, "error": error, "message": message}
return jsonify(response), status_code
"""Generates a standardized error JSON response.

NOTE: Older controller code sometimes called ``error_response(err, 404)``
assuming the second positional argument was the status code. To remain
backwards compatible we detect that usage and treat numeric ``message``
values as the status code instead of the human readable message.
"""

if isinstance(message, int) and status_code == 400:
status_code = message
message = None

response = {"success": False, "error": error}
if message:
response["message"] = message

return jsonify(response), status_code
40 changes: 31 additions & 9 deletions scripts/autotest.js
Original file line number Diff line number Diff line change
Expand Up @@ -191,14 +191,25 @@ function getPortConfig() { /* ... same as before ... */ }
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ baseDir, paths: ["non_existent_file_12345.txt"] }),
});
if (!resp.ok) throw new Error("Expected HTTP 200 but got " + resp.status);
const data = await resp.json();
if (!data.success) throw new Error("Expected success=true in response");
if (!Array.isArray(data.data) || data.data.length !== 1) throw new Error("Expected exactly one file result");
// Check for the specific error message prefix
if (resp.status !== 404) {
throw new Error(`Expected HTTP 404 but got ${resp.status}`);
}

let data = {};
try {
data = await resp.json();
} catch {
/* ignore parse errors */
}

if (data.success !== false) {
throw new Error("Expected success=false in response");
}

const fileNotFoundPrefix = "File not found on server:";
if (!data.data[0].content || !data.data[0].content.startsWith(fileNotFoundPrefix)) {
throw new Error(`Expected a '${fileNotFoundPrefix}' message, got: "${data.data[0].content?.substring(0,100)}..."`);
const errorMessage = typeof data.error === 'string' ? data.error : String(data.message ?? '');
if (!errorMessage.startsWith(fileNotFoundPrefix)) {
throw new Error(`Expected a '${fileNotFoundPrefix}' message, got: "${errorMessage.substring(0, 100)}..."`);
}
});

Expand Down Expand Up @@ -231,8 +242,19 @@ function getPortConfig() { /* ... same as before ... */ }

let found = false;
for (const path of scriptUrls) {
const url = path.startsWith('http') ? path : `${FRONTEND_BASE_URL}${path}`;
const jsResp = await fetch(url);
let url;
try {
url = new URL(path, FRONTEND_BASE_URL).toString();
} catch {
continue; // Skip malformed script URLs
}

let jsResp;
try {
jsResp = await fetch(url);
} catch {
continue;
}
if (!jsResp.ok) continue;
const js = await jsResp.text();
if (js.includes('execCommand("copy"') || js.includes("execCommand('copy'")) {
Expand Down
27 changes: 15 additions & 12 deletions views/CodemapPreviewModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,11 @@ export default function CodemapPreviewModal({}: Props) {
const [hoveredRowIndex, setHoveredRowIndex] = useState<number | null>(null);
const [viewMode, setViewMode] = useState<ViewMode>("overview");
const [selectedFileType, setSelectedFileType] = useState<string>("all");

const dataToDisplay = codemapModalData || {};

const dataToDisplay = useMemo<Record<string, EnhancedFileInfo>>(
() => codemapModalData ?? {},
[codemapModalData]
);

// Enhanced analytics and metrics
const analytics = useMemo(() => {
Expand Down Expand Up @@ -159,19 +162,19 @@ export default function CodemapPreviewModal({}: Props) {
if (info.imports) {
moduleGraph[file] = new Set();
info.imports.forEach(imp => {
const module = imp.module;
moduleGraph[file].add(module);
if (!reverseGraph[module]) {
reverseGraph[module] = new Set();
const moduleName = imp.module;
moduleGraph[file].add(moduleName);

if (!reverseGraph[moduleName]) {
reverseGraph[moduleName] = new Set();
}
reverseGraph[module].add(file);
reverseGraph[moduleName].add(file);

// Classify as internal or external
if (module.startsWith('.') || module.startsWith('/') || files.some(f => f.includes(module))) {
internalModules.add(module);
if (moduleName.startsWith('.') || moduleName.startsWith('/') || files.some(f => f.includes(moduleName))) {
internalModules.add(moduleName);
} else {
externalModules.add(module);
externalModules.add(moduleName);
}
});
}
Expand Down
18 changes: 8 additions & 10 deletions views/FileTreeView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {
GitBranch,
Code,
Database,
Image,
Image as ImageIcon,
FileText,
Package,
} from "lucide-react";
Expand Down Expand Up @@ -174,10 +174,10 @@ const FileTreeView = forwardRef<FileTreeViewHandle, Props>(
yaml: <Database className="h-4 w-4 text-[rgb(var(--color-accent-4))]" />,
md: <FileText className="h-4 w-4 text-[rgb(var(--color-text-primary))]" />,
txt: <FileText className="h-4 w-4 text-[rgb(var(--color-text-secondary))]" />,
png: <Image className="h-4 w-4 text-[rgb(var(--color-accent-1))]" />,
jpg: <Image className="h-4 w-4 text-[rgb(var(--color-accent-1))]" />,
jpeg: <Image className="h-4 w-4 text-[rgb(var(--color-accent-1))]" />,
svg: <Image className="h-4 w-4 text-[rgb(var(--color-accent-1))]" />,
png: <ImageIcon className="h-4 w-4 text-[rgb(var(--color-accent-1))]" />,
jpg: <ImageIcon className="h-4 w-4 text-[rgb(var(--color-accent-1))]" />,
jpeg: <ImageIcon className="h-4 w-4 text-[rgb(var(--color-accent-1))]" />,
svg: <ImageIcon className="h-4 w-4 text-[rgb(var(--color-accent-1))]" />,
git: <GitBranch className="h-4 w-4 text-[rgb(var(--color-accent-4))]" />,
lock: <Package className="h-4 w-4 text-[rgb(var(--color-text-muted))]" />,
};
Expand Down Expand Up @@ -215,11 +215,9 @@ const FileTreeView = forwardRef<FileTreeViewHandle, Props>(
const isOpen = isDir && !collapsed.has(node.absolutePath);
const rowId = node.absolutePath;

const selectableFiles = useMemo(() => {
const origin = findNodeByPath(fullTree, node.relativePath) ?? node;
return collectFileDescendants(origin);
}, [node, fullTree]);

const origin = findNodeByPath(fullTree, node.relativePath) ?? node;
const selectableFiles = collectFileDescendants(origin);

const isEmpty = selectableFiles.length === 0;
const checked = !isEmpty && selectableFiles.every((p) => selectedSet.has(p));
const partial = !isEmpty && !checked && selectableFiles.some((p) => selectedSet.has(p));
Expand Down
19 changes: 9 additions & 10 deletions views/FolderBrowserView.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// views/FolderBrowserView.tsx
// FIX: Clear search input when browsing into a new folder.

import React, { useEffect, useState } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import {
ChevronLeft,
Folder,
Expand Down Expand Up @@ -77,21 +77,20 @@ export default function FolderBrowserView({
if (!isOpen) return;
void loadDrives();
if (currentPath) void browse(currentPath);
// eslint‑disable‑next‑line react-hooks/exhaustive-deps
}, [isOpen]);
}, [isOpen, currentPath, loadDrives, browse]);

/* -------------- helpers --------------- */
async function fetchJson(url: string) {
const fetchJson = useCallback(async (url: string) => {
const r = await fetch(url);
if (!r.ok) {
const msg = `${r.status} ${r.statusText}`;
throw new Error(msg);
}
return r.json();
}
}, []);

/* -------------- API calls -------------- */
async function loadDrives() {
const loadDrives = useCallback(async () => {
try {
setLoading(true);
const raw = await fetchJson(`${API}/api/select_drives`);
Expand All @@ -103,9 +102,9 @@ export default function FolderBrowserView({
} finally {
setLoading(false);
}
}
}, [fetchJson]);

async function browse(dir: string) {
const browse = useCallback(async (dir: string) => {
try {
setLoading(true);
setSearch(''); // <<< FIX: Clear search term when browsing
Expand All @@ -125,7 +124,7 @@ export default function FolderBrowserView({
} finally {
setLoading(false);
}
}
}, [fetchJson]);

/* -------------- derived --------------- */
// Filter folders based on the search state
Expand Down Expand Up @@ -263,4 +262,4 @@ export default function FolderBrowserView({
</DialogContent>
</Dialog>
);
}
}
4 changes: 2 additions & 2 deletions views/KanbanBoardView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -712,7 +712,7 @@ const KanbanBoardView: React.FC = () => {
<span className="text-xs text-[rgb(var(--color-text-muted))]">Active filters:</span>
{searchTerm && (
<Badge variant="secondary" className="text-xs">
Search: "{searchTerm}"
Search: &ldquo;{searchTerm}&rdquo;
<Button
variant="ghost"
size="icon"
Expand Down Expand Up @@ -790,4 +790,4 @@ const KanbanBoardView: React.FC = () => {
);
};

export default KanbanBoardView;
export default KanbanBoardView;
Loading
Loading