Multi-layer workflow for creating complete panel features by analyzing existing patterns and generating coordinated server routes, services, UI components, regi
复制下面这句话,粘贴给 Claude Code、Codex、Cursor 等 AI 编程工具,它会读取安装说明并在你确认后完成安装。
请阅读 https://ai.atlankj.com/install/asset/gh-create-full-stack-panel-feature-with-server-wiring-325325932b56 ,按照其中的说明把「create-full-stack-panel-feature-with-server-wiring」安装到你(当前 AI 工具)中。执行前先告诉我将运行的命令和写入的位置,等我确认。
查看 AI 将读取的安装说明正在读取 GitHub 原文…
内容来自 GitHub 原始文件,由原作者维护。在 GitHub 查看
This skill guides you through creating a complete, pattern-consistent panel feature across all application layers: backend API routes, service logic, frontend components, registration, styling — and crucially, wiring the new route handler into the server's dispatch table, whether that uses Express or a manual http.createServer pattern.
http.createServer dispatch loop instead of Express router mountingLocate existing examples for each layer you need to implement:
# Find existing server routes
find . -name "*route*.js" -o -name "*routes*.js" -o -name "*route*.ts" -o -name "*routes*.ts" | grep -E "(panel|dashboard|server)"
# Find service files
find . -name "*service*.js" -o -name "*service*.ts" | grep -E "(panel|dashboard|server)"
# Find UI components
find . -name "*.jsx" -o -name "*.tsx" -o -name "*.ts" | grep -i panel
# Find the server entry point (critical for wiring step)
find . -name "index.ts" -o -name "index.js" -o -name "server.ts" -o -name "server.js" | grep -v node_modules | head -10
# Find registration/config files
find . -name "*config*.js" -o -name "*registry*.js" -o -name "*config*.ts" -o -name "*registry*.ts"
# Find styling files
find . -name "*.css" -o -name "*.scss" | grep -i panel
Read 1-2 representative examples from each layer to understand:
MetricsPanel, metrics-service.ts)Also read the server entry point (server/index.ts, src/server.ts, etc.) to determine which dispatch pattern is used:
# Read the server entry point to identify routing mechanism
cat server/index.ts # or server/index.js, src/server.ts, etc.
Example analysis checklist:
Server Route Pattern:
- ✓ Endpoint naming: /api/panels/{type}
- ✓ Authentication middleware
- ✓ Response format: { success, data, error }
Service Pattern:
- ✓ Export structure: class vs functions
- ✓ Data transformation logic
- ✓ Error propagation
Component Pattern:
- ✓ Props interface
- ✓ State management (hooks/class)
- ✓ Data fetching approach
- ✓ Loading/error states
Registration Pattern:
- ✓ Registry file location
- ✓ Registration format (array/object)
- ✓ Required metadata fields
Server Dispatch Pattern:
- ✓ Express app.use() / router.use() → Express routing
- ✓ switch/case on url/pathname → Manual dispatch table
- ✓ if/else if chain on url → Manual dispatch chain
- ✓ handler map object { '/path': fn }→ Handler map pattern
Based on patterns, list the files you need to create and the existing files you need to update:
Typical full-stack panel structure:
server/routes/system-health-route.ts)server/services/system-health-service.ts)client/panels/SystemHealthPanel.ts)client/config/panel-registry.ts)client/styles/system-health-panel.css)types/system-health.ts)server/index.ts to import and dispatch to the new handler)Before selecting templates in Step 4, determine the frontend framework in use:
# Check for React
grep -s "react" package.json | head -5
# Check for TypeScript without React (vanilla TS)
ls client/components/*.ts 2>/dev/null | head -3
ls client/panels/*.ts 2>/dev/null | head -3
# Check for a Panel base class (common in vanilla TS dashboards)
grep -r "class.*Panel" client/ --include="*.ts" -l | head -3
grep -r "extends Panel" client/ --include="*.ts" -l | head -3
Decision rule:
react or react-dom is present in package.json → use the React/JSX template (Step 4C-React)..ts files extending a Panel base class → use the Vanilla TS template (Step 4C-Vanilla).Read the server entry point and classify it:
# Look for Express usage
grep -n "express\|app\.use\|router\." server/index.ts 2>/dev/null || \
grep -n "express\|app\.use\|router\." server/index.js 2>/dev/null || \
grep -n "express\|app\.use\|router\." src/server.ts 2>/dev/null
# Look for manual http.createServer dispatch
grep -n "createServer\|switch.*url\|switch.*pathname\|req\.url\|req\.pathname" server/index.ts 2>/dev/null || \
grep -n "createServer\|switch.*url\|switch.*pathname\|req\.url\|req\.pathname" server/index.js 2>/dev/null
Decision rule:
app.use('/api/feature', featureRouter) style → Express pattern (Step 5A)switch(pathname) { case '/api/feature': ... } style → Manual switch/case pattern (Step 5B)if (url.startsWith('/api/feature')) style → Manual if/else chain (Step 5C){ '/api/feature': handlerFn } map style → Handler map pattern (Step 5D)Generate files in dependency order (backend → frontend → registration):
⚠️ Unicode / multi-byte character warning
write_filemay fail with'unknown error'(or silently produce a truncated file) when the content contains multi-byte Unicode characters such as emoji (e.g. document/chart icons) or box-drawing characters (e.g. ─ │ ╔). Prevention: prefer ASCII-safe equivalents in generated source code — e.g.[OK]instead of a checkmark emoji,->instead of an arrow, plain hyphens/pipes instead of box-drawing chars. Recovery: if awrite_filecall returns an error or the resulting file is empty or truncated, fall back torun_shellwith a heredoc:# Recovery path -- write file content via shell heredoc (avoids write_file Unicode bug) cat > path/to/file.ts << 'HEREDOC' // file content here -- ensure no raw emoji or box-drawing chars are present HEREDOCVerify the file was written correctly with
wc -l path/to/file.tsorhead -5 path/to/file.tsafter every write that previously failed.
Express style:
// server/routes/{feature}-route.js
const express = require('express');
const router = express.Router();
const featureService = require('../services/{feature}-service');
router.get('/api/panels/{feature}', async (req, res) => {
try {
const data = await featureService.getData();
res.json({ success: true, data });
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
module.exports = router;
Manual dispatch style (TypeScript):
// server/routes/{feature}-route.ts
import { IncomingMessage, ServerResponse } from 'http';
import { featureService } from '../services/{feature}-service';
export async function handle{Feature}Request(
req: IncomingMessage,
res: ServerResponse,
action?: string
): Promise<void> {
try {
const data = await featureService.getData(action);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: true, data }));
} catch (err) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: false, error: err instanceof Error ? err.message : String(err) }));
}
}
// server/services/{feature}-service.ts
export interface {Feature}Data {
// Define your data shape
}
export async function getData(action?: string): Promise<{Feature}Data> {
// Implementation following existing service patterns
// - Data fetching
// - Business logic
// - Data transformation
return {} as {Feature}Data;
}
Use this template only when React is confirmed in Step 3b.
// client/components/{Feature}Panel.jsx
import React, { useState, useEffect } from 'react';
import './styles/{feature}-panel.css';
const FeaturePanel = () => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch('/api/panels/{feature}')
.then(res => res.json())
.then(result => {
setData(result.data);
setLoading(false);
})
.catch(err => {
setError(err.message);
setLoading(false);
});
}, []);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<div className="{feature}-panel">
{/* Render data following UI patterns */}
</div>
);
};
export default FeaturePanel;
Use this template when components extend a Panel base class (no React).
// client/panels/{Feature}Panel.ts
import { Panel } from '../core/Panel';
export class FeaturePanel extends Panel {
private intervalId: number | null = null;
constructor(id: string) {
super(id);
this.setTitle('Feature Display Name');
}
async onLoad(): Promise<void> {
if (this.isFetching) return;
this.isFetching = true;
try {
const res = await fetch('/api/panels/{feature}', {
headers: { Authorization: `Bearer ${localStorage.getItem('token') ?? ''}` }
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const { data } = await res.json();
this.render(data);
} catch (err) {
this.showError(err instanceof Error ? err.message : String(err), () => this.onLoad());
} finally {
this.isFetching = false;
}
}
private render(data: unknown): void {
const container = document.createElement('div');
container.className = '{feature}-panel';
// Build DOM nodes from data instead of JSX:
// const item = document.createElement('p');
// item.textContent = String(data);
// container.appendChild(item);
this.setContent(container);
}
// Call this to start polling (optional)
startPolling(intervalMs = 60_000): void {
this.onLoad();
this.intervalId = window.setInterval(() => this.onLoad(), intervalMs);
}
destroy(): void {
if (this.intervalId !== null) clearInterval(this.intervalId);
super.destroy();
}
}
Key differences from React template:
Panel base class; uses setContent() / showError() / setTitle() / setCount() instead of React state.document.createElement / innerHTML instead of JSX.useState / useEffect; lifecycle is onLoad() + destroy()..ts, not .jsx/.tsx.React projects — import the component directly:
// client/config/panel-registry.js (update existing)
import FeaturePanel from '../components/FeaturePanel';
export const panels = [
// ... existing panels
{
id: '{feature}',
name: 'Feature Display Name',
component: FeaturePanel,
icon: 'icon-name',
category: 'appropriate-category'
}
];
Vanilla TS projects — register via factory:
// client/config/panel-registry.ts (update existing)
import { FeaturePanel } from '../panels/FeaturePanel';
export const panels = [
// ... existing panels
{
id: '{feature}',
name: 'Feature Display Name',
factory: (id: string) => new FeaturePanel(id),
icon: 'icon-name',
category: 'appropriate-category'
}
];
/* client/styles/{feature}-panel.css */
.{feature}-panel {
/* Follow existing panel styling conventions */
padding: 1rem;
border-radius: 4px;
}
.{feature}-panel__header {
/* Consistent header styling */
}
This step is frequently missed and causes features to silently return 404. After creating the route handler, you must register it with the server's request dispatcher.
First, re-read the server entry point to confirm the exact dispatch pattern:
cat server/index.ts # adjust path as needed
Add a require/import and an app.use() call alongside the existing routes:
// server/index.ts — Express style
import featureRouter from './routes/{feature}-route';
// Place with other app.use() route registrations
app.use('/api/panels/{feature}', featureRouter);
Checklist:
app.use() call is placed before any catch-all 404 handlerThis is the pattern used when the server is built with http.createServer and routes are dispatched via a switch on the URL pathname. Read the existing cases to match the exact style, then add a new case:
// server/index.ts — manual switch/case dispatch
import { handle{Feature}Request } from './routes/{feature}-route';
// Inside the createServer callback, find the switch block:
const url = new URL(req.url ?? '/', `http://${req.headers.host}`);
const pathname = url.pathname;
const action = url.searchParams.get('action') ?? undefined;
switch (pathname) {
// ... existing cases ...
case '/api/panels/{feature}':
await handle{Feature}Request(req, res, action);
break;
// ... keep the default/404 case last ...
default:
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Not found' }));
}
Checklist:
case is placed before the default casebreak (or return) statement is present after the handler callfetch('/api/panels/{feature}') callaction / query-param forwarding matches the handler's signatureIf the server uses an if/else if chain instead of switch:
// server/index.ts — if/else chain
import { handle{Feature}Request } from './routes/{feature}-route';
// Inside the createServer callback:
if (pathname === '/api/panels/existing-a') {
await handleExistingA(req, res);
} else if (pathname === '/api/panels/existing-b') {
await handleExistingB(req, res);
// ↓ Add your new branch before the final else/404 block
} else if (pathname === '/api/panels/{feature}') {
await handle{Feature}Request(req, res, action);
} else {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Not found' }));
}
Checklist:
else if branch is before the final else (404) blockIf the server uses a map object to look up handlers:
// server/index.ts — handler map
import { handle{Feature}Request } from './routes/{feature}-route';
const handlers: Record<string, RequestHandler> = {
'/api/panels/existing-a': handleExistingA,
'/api/panels/existing-b': handleExistingB,
// ↓ Add your new entry
'/api/panels/{feature}': handle{Feature}Request,
};
Checklist:
After updating the server entry point, confirm the wiring is complete:
# Confirm the import exists
grep -n "handle{Feature}Request\|{feature}-route" server/index.ts
# Confirm the dispatch entry exists
grep -n "{feature}" server/index.ts
# Confirm no syntax errors (TypeScript projects)
npx tsc --noEmit 2>&1 | head -20
# Quick smoke test: start the server and curl the endpoint
# curl http://localhost:PORT/api/panels/{feature}
After creating all files and wiring the server, check:
Verify that all pieces connect:
# Server route handler file exists
ls server/routes/{feature}-route.*
# Server entry point imports and dispatches the new handler
grep -n "{feature}" server/index.ts # or server/index.js
# Component is imported in registry
grep -r "import.*{Feature}Panel" client/config/
# Styles are imported
grep -r "import.*{feature}-panel.css" client/
case in a switch block causes silent 404scase after the default case in a switch (unreachable code)For simple features: May omit service layer if route logic is trivial
For complex features: May need additional files:
For TypeScript projects: Add .d.ts or .ts type definition files
For Express projects: Step 5A covers the wiring; it is usually straightforward (app.use()).
For manual http.createServer projects: Steps 5B–5D are critical. Always read the server entry point before assuming the pattern — projects vary between switch/case, if/else chains, and handler maps.