fix: add trailing slashes to agent work orders endpoints

- add trailing slashes to prevent FastAPI mount() 307 redirects
- add defensive null check for repository_url in detail view
- fixes ERR_NAME_NOT_RESOLVED when browser follows redirect to archon-server
This commit is contained in:
Rasmus Widing
2025-10-17 09:53:53 +03:00
parent edf3a51fa5
commit 8f3e8bc220
23 changed files with 89 additions and 61 deletions

View File

@@ -8,6 +8,7 @@
"name": "archon-ui",
"version": "0.1.0",
"dependencies": {
"@hookform/resolvers": "^3.10.0",
"@mdxeditor/editor": "^3.42.0",
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-checkbox": "^1.3.3",
@@ -34,6 +35,7 @@
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.54.2",
"react-icons": "^5.5.0",
"react-markdown": "^10.1.0",
"react-router-dom": "^6.26.2",
@@ -1709,6 +1711,15 @@
"integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
"license": "MIT"
},
"node_modules/@hookform/resolvers": {
"version": "3.10.0",
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.10.0.tgz",
"integrity": "sha512-79Dv+3mDF7i+2ajj7SkypSKHhl1cbln1OGavqrsF7p6mbUv11xpqpacPsGDCTRvCSjEEIez2ef1NveSVL3b0Ag==",
"license": "MIT",
"peerDependencies": {
"react-hook-form": "^7.0.0"
}
},
"node_modules/@humanwhocodes/config-array": {
"version": "0.13.0",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",

View File

@@ -54,6 +54,8 @@
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.54.2",
"@hookform/resolvers": "^3.10.0",
"react-icons": "^5.5.0",
"react-markdown": "^10.1.0",
"react-router-dom": "^6.26.2",

View File

@@ -14,6 +14,8 @@ import { SettingsProvider, useSettings } from './contexts/SettingsContext';
import { TooltipProvider } from './features/ui/primitives/tooltip';
import { ProjectPage } from './pages/ProjectPage';
import StyleGuidePage from './pages/StyleGuidePage';
import { AgentWorkOrdersPage } from './pages/AgentWorkOrdersPage';
import { AgentWorkOrderDetailPage } from './pages/AgentWorkOrderDetailPage';
import { DisconnectScreenOverlay } from './components/DisconnectScreenOverlay';
import { ErrorBoundaryWithBugReport } from './components/bug-report/ErrorBoundaryWithBugReport';
import { MigrationBanner } from './components/ui/MigrationBanner';
@@ -43,6 +45,8 @@ const AppRoutes = () => {
) : (
<Route path="/projects" element={<Navigate to="/" replace />} />
)}
<Route path="/agent-work-orders" element={<AgentWorkOrdersPage />} />
<Route path="/agent-work-orders/:id" element={<AgentWorkOrderDetailPage />} />
</Routes>
);
};

View File

