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
3 changes: 3 additions & 0 deletions .claude/commands/bug.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@ Execute every command to validate the bug is fixed with zero regressions.
- `uv run pytest tests/unit` - Run unit tests
- `uv run pytest tests/integration` - Run integration tests (if applicable)

## Final Check
- Use `mcp__plugin_serena_serena__think_about_whether_you_are_done` to verify all tasks are complete.

## Notes
<optionally list any additional notes or context that are relevant to the bug that will be helpful to the developer>
```
Expand Down
3 changes: 3 additions & 0 deletions .claude/commands/chore.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ Execute every command to validate the chore is complete with zero regressions.
- `uv run pytest tests/unit` - Run unit tests
- `uv run pytest tests/integration` - Run integration tests (if applicable)

## Final Check
- Use `mcp__plugin_serena_serena__think_about_whether_you_are_done` to verify all tasks are complete.

## Notes
<optionally list any additional notes or context that are relevant to the chore that will be helpful to the developer>
```
Expand Down
3 changes: 3 additions & 0 deletions .claude/commands/feature.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,9 @@ Execute every command to validate the feature works correctly with zero regressi
- `uv run pytest tests/unit` - Run unit tests
- `uv run pytest tests/integration` - Run integration tests (if applicable)

## Final Check
- Use `mcp__plugin_serena_serena__think_about_whether_you_are_done` to verify all tasks are complete.