@@ -1,4 +1,4 @@
import { BookOpen, Palette, Settings } from "lucide-react";
import { BookOpen, Bot, Palette, Settings } from "lucide-react";
import type React from "react";
import { Link, useLocation } from "react-router-dom";
// TEMPORARY: Use old SettingsContext until settings are migrated
@@ -34,6 +34,12 @@ export function Navigation({ className }: NavigationProps) {
label: "Knowledge Base",
enabled: true,
},
{
path: "/agent-work-orders",
icon: <Bot className="h-5 w-5" />,
label: "Agent Work Orders",
enabled: true,
},
{
path: "/mcp",
icon: (

View File

@@ -150,7 +150,7 @@ export const KnowledgeCardTitle: React.FC<KnowledgeCardTitleProps> = ({
"focus:ring-1 focus:ring-cyan-400 px-2 py-1",
)}
/>
{description && description.trim() && (
{description?.trim() && (
<Tooltip delayDuration={200}>
<TooltipTrigger asChild>
<Info
@@ -183,7 +183,7 @@ export const KnowledgeCardTitle: React.FC<KnowledgeCardTitleProps> = ({
{title}
</h3>
</SimpleTooltip>
{description && description.trim() && (
{description?.trim() && (
<Tooltip delayDuration={200}>
<TooltipTrigger asChild>
<Info

View File

@@ -67,17 +67,17 @@ export const LevelSelector: React.FC<LevelSelectorProps> = ({ value, onValueChan
Crawl Depth
</div>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
className="text-gray-400 hover:text-cyan-500 transition-colors cursor-help"
aria-label="Show crawl depth level details"
>
<Info className="w-4 h-4" />
</button>
</TooltipTrigger>
<TooltipContent side="right">{tooltipContent}</TooltipContent>
</Tooltip>
<TooltipTrigger asChild>
<button
type="button"
className="text-gray-400 hover:text-cyan-500 transition-colors cursor-help"
aria-label="Show crawl depth level details"
>
<Info className="w-4 h-4" />
</button>
</TooltipTrigger>
<TooltipContent side="right">{tooltipContent}</TooltipContent>
</Tooltip>
</div>
<div className="text-xs text-gray-500 dark:text-gray-400">
Higher levels crawl deeper into the website structure

View File

@@ -41,10 +41,7 @@ export const ContentViewer: React.FC<ContentViewerProps> = ({ selectedItem, onCo
try {
// Escape HTML entities FIRST per Prism documentation requirement
// Prism expects pre-escaped input to prevent XSS
const escaped = code
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
const escaped = code.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
const lang = language?.toLowerCase() || "javascript";
const grammar = Prism.languages[lang] || Prism.languages.javascript;

View File

@@ -36,7 +36,7 @@ export const KnowledgeInspector: React.FC<KnowledgeInspectorProps> = ({
useEffect(() => {
setViewMode(initialTab);
setSelectedItem(null); // Clear selected item when switching tabs
}, [item.source_id, initialTab]);
}, [initialTab]);
// Use pagination hook for current view mode
const paginationData = useInspectorPagination({

View File

@@ -155,7 +155,7 @@ export function usePaginatedInspectorData({
useEffect(() => {
resetDocs();
resetCode();
}, [sourceId, enabled, resetDocs, resetCode]);
}, [resetDocs, resetCode]);
return {
documents: {

View File

@@ -1,5 +1,5 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { act, renderHook, waitFor } from "@testing-library/react";
import { renderHook, waitFor } from "@testing-library/react";
import React from "react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { ActiveOperationsResponse, ProgressResponse } from "../../types";

View File

@@ -45,7 +45,7 @@ export function useOperationProgress(
hasCalledComplete.current = false;
hasCalledError.current = false;
consecutiveNotFound.current = 0;
}, [progressId]);
}, []);
const query = useQuery<ProgressResponse | null>({
queryKey: progressId ? progressKeys.detail(progressId) : DISABLED_QUERY_KEY,
@@ -240,12 +240,12 @@ export function useMultipleOperations(
// Reset tracking sets when progress IDs change
// Use sorted JSON stringification for stable dependency that handles reordering
const progressIdsKey = useMemo(() => JSON.stringify([...progressIds].sort()), [progressIds]);
const _progressIdsKey = useMemo(() => JSON.stringify([...progressIds].sort()), [progressIds]);
useEffect(() => {
completedIds.current.clear();
errorIds.current.clear();
notFoundCounts.current.clear();
}, [progressIdsKey]); // Stable dependency across reorderings
}, []); // Stable dependency across reorderings
const queries = useQueries({
queries: progressIds.map((progressId) => ({

View File

@@ -51,7 +51,6 @@ export const ProjectCard: React.FC<ProjectCardProps> = ({
optimistic && "opacity-80 ring-1 ring-cyan-400/30",
)}
>
{/* Main content area with padding */}
<div className="flex-1 p-4 pb-2">
{/* Title section */}

View File

@@ -1,7 +1,7 @@
import { motion } from "framer-motion";
import { LayoutGrid, List, Plus, Search, X } from "lucide-react";
import type React from "react";
import { ReactNode } from "react";
import type { ReactNode } from "react";
import { Button } from "../../ui/primitives/button";
import { Input } from "../../ui/primitives/input";
import { cn } from "../../ui/primitives/styles";

View File

@@ -55,7 +55,7 @@ export const DocsTab = ({ project }: DocsTabProps) => {
await createDocumentMutation.mutateAsync({
title,
document_type,
content: { markdown: "# " + title + "\n\nStart writing your document here..." },
content: { markdown: `# ${title}\n\nStart writing your document here...` },
// NOTE: Archon does not have user authentication - this is a single-user local app.
// "User" is a constant representing the sole user of this Archon instance.
author: "User",
@@ -94,7 +94,7 @@ export const DocsTab = ({ project }: DocsTabProps) => {
setShowAddModal(false);
setShowDeleteModal(false);
setDocumentToDelete(null);
}, [projectId]);
}, []);
// Auto-select first document when documents load
useEffect(() => {

View File

@@ -52,13 +52,7 @@ export const AddDocumentModal = ({ open, onOpenChange, onAdd }: AddDocumentModal
setError(null);
onOpenChange(false);
} catch (err) {
setError(
typeof err === "string"
? err
: err instanceof Error
? err.message
: "Failed to create document"
);
setError(typeof err === "string" ? err : err instanceof Error ? err.message : "Failed to create document");
} finally {
setIsAdding(false);
}
@@ -81,7 +75,10 @@ export const AddDocumentModal = ({ open, onOpenChange, onAdd }: AddDocumentModal
)}
<div>
<label htmlFor="document-title" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
<label
htmlFor="document-title"
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
>
Document Title
</label>
<Input
@@ -96,7 +93,10 @@ export const AddDocumentModal = ({ open, onOpenChange, onAdd }: AddDocumentModal
</div>
<div>
<label htmlFor="document-type" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
<label
htmlFor="document-type"
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
>
Document Type
</label>
<Select value={type} onValueChange={setType} disabled={isAdding}>
@@ -104,11 +104,21 @@ export const AddDocumentModal = ({ open, onOpenChange, onAdd }: AddDocumentModal
<SelectValue placeholder="Select a document type" />
</SelectTrigger>
<SelectContent color="cyan">
<SelectItem value="spec" color="cyan">Specification</SelectItem>
<SelectItem value="api" color="cyan">API Documentation</SelectItem>
<SelectItem value="guide" color="cyan">Guide</SelectItem>
<SelectItem value="note" color="cyan">Note</SelectItem>
<SelectItem value="design" color="cyan">Design</SelectItem>
<SelectItem value="spec" color="cyan">
Specification
</SelectItem>
<SelectItem value="api" color="cyan">
API Documentation
</SelectItem>
<SelectItem value="guide" color="cyan">
Guide
</SelectItem>
<SelectItem value="note" color="cyan">
Note
</SelectItem>
<SelectItem value="design" color="cyan">
Design
</SelectItem>
</SelectContent>
</Select>
</div>

View File

@@ -118,7 +118,7 @@ export const DocumentCard = memo(({ document, isActive, onSelect, onDelete }: Do
aria-label={`${isActive ? "Selected: " : ""}${document.title}`}
className={cn("relative w-full cursor-pointer transition-all duration-300 group", isActive && "scale-[1.02]")}
>
<div>
<div>
{/* Document Type Badge */}
<div
className={cn(
@@ -177,7 +177,7 @@ export const DocumentCard = memo(({ document, isActive, onSelect, onDelete }: Do
<Trash2 className="w-4 h-4" aria-hidden="true" />
</Button>
)}
</div>
</div>
</Card>
);
});

View File

@@ -60,11 +60,8 @@ export const documentService = {
* Delete a document
*/
async deleteDocument(projectId: string, documentId: string): Promise<void> {
await callAPIWithETag<{ success: boolean; message: string }>(
`/api/projects/${projectId}/docs/${documentId}`,
{
method: "DELETE",
},
);
await callAPIWithETag<{ success: boolean; message: string }>(`/api/projects/${projectId}/docs/${documentId}`, {
method: "DELETE",
});
},
};

View File

@@ -3,7 +3,7 @@ import { useRef } from "react";
import { useDrop } from "react-dnd";
import { cn } from "../../../ui/primitives/styles";
import type { Task } from "../types";
import { getColumnColor, getColumnGlow, ItemTypes } from "../utils/task-styles";
import { getColumnGlow, ItemTypes } from "../utils/task-styles";
import { TaskCard } from "./TaskCard";
interface KanbanColumnProps {
@@ -90,7 +90,7 @@ export const KanbanColumn = ({
<div
className={cn(
"inline-flex items-center gap-2 px-3 py-1.5 rounded-full text-sm font-medium border backdrop-blur-md",
statusInfo.color
statusInfo.color,
)}
>
{statusInfo.icon}

View File

@@ -3,7 +3,7 @@ import { renderHook, waitFor } from "@testing-library/react";
import React from "react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { Task } from "../../types";
import { taskKeys, useCreateTask, useProjectTasks, useTaskCounts } from "../useTaskQueries";
import { taskKeys, useCreateTask, useProjectTasks } from "../useTaskQueries";
// Mock the services
vi.mock("../../services", () => ({

View File

@@ -1,13 +1,13 @@
import { useQueryClient } from "@tanstack/react-query";
import { motion } from "framer-motion";
import { Activity, CheckCircle2, FileText, LayoutGrid, List, ListTodo, Pin } from "lucide-react";
import { Activity, CheckCircle2, FileText, List, ListTodo, Pin } from "lucide-react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { useStaggeredEntrance } from "../../../hooks/useStaggeredEntrance";
import { isOptimistic } from "../../shared/utils/optimistic";
import { DeleteConfirmModal } from "../../ui/components/DeleteConfirmModal";
import { OptimisticIndicator } from "../../ui/primitives/OptimisticIndicator";
import { Button, PillNavigation, SelectableCard } from "../../ui/primitives";
import { OptimisticIndicator } from "../../ui/primitives/OptimisticIndicator";
import { StatPill } from "../../ui/primitives/pill";
import { cn } from "../../ui/primitives/styles";
import { NewProjectModal } from "../components/NewProjectModal";
@@ -71,7 +71,7 @@ export function ProjectsView({ className = "", "data-id": dataId }: ProjectsView
const sortedProjects = useMemo(() => {
// Filter by search query
const filtered = (projects as Project[]).filter((project) =>
project.title.toLowerCase().includes(searchQuery.toLowerCase())
project.title.toLowerCase().includes(searchQuery.toLowerCase()),
);
// Sort: pinned first, then alphabetically

View File

@@ -60,7 +60,7 @@ export async function callAPIWithETag<T = unknown>(endpoint: string, options: Re
// Only set Content-Type for requests that have a body (POST, PUT, PATCH, etc.)
// GET and DELETE requests should not have Content-Type header
const method = options.method?.toUpperCase() || "GET";
const _method = options.method?.toUpperCase() || "GET";
const hasBody = options.body !== undefined && options.body !== null;
if (hasBody && !headers["Content-Type"]) {
headers["Content-Type"] = "application/json";

View File

@@ -164,7 +164,7 @@ export const ComboBox = React.forwardRef<HTMLButtonElement, ComboBoxProps>(
const highlightedElement = optionsRef.current.querySelector('[data-highlighted="true"]');
highlightedElement?.scrollIntoView({ block: "nearest" });
}
}, [highlightedIndex, open]);
}, [open]);
return (
<Popover.Root open={open} onOpenChange={setOpen}>

View File

@@ -13,9 +13,10 @@ RUN apt-get update && apt-get install -y \
COPY pyproject.toml .
# Install server dependencies to a virtual environment using uv
# Install base dependencies (includes structlog) and server groups
RUN uv venv /venv && \
. /venv/bin/activate && \
uv pip install --group server --group server-reranking
uv pip install . --group server --group server-reranking
# Runtime stage
FROM python:3.12-slim
@@ -56,8 +57,9 @@ ENV PATH=/venv/bin:$PATH
ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
RUN playwright install chromium
# Copy server code and tests
# Copy server code, agent work orders, and tests
COPY src/server/ src/server/
COPY src/agent_work_orders/ src/agent_work_orders/
COPY src/__init__.py src/
COPY tests/ tests/
@@ -76,4 +78,4 @@ HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
CMD sh -c "python -c \"import urllib.request; urllib.request.urlopen('http://localhost:${ARCHON_SERVER_PORT}/health')\""
# Run the Server service
CMD sh -c "python -m uvicorn src.server.main:socket_app --host 0.0.0.0 --port ${ARCHON_SERVER_PORT} --workers 1"
CMD sh -c "python -m uvicorn src.server.main:app --host 0.0.0.0 --port ${ARCHON_SERVER_PORT} --workers 1"