## Notes
<optionally list any additional notes, future considerations, or context that are relevant to the feature that will be helpful to the developer>
```
Expand Down
5 changes: 4 additions & 1 deletion .claude/commands/implement.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,7 @@ $ARGUMENTS

## Report
- Summarize the work you've just done in a concise bullet point list.
- Report the files and total lines changed with `git diff --stat`
- Report the files and total lines changed with `git diff --stat`

## Final Check
- Use `mcp__plugin_serena_serena__think_about_whether_you_are_done` to verify all tasks are complete.
18 changes: 6 additions & 12 deletions api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,8 @@
logger = logging.getLogger(__name__)


# Create MCP HTTP apps (needed for lifespan integration)
# Streamable HTTP (modern, recommended) - used by newer clients
# Create MCP HTTP app (needed for lifespan integration)
mcp_http_app = mcp_server.http_app(path="/")
# SSE transport (legacy, wider compatibility) - used by mcp-remote, older clients
mcp_sse_app = mcp_server.http_app(path="/", transport="sse")


@asynccontextmanager
Expand All @@ -63,11 +60,10 @@ async def lifespan(app: FastAPI):
except Exception as e:
logger.error(f"Failed to initialize database: {e}")

# Initialize MCP server lifespans (required for session management)
# Initialize MCP server lifespan
async with mcp_http_app.lifespan(app):
async with mcp_sse_app.lifespan(app):
logger.info("MCP server initialized (HTTP + SSE)")
yield
logger.info("MCP server initialized")
yield

# Cleanup database connection
logger.info("Shutting down pyplots API...")
Expand Down Expand Up @@ -136,10 +132,8 @@ async def add_cache_headers(request: Request, call_next):
return response


# Mount MCP servers for AI assistant integration
# Note: Apps are created earlier with lifespan integration
app.mount("/mcp", mcp_http_app) # Streamable HTTP at /mcp
app.mount("/sse", mcp_sse_app) # SSE transport at /sse
# Mount MCP server for AI assistant integration
app.mount("/mcp", mcp_http_app)

# Register routers
app.include_router(health_router)
Expand Down
4 changes: 4 additions & 0 deletions api/mcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ async def get_mcp_db_session() -> AsyncSession:
return _mcp_session_factory()


# Enable stateless HTTP mode via environment variable (recommended approach)
# This allows horizontal scaling without session affinity
os.environ.setdefault("FASTMCP_STATELESS_HTTP", "true")

# Initialize FastMCP server
mcp_server = FastMCP("pyplots")

Expand Down
88 changes: 88 additions & 0 deletions app/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import js from '@eslint/js';
import tseslint from '@typescript-eslint/eslint-plugin';
import tsparser from '@typescript-eslint/parser';
import reactHooks from 'eslint-plugin-react-hooks';
import reactRefresh from 'eslint-plugin-react-refresh';

export default [
js.configs.recommended,
{
files: ['src/**/*.{ts,tsx}'],
languageOptions: {
parser: tsparser,
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
ecmaFeatures: {
jsx: true,
},
},
globals: {
// Browser globals
window: 'readonly',
document: 'readonly',
navigator: 'readonly',
console: 'readonly',
setTimeout: 'readonly',
clearTimeout: 'readonly',
setInterval: 'readonly',
clearInterval: 'readonly',
fetch: 'readonly',
URL: 'readonly',
URLSearchParams: 'readonly',
localStorage: 'readonly',
history: 'readonly',
location: 'readonly',
requestAnimationFrame: 'readonly',
cancelAnimationFrame: 'readonly',
// DOM types
HTMLElement: 'readonly',
HTMLDivElement: 'readonly',
HTMLInputElement: 'readonly',
HTMLButtonElement: 'readonly',
HTMLAnchorElement: 'readonly',
HTMLImageElement: 'readonly',
HTMLIFrameElement: 'readonly',
Element: 'readonly',
Node: 'readonly',
NodeList: 'readonly',
// Events
MouseEvent: 'readonly',
KeyboardEvent: 'readonly',
TouchEvent: 'readonly',
ClipboardEvent: 'readonly',
Event: 'readonly',
MessageEvent: 'readonly',
// APIs
AbortController: 'readonly',
RequestInit: 'readonly',
Response: 'readonly',
ResizeObserver: 'readonly',
IntersectionObserver: 'readonly',
MutationObserver: 'readonly',
Blob: 'readonly',
File: 'readonly',
FileReader: 'readonly',
// React (for JSX runtime)
React: 'readonly',
},
},
plugins: {
'@typescript-eslint': tseslint,
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...tseslint.configs.recommended.rules,
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
'no-unused-vars': 'off',
// Allow ref updates during render (common pattern for keeping refs in sync)
'react-hooks/refs': 'off',
Comment on lines +81 to +82
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rule 'react-hooks/refs' on line 82 does not exist in eslint-plugin-react-hooks. The plugin only provides two rules: 'react-hooks/rules-of-hooks' and 'react-hooks/exhaustive-deps'. This rule should be removed or corrected to a valid rule name. If the intent was to disable exhaustive-deps warnings for ref updates, that should be handled differently (e.g., by using eslint-disable comments where needed or adjusting the exhaustive-deps configuration).

Suggested change
// Allow ref updates during render (common pattern for keeping refs in sync)
'react-hooks/refs': 'off',

Copilot uses AI. Check for mistakes.
},
},
{
ignores: ['dist/**', 'node_modules/**', '*.config.js'],
},
];
8 changes: 7 additions & 1 deletion app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"lint": "eslint src --ext ts,tsx",
"lint": "eslint src",
"type-check": "tsc --noEmit",
"test": "vitest run",
"test:watch": "vitest"
Expand All @@ -30,11 +30,17 @@
"react-syntax-highlighter": "^16.1.0"
},
"devDependencies": {
"@eslint/js": "^9.39.2",
"@types/react": "^19.2.8",
"@types/react-dom": "^19.1.7",
"@types/react-syntax-highlighter": "^15.5.13",
"@typescript-eslint/eslint-plugin": "^8.54.0",
"@typescript-eslint/parser": "^8.54.0",
"@vitejs/plugin-react-swc": "^4.0.0",
"@vitest/coverage-v8": "^4.0.17",
"eslint": "^9.39.2",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.26",
"typescript": "^5.9.2",
"vite": "^7.3.1",
"vitest": "^4.0.17"
Expand Down
18 changes: 9 additions & 9 deletions app/src/components/FilterBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ export function FilterBar({
setIsSearchManuallyExpanded(true);
setDropdownAnchor(searchContainerRef.current);
setTimeout(() => inputRef.current?.focus(), 0);
}, []);
}, [inputRef]);

// Collapse when empty and loses focus (only if there are filters)
const handleSearchBlur = useCallback(() => {
Expand All @@ -133,15 +133,17 @@ export function FilterBar({
setDropdownAnchor(null);
setSelectedCategory(null);
setSearchQuery('');
setHighlightedIndex(-1);
setIsSearchManuallyExpanded(false);
}, []);

// Select category from dropdown
const handleCategorySelect = useCallback((category: FilterCategory) => {
setSelectedCategory(category);
setSearchQuery('');
setHighlightedIndex(-1);
setTimeout(() => inputRef.current?.focus(), 50);
}, []);
}, [inputRef]);

// Select value (add new filter group)
const handleValueSelect = useCallback(
Expand All @@ -153,14 +155,15 @@ export function FilterBar({
}
setSelectedCategory(null);
setSearchQuery('');
setHighlightedIndex(-1);
// Keep expanded and focused for next filter
setIsSearchManuallyExpanded(true);
setTimeout(() => {
setDropdownAnchor(searchContainerRef.current);
inputRef.current?.focus();
}, 50);
},
[onAddFilter, onTrackEvent, searchQuery]
[onAddFilter, onTrackEvent, searchQuery, inputRef]
);

// Chip click - open chip menu
Expand Down Expand Up @@ -265,11 +268,6 @@ export function FilterBar({

const dropdownItems = getDropdownItems();

// Reset highlight when dropdown content changes
useEffect(() => {
setHighlightedIndex(-1);
}, [searchQuery, selectedCategory, dropdownAnchor]);

// Handle keyboard navigation
const handleKeyDown = useCallback(
(event: React.KeyboardEvent) => {
Expand All @@ -295,7 +293,7 @@ export function FilterBar({
inputRef.current?.blur();
}
},
[dropdownItems, highlightedIndex, handleCategorySelect, handleValueSelect, handleDropdownClose]
[dropdownItems, highlightedIndex, handleCategorySelect, handleValueSelect, handleDropdownClose, inputRef]
);

// Get active group for chip menu
Expand Down Expand Up @@ -471,6 +469,7 @@ export function FilterBar({
value={searchQuery}
onChange={(e) => {
setSearchQuery(e.target.value);
setHighlightedIndex(-1);
if (!dropdownAnchor) {
setDropdownAnchor(searchContainerRef.current);
}
Expand All @@ -480,6 +479,7 @@ export function FilterBar({
setIsSearchManuallyExpanded(true);
}
setDropdownAnchor(searchContainerRef.current);
setHighlightedIndex(-1);
}}
onBlur={handleSearchBlur}
onKeyDown={handleKeyDown}
Expand Down
31 changes: 22 additions & 9 deletions app/src/components/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,45 +24,58 @@ export function Footer({ onTrackEvent, selectedSpec, selectedLibrary }: FooterPr
}}
>
<Link
href="https://www.linkedin.com/in/markus-neusinger/"
href={GITHUB_URL}
target="_blank"
rel="noopener noreferrer"
onClick={() => onTrackEvent?.('external_link', { destination: 'linkedin', spec: selectedSpec, library: selectedLibrary })}
onClick={() => onTrackEvent?.('external_link', { destination: 'github', spec: selectedSpec, library: selectedLibrary })}
sx={{
color: '#9ca3af',
textDecoration: 'none',
'&:hover': { color: '#6b7280' },
}}
>
markus neusinger
github
</Link>
<span>·</span>
<Link
href={GITHUB_URL}
href="https://plausible.io/pyplots.ai"
target="_blank"
rel="noopener noreferrer"
onClick={() => onTrackEvent?.('external_link', { destination: 'github', spec: selectedSpec, library: selectedLibrary })}
onClick={() => onTrackEvent?.('external_link', { destination: 'stats', spec: selectedSpec, library: selectedLibrary })}
sx={{
color: '#9ca3af',
textDecoration: 'none',
'&:hover': { color: '#6b7280' },
}}
>
github
stats
</Link>
<span>·</span>
<Link
href="https://plausible.io/pyplots.ai"
href="https://www.linkedin.com/in/markus-neusinger/"
target="_blank"
rel="noopener noreferrer"
onClick={() => onTrackEvent?.('external_link', { destination: 'stats', spec: selectedSpec, library: selectedLibrary })}
onClick={() => onTrackEvent?.('external_link', { destination: 'linkedin', spec: selectedSpec, library: selectedLibrary })}
sx={{
color: '#9ca3af',
textDecoration: 'none',
'&:hover': { color: '#6b7280' },
}}
>
stats
markus neusinger
</Link>
<span>·</span>
<Link
component={RouterLink}
to="/mcp"
onClick={() => onTrackEvent?.('internal_link', { destination: 'mcp', spec: selectedSpec, library: selectedLibrary })}
sx={{
color: '#9ca3af',
textDecoration: 'none',
'&:hover': { color: '#6b7280' },
}}
>
mcp
</Link>
<span>·</span>
<Link
Expand Down
Loading
Loading