# skill_records

skill_id: codebase-pattern-analyzer__v0_771e3d04
name: codebase-pattern-analyzer
path: example/skills/codebase-pattern-analyzer/SKILL.md
category: workflow
visibility: private
lineage_origin: captured
lineage_source_task_id: phase0-worldmonitor-analysis
lineage_created_at: 2026-03-10T15:13:59.460209
lineage_created_by: clawskills-evolution-pipeline
first_seen: 2026-03-10T15:13:59.460235
last_updated: 2026-03-10T15:13:59.460235
description:
Analyze a reference codebase to discover and extract reusable architectural patterns. Produces structured pattern descriptions that can be turned into standalone skills. This is a meta-skill that bootstraps the skill evolution chain.

lineage_change_summary:
Captured from WorldMonitor codebase analysis: Analyze a reference codebase to discover and extract reusable architectural patterns. Produces struc

lineage_content_snapshot:
{"SKILL.md": "---\nname: codebase-pattern-analyzer\ndescription: Analyze a reference codebase to discover and extract reusable architectural patterns. Produces structured pattern descriptions that can be turned into standalone skills. This is a meta-skill that bootstraps the skill evolution chain.\n---\n\n# Codebase Pattern Analyzer\n\nSystematically analyze a reference codebase and extract reusable architectural patterns into structured descriptions. These descriptions become the raw material for new skills (via the `skill-template-generator` skill).\n\n## When to Use\n\n- You need to understand how a large codebase is structured before replicating it\n- You want to extract a specific pattern (component architecture, service layer, API proxy, etc.) from a reference project\n- You are bootstrapping a new project that should follow the conventions of an existing one\n\n## Step 1: Map the Directory Structure\n\nRead the top-level directory and identify the major code zones:\n\n```\nlist_dir <project_root>\nlist_dir <project_root>/src\n```\n\nClassify each directory into one of these roles:\n\n| Role | Typical Dirs | What to Look For |\n|------|-------------|------------------|\n| **Components** | `src/components/` | UI classes, base class inheritance, DOM manipulation |\n| **Services** | `src/services/` | Data fetching, API calls, circuit breakers, caching |\n| **Config** | `src/config/` | Constants, panel definitions, API endpoints, feature flags |\n| **Styles** | `src/styles/` | CSS custom properties, theme variables, grid layout |\n| **Utils** | `src/utils/` | Helper functions, formatting, DOM utilities |\n| **API Layer** | `api/`, `server/` | Serverless functions, route handlers, CORS, proxy logic |\n| **Entry Point** | `src/main.ts` | Bootstrap, panel instantiation, scheduler setup |\n\n## Step 2: Identify the Component Pattern\n\nFind the base component class (usually the most imported file in `components/`):\n\n```\nread_file <project_root>/src/components/Panel.ts\n```\n\nExtract these structural elements:\n\n1. **Constructor options interface** — what config each component accepts (id, title, className, etc.)\n2. **DOM structure** — how the element tree is built (header, content area, resize handles)\n3. **Lifecycle methods** — constructor, destroy, show/hide, refresh\n4. **State management** — loading/error/content states, fetching guards\n5. **Data binding** — how data flows from service → render → DOM\n\nRecord the pattern as:\n```\nCOMPONENT PATTERN:\n  Base class: [name]\n  Options: [list of constructor params]\n  DOM tree: [element hierarchy]\n  States: [loading, error, content]\n  Lifecycle: [init → fetch → render → destroy]\n  Key methods: [getElement, showLoading, showError, setContent, refresh, destroy]\n```\n\n## Step 3: Identify the Service Layer Pattern\n\nExamine 2-3 service files to find the common structure:\n\n```\nread_file <project_root>/src/services/news.ts\nread_file <project_root>/src/services/stock-market.ts\n```\n\nLook for:\n\n1. **Circuit breaker / resilience** — retry logic, cooldown, cached fallback\n2. **Interface definitions** — typed data shapes returned by each service\n3. **Fetch pattern** — how HTTP calls are made (direct fetch, proxy, batching)\n4. **Caching strategy** — TTLs, stale-while-revalidate, in-memory cache\n5. **Error handling** — what happens on failure, default return values\n\nRecord as:\n```\nSERVICE PATTERN:\n  Resilience: [circuit breaker with N failures → cooldown]\n  Cache: [in-memory, TTL=Xms]\n  Fetch: [browser fetch → /api/... proxy → external API]\n  Error: [catch → recordFailure → return default]\n  Exports: [async functions, not class instances]\n```\n\n## Step 4: Identify the API Proxy Pattern\n\nRead the server-side proxy layer:\n\n```\nlist_dir <project_root>/api/\nread_file <project_root>/api/_cors.js\nread_file <project_root>/api/stocks.ts    # or similar endpoint\n```\n\nExtract:\n\n1. **File-per-endpoint structure** — one file per API domain\n2. **CORS handling** — origin allowlist, preflight response\n3. **API key isolation** — `process.env.*` usage, never exposed to frontend\n4. **Cache headers** — `Cache-Control`, `s-maxage`, `stale-while-revalidate`\n5. **Error wrapping** — structured JSON errors, never raw upstream responses\n6. **Input validation** — query parameter checks before upstream call\n\n## Step 5: Identify the Styling System\n\n```\nread_file <project_root>/src/styles/main.css   # first 100 lines for CSS variables\n```\n\nExtract:\n\n1. **CSS custom properties** — color tokens, font stacks, spacing\n2. **Theme structure** — dark/light mode switch mechanism\n3. **Grid layout** — `grid-template-columns`, `auto-fill`, `minmax()` values\n4. **Component styles** — panel base styles, header, content, scrollbar\n5. **Semantic colors** — positive/negative, severity levels, status indicators\n6. **Typography** — font family, sizes, letter-spacing, text-transform\n\n## Step 6: Identify the Scheduling / Refresh Pattern\n\n```\nread_file <project_root>/src/app/refresh-scheduler.ts\n```\n\nExtract:\n\n1. **Registration API** — how panels register their refresh functions\n2. **Interval management** — per-panel configurable intervals\n3. **Visibility awareness** — pause when tab hidden, flush stale on return\n4. **In-flight guards** — prevent duplicate concurrent refreshes\n5. **Stagger logic** — avoid API burst when resuming\n\n## Step 7: Synthesize Into Pattern Descriptions\n\nFor each pattern discovered, produce a structured description:\n\n```markdown\n## Pattern: [Name]\n\n**Source file(s)**: [paths in reference codebase]\n**Category**: component | service | api | style | scheduler | utility\n\n### Structure\n[Key structural elements — interfaces, classes, functions]\n\n### Key Code\n[Minimal representative code snippet, 20-40 lines]\n\n### Conventions\n[Naming, file organization, import paths]\n\n### Dependencies\n[Other patterns this depends on]\n\n### Adaptation Notes\n[What must change when applying to a different project]\n```\n\n## Output\n\nThe final output should be a list of pattern descriptions, one per architectural concern. These become the input to the `skill-template-generator` skill, which turns each pattern into a standalone SKILL.md.\n\n## Tips\n\n- **Read broadly first, deeply second** — scan directory listings before reading individual files\n- **Follow imports** — if a component imports from `../services/`, read that service next\n- **Count instances** — if 10+ components extend the same base class, that's a core pattern\n- **Note what's NOT used** — no framework (React, Vue) means vanilla DOM; no ORM means raw fetch\n- **Compare 2-3 examples** of the same pattern to distinguish the template from the instance-specific parts\n\n"}


skill_id: skill-template-generator__v0_72897a0d
name: skill-template-generator
path: example/skills/skill-template-generator/SKILL.md
category: workflow
visibility: private
lineage_origin: captured
lineage_source_task_id: phase0-worldmonitor-analysis
lineage_created_at: 2026-03-10T15:13:59.460420
lineage_created_by: clawskills-evolution-pipeline
first_seen: 2026-03-10T15:13:59.460425
last_updated: 2026-03-10T15:13:59.460425
description:
Generate properly-formatted SKILL.md files from extracted architectural patterns. Turns raw pattern descriptions into reusable skills that ClawSkills can discover, select, and evolve.

lineage_change_summary:
Captured from WorldMonitor codebase analysis: Generate properly-formatted SKILL.md files from extracted architectural patterns. Turns raw pattern 

lineage_content_snapshot:
{"SKILL.md": "---\nname: skill-template-generator\ndescription: Generate properly-formatted SKILL.md files from extracted architectural patterns. Turns raw pattern descriptions into reusable skills that ClawSkills can discover, select, and evolve.\n---\n\n# Skill Template Generator\n\nGiven a structured pattern description (from `codebase-pattern-analyzer` or manual analysis), generate a valid SKILL.md file that ClawSkills can register, select for tasks, and evolve over time.\n\n## When to Use\n\n- After extracting a pattern from a reference codebase, you need to package it as a reusable skill\n- You want to create a new skill that teaches an agent how to perform a specific kind of task\n- You are converting informal documentation or code examples into the SKILL.md format\n\n## SKILL.md Format\n\nEvery skill is a directory containing at minimum a `SKILL.md` file:\n\n```\nmy-skill-name/\n├── SKILL.md            # Required — the skill definition\n├── scripts/            # Optional — helper scripts\n├── references/         # Optional — reference data files\n└── assets/             # Optional — images, templates\n```\n\n### Frontmatter (Required)\n\nThe file MUST start with YAML frontmatter containing exactly two fields:\n\n```yaml\n---\nname: my-skill-name\ndescription: One-sentence description of what this skill teaches. Must be specific enough for LLM skill selection to match it to relevant tasks.\n---\n```\n\n**Rules:**\n- `name` must match the directory name (kebab-case, lowercase)\n- `description` should be 15-40 words, include key terms an LLM would search for\n- No other frontmatter fields — everything else goes in the markdown body\n\n### Body Structure\n\nThe markdown body follows this template:\n\n```markdown\n# [Skill Title]\n\n[1-2 sentence summary of what this skill enables]\n\n## When to Use\n\n- [Trigger condition 1]\n- [Trigger condition 2]\n- [Trigger condition 3]\n\n## [Core Content Sections]\n\n[Step-by-step instructions, code templates, API references, etc.]\n\n## Key Patterns\n\n1. [Convention or best practice 1]\n2. [Convention or best practice 2]\n...\n```\n\n## Step 1: Determine Skill Category\n\nMatch the pattern to one of three ClawSkills categories:\n\n| Category | Use When | Example |\n|----------|----------|---------|\n| `tool_guide` | The skill teaches how to use a specific tool or technique | \"How to analyze a codebase\", \"How to use Finnhub API\" |\n| `workflow` | The skill prescribes an end-to-end procedure with ordered steps | \"Create a panel component\", \"Set up API proxy endpoint\" |\n| `reference` | The skill provides knowledge that informs decisions | \"WorldMonitor architecture index\", \"News API comparison\" |\n\nThe category affects how ClawSkills judges the skill during execution analysis:\n- **workflow**: Was the agent able to follow the prescribed steps?\n- **tool_guide**: Did the agent use the described tool/approach?\n- **reference**: Did the knowledge influence agent decisions?\n\n## Step 2: Write the Description\n\nThe `description` field is the **most critical line** — it determines whether ClawSkills selects this skill for a given task.\n\n**Good descriptions** (specific, searchable):\n- \"Create a dashboard panel component using vanilla TypeScript DOM API, following the worldmonitor Panel architecture.\"\n- \"Integrate with Finnhub Stock API for real-time and historical stock market data.\"\n- \"CSS grid layout system for a responsive panel dashboard with dark theme.\"\n\n**Bad descriptions** (vague, generic):\n- \"A useful skill for building things.\"\n- \"TypeScript patterns.\"\n- \"How to make a panel.\"\n\n**Formula**: `[Action verb] + [specific subject] + [key technology/approach] + [context/project reference]`\n\n## Step 3: Write Actionable Instructions\n\nThe body must be concrete enough that an AI agent can follow it without guessing.\n\n### For Workflow Skills\n\nInclude:\n1. **File paths** — exact paths where files should be created (`src/components/MyPanel.ts`)\n2. **Code templates** — complete, runnable code blocks (not pseudocode)\n3. **Import statements** — every import the code needs\n4. **Interface definitions** — typed data shapes\n5. **Integration points** — how this connects to other parts of the project\n\nUse `{baseDir}` for paths relative to the skill directory:\n```\nRead the reference file at {baseDir}/references/example.json\n```\n\n### For Tool Guide Skills\n\nInclude:\n1. **API endpoints** — full URLs with parameter documentation\n2. **Auth mechanism** — how to authenticate (query param, header, etc.)\n3. **Response shapes** — JSON examples with field descriptions\n4. **Rate limits** — free tier limitations\n5. **Error handling** — common errors and how to handle them\n\n### For Reference Skills\n\nInclude:\n1. **Structured index** — tables, lists, or maps of the reference material\n2. **Key file paths** — where to find specific things in the reference codebase\n3. **Architecture decisions** — WHY certain patterns were chosen\n4. **Comparison tables** — alternatives and tradeoffs\n\n## Step 4: Add Dependency Hints\n\nIf the skill depends on patterns from other skills, mention them explicitly:\n\n```markdown\n## Prerequisites\n\nThis skill builds on:\n- `panel-component` — for the Panel base class\n- `data-service` — for the circuit breaker pattern\n- `api-proxy-endpoint` — for server-side API key isolation\n```\n\nThis helps ClawSkills understand skill composition when DERIVING new skills.\n\n## Step 5: Validate the Skill\n\nBefore saving, verify:\n\n1. **Frontmatter is valid YAML** — no tabs, proper indentation, quotes around special chars\n2. **Name matches directory** — `name: foo-bar` lives in `foo-bar/SKILL.md`\n3. **Code blocks are complete** — every snippet can be copy-pasted and run\n4. **No broken references** — all file paths and URLs are valid\n5. **Description is specific** — would an LLM pick this skill for the right task?\n\n## Example: Converting a Pattern to a Skill\n\n**Input** (from codebase-pattern-analyzer):\n\n```\nPattern: Circuit Breaker Data Service\nSource: worldmonitor/src/services/*.ts\nCategory: service\nStructure: Module-level CircuitBreaker instance, async fetch functions, typed interfaces\nKey code: createCircuitBreaker({ name, cacheTtlMs }), breaker.execute(fn, default)\n```\n\n**Output** (`data-service/SKILL.md`):\n\n```markdown\n---\nname: data-service\ndescription: Create data fetching services with circuit breaker pattern for API resilience. Services handle fetch, cache, retry, and expose typed data to panel components.\n---\n\n# Data Service Pattern\n\nEach panel's data comes from a dedicated service module in `src/services/`. ...\n\n## Circuit Breaker\n\n[Full implementation code]\n\n## Service Module Pattern\n\n[Template with typed interfaces, breaker usage, export functions]\n\n## Key Patterns\n\n1. One circuit breaker per API endpoint\n2. Export typed interfaces for data shapes\n3. Wrap fetch calls in breaker.execute(fn, defaultValue)\n...\n```\n\n## Evolution Hooks\n\nSkills generated by this workflow are designed to evolve:\n\n- **FIX**: When an API changes or code pattern breaks, ClawSkills updates the skill in-place\n- **DERIVED**: When a new panel needs a similar service, ClawSkills derives a specialized version\n- **CAPTURED**: When an agent discovers a novel pattern during execution, ClawSkills captures it as a new skill\n\nThe better the initial skill quality, the better the evolved descendants.\n\n"}


skill_id: worldmonitor-reference__v0_97df4f96
name: worldmonitor-reference
path: example/skills/worldmonitor-reference/SKILL.md
category: workflow
visibility: private
lineage_origin: captured
lineage_source_task_id: phase0-worldmonitor-analysis
lineage_created_at: 2026-03-10T15:13:59.460632
lineage_created_by: clawskills-evolution-pipeline
first_seen: 2026-03-10T15:13:59.460635
last_updated: 2026-03-10T15:13:59.460636
description:
Curated architecture index of the WorldMonitor open-source dashboard. Reference for extracting patterns — vanilla TypeScript, Panel class hierarchy, service layer, CSS grid, API edge functions, refresh scheduler, and 60+ data source integrations.

lineage_change_summary:
Captured from WorldMonitor codebase analysis: Curated architecture index of the WorldMonitor open-source dashboard. Reference for extracting patte

lineage_content_snapshot:
{"SKILL.md": "---\nname: worldmonitor-reference\ndescription: Curated architecture index of the WorldMonitor open-source dashboard. Reference for extracting patterns — vanilla TypeScript, Panel class hierarchy, service layer, CSS grid, API edge functions, refresh scheduler, and 60+ data source integrations.\n---\n\n# WorldMonitor Architecture Reference\n\nWorldMonitor is a real-time global intelligence dashboard built with vanilla TypeScript (no framework). This skill provides a curated index of its architecture, organized by the patterns most relevant to building a personal daily monitor.\n\n## When to Use\n\n- You are creating a new dashboard panel and need to follow WorldMonitor conventions\n- You need to understand how a specific pattern works before adapting it\n- You are deciding which WorldMonitor patterns to adopt vs. simplify for a personal project\n\n## Project Overview\n\n| Aspect | Detail |\n|--------|--------|\n| **Language** | TypeScript (strict), vanilla DOM — no React/Vue/Angular |\n| **Bundler** | Vite |\n| **Deployment** | Vercel (60+ Edge Functions) + Railway (WebSocket relay) |\n| **Desktop** | Tauri 2 (Rust) with Node.js sidecar |\n| **Map engines** | globe.gl + Three.js (3D), deck.gl + MapLibre GL (2D) |\n| **CSS** | Custom properties, dark/light themes, responsive grid |\n| **API contracts** | Protocol Buffers (92 proto files, 22 services) |\n| **Data sources** | 435+ RSS feeds, 45 data layers, 30+ live video streams |\n\n## Directory Structure\n\n```\nworldmonitor/\n├── src/\n│   ├── main.ts                    # Entry point: Sentry, analytics, App bootstrap\n│   ├── App.ts                     # Root application class\n│   ├── components/                # All UI panel classes (80+ files)\n│   │   ├── Panel.ts               # *** BASE CLASS — most important file ***\n│   │   ├── VirtualList.ts         # Virtual scrolling for large lists\n│   │   ├── NewsPanel.ts           # News with clustering, NER, summaries\n│   │   ├── MarketPanel.ts         # Stock market with sparklines\n│   │   ├── SearchModal.ts         # Cmd+K command palette\n│   │   ├── InsightsPanel.ts       # AI briefing + metrics\n│   │   ├── WorldClockPanel.ts     # Multi-timezone clock\n│   │   └── ...                    # 70+ more panels\n│   ├── services/                  # Data layer (117 files)\n│   │   ├── runtime.ts             # SmartPollLoop, desktop detection, fetch routing\n│   │   ├── bootstrap.ts           # Initial data hydration from Redis\n│   │   ├── rss.ts                 # RSS feed aggregation\n│   │   ├── news/index.ts          # News fetching + processing\n│   │   ├── market/index.ts        # Market data (Yahoo Finance, Finnhub)\n│   │   ├── i18n.ts                # 21-language internationalization\n│   │   ├── threat-classifier.ts   # 3-stage threat classification\n│   │   ├── clustering.ts          # News headline clustering\n│   │   └── ...                    # 100+ more services\n│   ├── config/                    # Static configuration\n│   │   ├── variant.ts             # Multi-variant (world/tech/finance/commodity/happy)\n│   │   ├── feeds.ts               # 435+ RSS feed definitions\n│   │   ├── panels.ts              # Panel enable/disable config\n│   │   ├── markets.ts             # Stock symbol lists\n│   │   └── ...\n│   ├── app/                       # Application-level modules\n│   │   ├── app-context.ts         # Shared state (inFlight, isDestroyed)\n│   │   ├── refresh-scheduler.ts   # *** REFRESH PATTERN ***\n│   │   ├── panel-layout.ts        # Grid layout management\n│   │   ├── search-manager.ts      # Command palette logic\n│   │   └── event-handlers.ts      # Global keyboard/visibility events\n│   ├── utils/                     # Utilities (22 files)\n│   │   ├── dom-utils.ts           # h(), fragment(), replaceChildren() — JSX-free helpers\n│   │   ├── sanitize.ts            # escapeHtml(), sanitizeUrl() — XSS prevention\n│   │   ├── index.ts               # formatTime, formatNumber, etc.\n│   │   └── ...\n│   ├── styles/                    # CSS\n│   │   ├── main.css               # *** THEME VARIABLES + GRID LAYOUT ***\n│   │   ├── panels.css             # Panel-specific styles (tabs, rows, badges)\n│   │   ├── base-layer.css         # Foundation layer\n│   │   └── ...\n│   └── generated/                 # Auto-generated from .proto files\n│       ├── client/                # TypeScript API clients\n│       └── server/                # TypeScript API handlers\n├── api/                           # Vercel Edge Functions (60+)\n│   ├── _cors.js                   # CORS origin allowlist\n│   ├── _rate-limit.js             # Upstash Redis rate limiting\n│   ├── _api-key.js                # API key injection\n│   ├── bootstrap.js               # Pre-fetch 15 Redis keys for first render\n│   ├── rss-proxy.js               # RSS feed proxy with domain allowlist\n│   ├── market/v1/[rpc].ts         # Market data endpoints (proto-first)\n│   ├── news/v1/[rpc].ts           # News endpoints\n│   └── ...                        # 22 service domains\n├── server/                        # Railway relay server\n├── proto/                         # Protocol Buffer definitions\n└── tests/                         # 30 test files, 554 test cases\n```\n\n## Core Pattern: Panel Base Class\n\n**File**: `src/components/Panel.ts` (~1000 lines)\n\nThe Panel class is the foundation for ALL UI components (80+ panels inherit from it).\n\n### Constructor Options\n\n```typescript\ninterface PanelOptions {\n  id: string;          // Unique panel identifier (used in DOM data-panel attr)\n  title: string;       // Display title in header\n  showCount?: boolean; // Show item count badge\n  className?: string;  // Additional CSS class (e.g., 'panel-wide')\n  trackActivity?: boolean; // Enable activity tracking\n  infoTooltip?: string;    // Hover tooltip text\n  premium?: 'locked' | 'enhanced'; // Pro feature gating\n}\n```\n\n### DOM Structure\n\n```\ndiv.panel[data-panel=\"{id}\"]\n├── div.panel-header\n│   ├── div.panel-header-left\n│   │   ├── span.panel-title\n│   │   └── span.panel-new-badge (optional)\n│   ├── span.panel-data-badge (optional, \"LIVE\"/\"CACHED\")\n│   └── span.panel-count (optional)\n├── div.panel-content\n│   └── (loading spinner | error state | rendered content)\n└── div.panel-resize-handle\n```\n\n### Key Methods\n\n| Method | Purpose |\n|--------|---------|\n| `getElement()` | Returns the outer `div.panel` |\n| `showLoading(msg?)` | Display spinner + message |\n| `showError(msg, onRetry?)` | Display error with optional retry button |\n| `setContent(html)` | Set content innerHTML (debounced 150ms) |\n| `setContentDirect(html)` | Set content immediately (no debounce) |\n| `setCount(n)` | Update the count badge |\n| `setStatusBadge(label, type)` | Show \"LIVE\"/\"CACHED\" badge |\n| `show() / hide()` | Toggle visibility |\n| `destroy()` | Remove element, abort controller, cleanup listeners |\n\n### Resize System\n\nPanels support both row span (1-4 rows) and column span (1-3 cols) resizing via drag handles. Sizes are persisted in `localStorage` with keys `worldmonitor-panel-spans` and `worldmonitor-panel-col-spans`.\n\n## Core Pattern: Data Service Layer\n\n**Directory**: `src/services/` (117 files)\n\n### SmartPollLoop\n\n**File**: `src/services/runtime.ts`\n\nInstead of raw `setInterval`, WorldMonitor uses `startSmartPollLoop()`:\n\n```typescript\nconst loop = startSmartPollLoop(async () => {\n  // fetch data, return boolean for success/failure\n}, intervalMs);\nloop.stop(); // cleanup\n```\n\nFeatures:\n- Exponential backoff on failure (1x → 2x → 4x, capped at 8x)\n- Hidden-tab throttle (pauses when `document.hidden`)\n- Circuit breaker integration (stops after N consecutive failures)\n- Adaptive refresh (can adjust interval based on data freshness)\n\n### Service Module Convention\n\nEach service is a module (not a class) exporting async functions:\n\n```typescript\n// src/services/market/index.ts\nexport async function fetchMarketQuotes(symbols: string[]): Promise<MarketQuote[]> { ... }\nexport async function fetchMarketNews(): Promise<MarketNews[]> { ... }\n```\n\nServices call `/api/...` proxy endpoints, never external APIs directly.\n\n## Core Pattern: CSS Theme System\n\n**File**: `src/styles/main.css` (~17,000 lines)\n\n### CSS Custom Properties (Dark Theme)\n\n```css\n:root {\n  /* Backgrounds */\n  --bg: #0a0a0a;\n  --bg-secondary: #111;\n  --surface: #141414;\n  --surface-hover: #1e1e1e;\n\n  /* Borders */\n  --border: #2a2a2a;\n  --border-strong: #444;\n  --border-subtle: #1a1a1a;\n\n  /* Text */\n  --text: #e8e8e8;\n  --text-secondary: #ccc;\n  --text-dim: #888;\n  --text-muted: #666;\n  --accent: #fff;\n\n  /* Semantic Colors */\n  --semantic-critical: #ff4444;\n  --semantic-high: #ff8800;\n  --semantic-elevated: #ffaa00;\n  --semantic-normal: #44aa44;\n  --semantic-info: #3b82f6;\n\n  /* Status */\n  --green: #44ff88;\n  --red: #ff4444;\n  --yellow: #ffaa00;\n\n  /* Font Stack */\n  --font-mono: 'SF Mono', 'Monaco', 'Cascadia Code', 'Fira Code', monospace;\n  --font-body: var(--font-mono);\n}\n```\n\n### Grid Layout\n\n```css\n.panels-grid {\n  display: grid;\n  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));\n  grid-auto-flow: row dense;\n  grid-auto-rows: minmax(200px, 380px);\n  gap: 4px;\n  padding: 4px;\n}\n```\n\n### Panel Base Styles\n\n```css\n.panel {\n  background: var(--surface);\n  border: 1px solid var(--border);\n  display: flex;\n  flex-direction: column;\n  overflow: hidden;\n}\n\n.panel-header {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  padding: 6px 10px;\n  background: var(--overlay-subtle);\n  border-bottom: 1px solid var(--border);\n}\n\n.panel-title {\n  font-size: 11px;\n  font-weight: 700;\n  color: var(--accent);\n  text-transform: uppercase;\n  letter-spacing: 0.5px;\n}\n\n.panel-content {\n  flex: 1;\n  overflow-y: auto;\n  padding: 8px;\n  font-size: 12px;\n}\n```\n\n## Core Pattern: API Proxy Layer\n\n**Directory**: `api/` (60+ Vercel Edge Functions)\n\n### CORS Helper (`api/_cors.js`)\n\nOrigin allowlist pattern — only approved origins can call API endpoints:\n\n```javascript\nconst ALLOWED_ORIGIN_PATTERNS = [\n  /^https:\\/\\/(.*\\.)?worldmonitor\\.app$/,\n  /^https?:\\/\\/localhost(:\\d+)?$/,\n];\n\nexport function getCorsHeaders(req, methods = 'GET, OPTIONS') {\n  const origin = req.headers.get('origin') || '';\n  const allowOrigin = isAllowedOrigin(origin) ? origin : 'https://worldmonitor.app';\n  return {\n    'Access-Control-Allow-Origin': allowOrigin,\n    'Access-Control-Allow-Methods': methods,\n    'Access-Control-Allow-Headers': 'Content-Type',\n  };\n}\n```\n\n### Edge Function Pattern\n\nEach endpoint file follows this structure:\n1. CORS preflight handling\n2. Input validation\n3. API key from `process.env`\n4. Upstream fetch with error wrapping\n5. `Cache-Control` headers for CDN caching\n6. Structured JSON response\n\n## Core Pattern: Refresh Scheduler\n\n**File**: `src/app/refresh-scheduler.ts`\n\n```typescript\ninterface RefreshRegistration {\n  name: string;\n  fn: () => Promise<boolean | void>;\n  intervalMs: number;\n  condition?: () => boolean;   // Skip refresh if false\n}\n\nclass RefreshScheduler {\n  scheduleRefresh(name, fn, intervalMs, condition?): void\n  flushStaleRefreshes(hiddenMs): void   // Staggered 150ms between each\n  destroy(): void\n}\n```\n\nKey behaviors:\n- Each panel registers with a unique name and interval\n- Visibility-aware: pauses when tab hidden, flushes stale on return\n- In-flight guard: `ctx.inFlight` set prevents duplicate refreshes\n- Stagger on resume: 150ms gap between each stale refresh to avoid API burst\n\n## Core Pattern: DOM Utilities\n\n**File**: `src/utils/dom-utils.ts`\n\nWorldMonitor uses a lightweight JSX-free helper `h()`:\n\n```typescript\nfunction h(tag: string, propsOrChild?, ...children): HTMLElement\nfunction fragment(...children): DocumentFragment\nfunction replaceChildren(el: Element, ...children): void\nfunction rawHtml(html: string): DocumentFragment\nfunction safeHtml(html: string): string  // XSS-safe innerHTML\n```\n\nExample usage:\n```typescript\nconst row = h('div', { className: 'stock-row' },\n  h('span', { className: 'symbol' }, 'AAPL'),\n  h('span', { className: 'price' }, '$178.72'),\n);\n```\n\n## Adaptation Notes for Personal Monitor\n\nWhen adapting WorldMonitor patterns for my-daily-monitor:\n\n| WorldMonitor Feature | Personal Monitor Adaptation |\n|---------------------|----------------------------|\n| 80+ panel classes | 15-20 panels (email, feishu, office, social, etc.) |\n| Proto-first API | Simple REST endpoints (no proto needed) |\n| Vercel Edge Functions | Vite dev server plugin or simple Express routes |\n| SmartPollLoop | Simplified RefreshScheduler with setInterval |\n| VirtualList | Not needed until panel has 100+ items |\n| Multi-variant (5 sites) | Single variant only |\n| 21 languages (i18n) | Chinese + English only |\n| Tauri desktop app | Web-only (may add later) |\n| Redis caching | In-memory cache only |\n\n## Key Files to Study\n\nWhen extracting a specific pattern, start with these files:\n\n| Pattern | Read First | Then Read |\n|---------|-----------|-----------|\n| Panel architecture | `src/components/Panel.ts` | Any concrete panel (e.g., `NewsPanel.ts`) |\n| Service layer | `src/services/runtime.ts` | `src/services/market/index.ts` |\n| CSS theme | `src/styles/main.css` (first 60 lines) | `src/styles/panels.css` |\n| API proxy | `api/_cors.js` | Any `api/*/v1/[rpc].ts` |\n| Refresh | `src/app/refresh-scheduler.ts` | `src/app/event-handlers.ts` |\n| Grid layout | `src/styles/main.css` (search `.panels-grid`) | `src/app/panel-layout.ts` |\n| DOM helpers | `src/utils/dom-utils.ts` | `src/utils/sanitize.ts` |\n| Entry point | `src/main.ts` | `src/App.ts` |\n\n"}


skill_id: panel-component__v0_dfce1df1
name: panel-component
path: example/skills/panel-component/SKILL.md
category: workflow
visibility: private
lineage_origin: captured
lineage_source_task_id: phase0-worldmonitor-analysis
lineage_created_at: 2026-03-10T15:13:59.460873
lineage_created_by: clawskills-evolution-pipeline
first_seen: 2026-03-10T15:13:59.460876
last_updated: 2026-03-10T18:58:42.052136
description:
Create a dashboard panel component using vanilla TypeScript DOM API, following the worldmonitor Panel architecture. Panels have a header with title/count, scrollable content area, loading/error states, and resize handles.

lineage_change_summary:
Captured from WorldMonitor codebase analysis: Create a dashboard panel component using vanilla TypeScript DOM API, following the worldmonitor Pane

lineage_content_snapshot:
{"SKILL.md": "---\nname: panel-component\ndescription: Create a dashboard panel component using vanilla TypeScript DOM API, following the worldmonitor Panel architecture. Panels have a header with title/count, scrollable content area, loading/error states, and resize handles.\n---\n\n# Panel Component Pattern\n\nCreate dashboard panel components using **vanilla TypeScript** (no framework, no JSX). Each panel is a class extending a `Panel` base class.\n\n## Architecture Overview\n\n```\nPanel (base class)\n├── element: HTMLElement (outer container, .panel)\n│   ├── header: HTMLElement (.panel-header)\n│   │   ├── headerLeft (.panel-header-left)\n│   │   │   ├── title (.panel-title)\n│   │   │   └── newBadge (.panel-new-badge) [optional]\n│   │   ├── statusBadge (.panel-data-badge) [optional]\n│   │   └── countEl (.panel-count) [optional]\n│   ├── content: HTMLElement (.panel-content)\n│   └── resizeHandle (.panel-resize-handle)\n```\n\n## Base Panel Class (Simplified for this project)\n\nCreate `src/components/Panel.ts`:\n\n```typescript\nexport interface PanelOptions {\n  id: string;\n  title: string;\n  showCount?: boolean;\n  className?: string;\n}\n\nexport class Panel {\n  protected element: HTMLElement;\n  protected content: HTMLElement;\n  protected header: HTMLElement;\n  protected countEl: HTMLElement | null = null;\n  protected panelId: string;\n  private _fetching = false;\n\n  constructor(options: PanelOptions) {\n    this.panelId = options.id;\n    this.element = document.createElement('div');\n    this.element.className = `panel ${options.className || ''}`;\n    this.element.dataset.panel = options.id;\n\n    // Header\n    this.header = document.createElement('div');\n    this.header.className = 'panel-header';\n\n    const headerLeft = document.createElement('div');\n    headerLeft.className = 'panel-header-left';\n\n    const title = document.createElement('span');\n    title.className = 'panel-title';\n    title.textContent = options.title;\n    headerLeft.appendChild(title);\n    this.header.appendChild(headerLeft);\n\n    // Count badge (optional)\n    if (options.showCount) {\n      this.countEl = document.createElement('span');\n      this.countEl.className = 'panel-count';\n      this.countEl.textContent = '0';\n      this.header.appendChild(this.countEl);\n    }\n\n    // Content area\n    this.content = document.createElement('div');\n    this.content.className = 'panel-content';\n    this.content.id = `${options.id}Content`;\n\n    this.element.appendChild(this.header);\n    this.element.appendChild(this.content);\n    this.showLoading();\n  }\n\n  public getElement(): HTMLElement { return this.element; }\n\n  public showLoading(message = 'Loading...'): void {\n    this.content.innerHTML = `\n      <div class=\"panel-loading\">\n        <div class=\"panel-loading-spinner\"></div>\n        <div class=\"panel-loading-text\">${message}</div>\n      </div>`;\n  }\n\n  public showError(message = 'Failed to load', onRetry?: () => void): void {\n    this.content.innerHTML = `\n      <div class=\"panel-error-state\">\n        <div class=\"panel-error-msg\">${message}</div>\n        ${onRetry ? '<button class=\"panel-retry-btn\" data-panel-retry>Retry</button>' : ''}\n      </div>`;\n    if (onRetry) {\n      this.content.querySelector('[data-panel-retry]')?.addEventListener('click', onRetry);\n    }\n  }\n\n  public setContent(html: string): void {\n    this.content.innerHTML = html;\n  }\n\n  public setCount(count: number): void {\n    if (this.countEl) this.countEl.textContent = count.toString();\n  }\n\n  public show(): void { this.element.classList.remove('hidden'); }\n  public hide(): void { this.element.classList.add('hidden'); }\n\n  protected setFetching(v: boolean): void { this._fetching = v; }\n  protected get isFetching(): boolean { return this._fetching; }\n\n  public destroy(): void {\n    this.element.remove();\n  }\n}\n```\n\n## Creating a Concrete Panel (Example: StockPanel)\n\nEach panel extends `Panel` and manages its own data fetching + rendering:\n\n```typescript\nimport { Panel } from './Panel';\n\ninterface StockQuote {\n  symbol: string;\n  name: string;\n  price: number | null;\n  change: number | null;\n  sparkline?: number[];\n}\n\nexport class StockPanel extends Panel {\n  private refreshTimer: ReturnType<typeof setInterval> | null = null;\n\n  constructor() {\n    super({ id: 'stocks', title: 'Stock Market', showCount: true });\n    this.fetchData();\n    this.refreshTimer = setInterval(() => this.fetchData(), 60_000);\n  }\n\n  private async fetchData(): Promise<void> {\n    if (this.isFetching) return;\n    this.setFetching(true);\n    try {\n      const quotes = await fetchStockQuotes(); // from data-service\n      this.render(quotes);\n      this.setCount(quotes.length);\n    } catch (err) {\n      this.showError('Failed to load stock data', () => this.fetchData());\n    } finally {\n      this.setFetching(false);\n    }\n  }\n\n  private render(quotes: StockQuote[]): void {\n    const rows = quotes.map(q => `\n      <div class=\"stock-row\">\n        <span class=\"stock-symbol\">${q.symbol}</span>\n        <span class=\"stock-name\">${q.name}</span>\n        <span class=\"stock-price\">${q.price != null ? '$' + q.price.toFixed(2) : '—'}</span>\n        <span class=\"stock-change ${(q.change ?? 0) >= 0 ? 'positive' : 'negative'}\">\n          ${q.change != null ? (q.change >= 0 ? '+' : '') + q.change.toFixed(2) + '%' : '—'}\n        </span>\n      </div>\n    `).join('');\n\n    this.setContent(`<div class=\"stock-list\">${rows}</div>`);\n  }\n\n  public override destroy(): void {\n    if (this.refreshTimer) clearInterval(this.refreshTimer);\n    super.destroy();\n  }\n}\n```\n\n## Key Patterns\n\n1. **Constructor** calls `super()` with panel config, then triggers initial data fetch\n2. **fetchData()** is async, uses `isFetching` guard, shows error on failure with retry\n3. **render()** builds HTML strings and calls `this.setContent(html)`\n4. **destroy()** cleans up timers and event listeners\n5. Use `showLoading()` during initial load (auto-called in constructor)\n6. Use `showError(msg, retryFn)` on failure\n7. Sparkline SVGs use inline `<svg>` with `<polyline>` — see sparkline utility\n\n## Sparkline Utility\n\n```typescript\nexport function miniSparkline(data: number[] | undefined, change: number | null, w = 50, h = 16): string {\n  if (!data || data.length < 2) return '';\n  const min = Math.min(...data);\n  const max = Math.max(...data);\n  const range = max - min || 1;\n  const color = change != null && change >= 0 ? 'var(--green)' : 'var(--red)';\n  const points = data.map((v, i) => {\n    const x = (i / (data.length - 1)) * w;\n    const y = h - ((v - min) / range) * (h - 2) - 1;\n    return `${x.toFixed(1)},${y.toFixed(1)}`;\n  }).join(' ');\n  return `<svg width=\"${w}\" height=\"${h}\" viewBox=\"0 0 ${w} ${h}\"><polyline points=\"${points}\" fill=\"none\" stroke=\"${color}\" stroke-width=\"1.2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>`;\n}\n```\n"}


skill_id: data-service__v0_eabdbaa3
name: data-service
path: example/skills/data-service/SKILL.md
category: workflow
visibility: private
lineage_origin: captured
lineage_source_task_id: phase0-worldmonitor-analysis
lineage_created_at: 2026-03-10T15:13:59.461081
lineage_created_by: clawskills-evolution-pipeline
first_seen: 2026-03-10T15:13:59.461084
last_updated: 2026-03-10T18:23:35.945030
description:
Create data fetching services with circuit breaker pattern for API resilience. Services handle fetch, cache, retry, and expose typed data to panel components.

lineage_change_summary:
Captured from WorldMonitor codebase analysis: Create data fetching services with circuit breaker pattern for API resilience. Services handle fetch

lineage_content_snapshot:
{"SKILL.md": "---\nname: data-service\ndescription: Create data fetching services with circuit breaker pattern for API resilience. Services handle fetch, cache, retry, and expose typed data to panel components.\n---\n\n# Data Service Pattern\n\nEach panel's data comes from a dedicated **service module** in `src/services/`. Services handle API calls, caching, error handling with circuit breaker pattern.\n\n## Circuit Breaker\n\nThe circuit breaker prevents cascading failures when an API is down. After N consecutive failures, it enters \"cooldown\" mode and serves cached data instead of hitting the API.\n\nCreate `src/utils/circuit-breaker.ts`:\n\n```typescript\ninterface CircuitState {\n  failures: number;\n  cooldownUntil: number;\n}\n\ninterface CacheEntry<T> {\n  data: T;\n  timestamp: number;\n}\n\nexport interface CircuitBreakerOptions {\n  name: string;\n  maxFailures?: number;\n  cooldownMs?: number;\n  cacheTtlMs?: number;\n}\n\nexport class CircuitBreaker<T> {\n  private state: CircuitState = { failures: 0, cooldownUntil: 0 };\n  private cache: CacheEntry<T> | null = null;\n  private name: string;\n  private maxFailures: number;\n  private cooldownMs: number;\n  private cacheTtlMs: number;\n\n  constructor(options: CircuitBreakerOptions) {\n    this.name = options.name;\n    this.maxFailures = options.maxFailures ?? 2;\n    this.cooldownMs = options.cooldownMs ?? 5 * 60 * 1000;  // 5 minutes\n    this.cacheTtlMs = options.cacheTtlMs ?? 10 * 60 * 1000; // 10 minutes\n  }\n\n  isOnCooldown(): boolean {\n    if (Date.now() < this.state.cooldownUntil) return true;\n    if (this.state.cooldownUntil > 0) {\n      this.state = { failures: 0, cooldownUntil: 0 };\n    }\n    return false;\n  }\n\n  getCached(): T | null {\n    if (this.cache && Date.now() - this.cache.timestamp < this.cacheTtlMs) {\n      return this.cache.data;\n    }\n    return null;\n  }\n\n  recordSuccess(data: T): void {\n    this.state = { failures: 0, cooldownUntil: 0 };\n    this.cache = { data, timestamp: Date.now() };\n  }\n\n  recordFailure(error?: string): void {\n    this.state.failures++;\n    if (this.state.failures >= this.maxFailures) {\n      this.state.cooldownUntil = Date.now() + this.cooldownMs;\n      console.warn(`[${this.name}] Cooldown for ${this.cooldownMs / 1000}s`);\n    }\n  }\n\n  async execute<R extends T>(fn: () => Promise<R>, defaultValue: R): Promise<R> {\n    if (this.isOnCooldown()) {\n      const cached = this.getCached();\n      return (cached as R) ?? defaultValue;\n    }\n    const cached = this.getCached();\n    if (cached !== null) return cached as R;\n    try {\n      const result = await fn();\n      this.recordSuccess(result);\n      return result;\n    } catch (e) {\n      console.error(`[${this.name}] Failed:`, e);\n      this.recordFailure(String(e));\n      return defaultValue;\n    }\n  }\n}\n\nexport function createCircuitBreaker<T>(options: CircuitBreakerOptions): CircuitBreaker<T> {\n  return new CircuitBreaker<T>(options);\n}\n```\n\n## Service Module Pattern\n\nEach service is a TypeScript module that exports async data-fetching functions:\n\n```typescript\n// src/services/stock-market.ts\nimport { createCircuitBreaker } from '../utils/circuit-breaker';\n\nexport interface StockQuote {\n  symbol: string;\n  name: string;\n  price: number | null;\n  change: number | null;\n  sparkline?: number[];\n}\n\nconst breaker = createCircuitBreaker<StockQuote[]>({\n  name: 'StockMarket',\n  cacheTtlMs: 60_000, // 1 minute cache\n});\n\nexport async function fetchStockQuotes(symbols: string[]): Promise<StockQuote[]> {\n  return breaker.execute(async () => {\n    const params = new URLSearchParams({ symbols: symbols.join(',') });\n    const resp = await fetch(`/api/stocks?${params}`);\n    if (!resp.ok) throw new Error(`HTTP ${resp.status}`);\n    return resp.json();\n  }, []);\n}\n```\n\n## Key Patterns\n\n1. **One circuit breaker per API endpoint** — create at module level\n2. **Export typed interfaces** for data shapes\n3. **Wrap fetch calls** in `breaker.execute(fn, defaultValue)`\n4. **Default values** are empty arrays `[]` or empty objects `{}`\n5. **Services are stateless modules** — no class instances, just exported functions\n6. **API proxy**: Frontend calls `/api/...` which proxies to external APIs (hides API keys)\n\n## API Proxy Pattern\n\nFor APIs requiring keys, create server-side proxy endpoints:\n\n```typescript\n// api/stocks.ts (Vercel serverless function)\nexport default async function handler(req, res) {\n  const { symbols } = req.query;\n  const apiKey = process.env.FINNHUB_API_KEY;\n  const data = await fetch(`https://finnhub.io/api/v1/quote?symbol=${symbols}&token=${apiKey}`);\n  res.json(await data.json());\n}\n```\n\nThis keeps API keys server-side and provides a single endpoint for the frontend.\n\n"}


skill_id: panel-grid-layout__v0_278cb25d
name: panel-grid-layout
path: example/skills/panel-grid-layout/SKILL.md
category: workflow
visibility: private
lineage_origin: captured
lineage_source_task_id: phase0-worldmonitor-analysis
lineage_created_at: 2026-03-10T15:13:59.461306
lineage_created_by: clawskills-evolution-pipeline
first_seen: 2026-03-10T15:13:59.461308
last_updated: 2026-03-10T15:22:14.525734
description:
CSS grid layout system for a responsive panel dashboard with dark theme. Uses CSS custom properties, auto-fill grid, and monospace font aesthetic inspired by worldmonitor.

lineage_change_summary:
Captured from WorldMonitor codebase analysis: CSS grid layout system for a responsive panel dashboard with dark theme. Uses CSS custom properties,

lineage_content_snapshot:
{"SKILL.md": "---\nname: panel-grid-layout\ndescription: CSS grid layout system for a responsive panel dashboard with dark theme. Uses CSS custom properties, auto-fill grid, and monospace font aesthetic inspired by worldmonitor.\n---\n\n# Panel Grid Layout & Dark Theme\n\n## CSS Variables (Dark Theme)\n\nCreate `src/styles/main.css` with these CSS custom properties:\n\n```css\n:root {\n  /* Backgrounds */\n  --bg: #0a0a0a;\n  --bg-secondary: #111;\n  --surface: #141414;\n  --surface-hover: #1e1e1e;\n\n  /* Borders */\n  --border: #2a2a2a;\n  --border-strong: #444;\n  --border-subtle: #1a1a1a;\n\n  /* Text */\n  --text: #e8e8e8;\n  --text-secondary: #ccc;\n  --text-dim: #888;\n  --text-muted: #666;\n  --text-faint: #555;\n  --accent: #fff;\n\n  /* Overlays */\n  --overlay-subtle: rgba(255, 255, 255, 0.03);\n  --overlay-light: rgba(255, 255, 255, 0.05);\n\n  /* Status & Semantic Colors */\n  --green: #44ff88;\n  --red: #ff4444;\n  --yellow: #ffaa00;\n  --semantic-critical: #ff4444;\n  --semantic-high: #ff8800;\n  --semantic-positive: #44ff88;\n  --semantic-info: #3b82f6;\n\n  /* Status indicators */\n  --status-live: #44ff88;\n  --status-cached: #ffaa00;\n  --status-unavailable: #ff4444;\n\n  /* Font */\n  --font-mono: 'SF Mono', 'Monaco', 'Cascadia Code', 'Fira Code', monospace;\n  --font-body: var(--font-mono);\n}\n```\n\n## Base Styles\n\n```css\n*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }\n\nhtml, body {\n  height: 100%;\n  background: var(--bg);\n  color: var(--text);\n  font-family: var(--font-body);\n  font-size: 13px;\n  line-height: 1.5;\n  -webkit-font-smoothing: antialiased;\n}\n\n/* Scrollbar */\n::-webkit-scrollbar { width: 6px; height: 6px; }\n::-webkit-scrollbar-track { background: transparent; }\n::-webkit-scrollbar-thumb { background: #333; border-radius: 3px; }\n::-webkit-scrollbar-thumb:hover { background: #555; }\n```\n\n## Grid Layout\n\n```css\n.panels-grid {\n  display: grid;\n  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));\n  grid-auto-flow: row dense;\n  grid-auto-rows: minmax(200px, 380px);\n  gap: 4px;\n  padding: 4px;\n  align-content: start;\n  align-items: stretch;\n  min-height: 0;\n}\n```\n\n## Panel Base Styles\n\n```css\n.panel {\n  background: var(--surface);\n  border: 1px solid var(--border);\n  display: flex;\n  flex-direction: column;\n  overflow: hidden;\n  height: 100%;\n  min-height: 200px;\n  min-width: 0;\n  transition: transform 0.15s, box-shadow 0.15s;\n  position: relative;\n}\n\n.panel.hidden { display: none; }\n\n.panel-header {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  padding: 6px 10px;\n  background: var(--overlay-subtle);\n  border-bottom: 1px solid var(--border);\n  flex-shrink: 0;\n}\n\n.panel-header-left {\n  display: flex;\n  align-items: center;\n  gap: 6px;\n  min-width: 0;\n}\n\n.panel-title {\n  font-size: 11px;\n  font-weight: 700;\n  color: var(--accent);\n  text-transform: uppercase;\n  letter-spacing: 0.5px;\n  white-space: nowrap;\n  overflow: hidden;\n  text-overflow: ellipsis;\n}\n\n.panel-count {\n  font-size: 10px;\n  background: var(--overlay-light);\n  color: var(--text-dim);\n  padding: 1px 6px;\n  border-radius: 8px;\n  font-variant-numeric: tabular-nums;\n}\n\n.panel-content {\n  flex: 1;\n  overflow-y: auto;\n  padding: 8px;\n  font-size: 12px;\n}\n\n.panel-content::-webkit-scrollbar { width: 4px; }\n.panel-content::-webkit-scrollbar-track { background: transparent; }\n.panel-content::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }\n```\n\n## Loading & Error States\n\n```css\n.panel-loading {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  justify-content: center;\n  padding: 32px 16px;\n  min-height: 120px;\n}\n\n.panel-loading-spinner {\n  width: 32px;\n  height: 32px;\n  border: 2px solid var(--border);\n  border-top-color: var(--green);\n  border-radius: 50%;\n  animation: spin 1s linear infinite;\n  margin-bottom: 12px;\n}\n\n@keyframes spin {\n  to { transform: rotate(360deg); }\n}\n\n.panel-loading-text {\n  font-size: 12px;\n  color: var(--accent);\n  letter-spacing: 0.5px;\n}\n\n.panel-error-state {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  justify-content: center;\n  gap: 8px;\n  padding: 1.5rem 1rem;\n  min-height: 120px;\n}\n\n.panel-error-msg {\n  color: var(--text-dim);\n  font-size: 11px;\n}\n\n.panel-retry-btn {\n  background: transparent;\n  border: 1px solid var(--border);\n  color: var(--text-dim);\n  padding: 4px 12px;\n  font-size: 10px;\n  cursor: pointer;\n  border-radius: 3px;\n}\n\n.panel-retry-btn:hover {\n  border-color: var(--accent);\n  color: var(--accent);\n}\n```\n\n## Row/Item Patterns (reusable across panels)\n\n```css\n/* Generic list row */\n.item-row {\n  display: flex;\n  align-items: center;\n  gap: 8px;\n  padding: 6px 8px;\n  border-bottom: 1px solid var(--border-subtle);\n  transition: background 0.1s;\n}\n.item-row:hover { background: var(--surface-hover); }\n.item-row:last-child { border-bottom: none; }\n\n/* Positive/Negative change indicators */\n.positive { color: var(--green); }\n.negative { color: var(--red); }\n\n/* Badge */\n.badge {\n  font-size: 9px;\n  font-weight: 700;\n  padding: 2px 6px;\n  border-radius: 3px;\n  letter-spacing: 0.5px;\n}\n.badge-live { background: rgba(68, 255, 136, 0.15); color: var(--green); }\n.badge-warn { background: rgba(255, 170, 0, 0.15); color: var(--yellow); }\n.badge-error { background: rgba(255, 68, 68, 0.15); color: var(--red); }\n\n/* Tabular numbers for all numeric content */\n.num { font-variant-numeric: tabular-nums; }\n```\n\n## Responsive Breakpoints\n\n```css\n@media (max-width: 768px) {\n  .panels-grid {\n    grid-template-columns: 1fr;\n    grid-auto-rows: minmax(180px, auto);\n  }\n}\n\n@media (min-width: 1600px) {\n  .panels-grid {\n    grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));\n  }\n}\n```\n\n## App Layout\n\n```css\n#app {\n  display: flex;\n  flex-direction: column;\n  height: 100vh;\n}\n\n.app-header {\n  display: flex;\n  align-items: center;\n  padding: 8px 12px;\n  background: var(--surface);\n  border-bottom: 1px solid var(--border);\n  flex-shrink: 0;\n}\n\n.app-header h1 {\n  font-size: 14px;\n  font-weight: 700;\n  color: var(--green);\n  letter-spacing: 1px;\n  text-transform: uppercase;\n}\n\n.main-content {\n  flex: 1;\n  overflow-y: auto;\n}\n```\n\n"}


skill_id: create-full-stack-panel-feature__v0_e47399dd
name: create-full-stack-panel-feature
path: example/skills/create-full-stack-panel-feature/SKILL.md
category: workflow
visibility: private
lineage_origin: imported
lineage_content_snapshot: {}
lineage_created_at: 2026-03-10T15:14:03.201390
first_seen: 2026-03-10T15:14:03.201396
last_updated: 2026-03-10T18:10:59.535130
description:
Multi-layer workflow for creating complete panel features by analyzing existing patterns and generating coordinated server routes, services, UI components, registration, and styling files.


skill_id: create-skill-from-source-code__v0_fbf67670
name: create-skill-from-source-code
path: example/skills/create-skill-from-source-code/SKILL.md
category: workflow
visibility: private
lineage_origin: imported
lineage_content_snapshot: {}
lineage_created_at: 2026-03-10T15:14:03.201500
first_seen: 2026-03-10T15:14:03.201502
last_updated: 2026-03-10T15:14:03.201502
description:
Analyze existing source code to identify key patterns and generate comprehensive educational skill documentation with proper YAML frontmatter, architectural analysis, code templates, and usage examples.


skill_id: enhance-typescript-ui-components__v0_3e5fa1e8
name: enhance-typescript-ui-components
path: example/skills/enhance-typescript-ui-components/SKILL.md
category: workflow
visibility: private
lineage_origin: imported
lineage_content_snapshot: {}
lineage_created_at: 2026-03-10T15:14:03.201520
first_seen: 2026-03-10T15:14:03.201521
last_updated: 2026-03-10T15:32:17.731011
description:
Add visual features (thumbnails, badges, dynamic styling) to TypeScript components by coordinating TypeScript logic changes with CSS updates


skill_id: safe-css-append__v0_b4e93e53
name: safe-css-append
path: example/skills/safe-css-append/SKILL.md
category: workflow
visibility: private
lineage_origin: imported
lineage_content_snapshot: {}
lineage_created_at: 2026-03-10T15:14:03.201538
first_seen: 2026-03-10T15:14:03.201540
last_updated: 2026-03-10T21:18:40.525724
description:
Safely append CSS styles from one file to another by identifying missing rules, adapting CSS variables, and verifying changes


skill_id: dashboard-domain-specification__v0_1ceaf9ed
name: dashboard-domain-specification
path: example/skills/dashboard-domain-specification/SKILL.md
category: reference
visibility: private
lineage_origin: captured
lineage_source_task_id: evolution-task-01
lineage_created_at: 2026-03-10T15:15:54.940974
lineage_created_by: openrouter/deepseek/deepseek-chat
first_seen: 2026-03-10T15:15:54.940990
last_updated: 2026-03-10T15:15:54.940991
description:
A reusable template for defining detailed domain specifications for dashboard projects, including panel requirements, data sources, and interfaces.

lineage_change_summary:
Capture the pattern for creating detailed domain specifications for dashboard projects, including panel requirements, data sources, and interfaces.

lineage_content_diff:
--- a/SKILL.md
+++ b/SKILL.md
@@ -0,0 +1,65 @@
+---
+name: dashboard-domain-specification
+description: A reusable template for defining detailed domain specifications for dashboard projects, including panel requirements, data sources, and interfaces.
+---
+
+# Dashboard Domain Specification
+
+This skill provides a structured approach to creating detailed domain specifications for dashboard projects. It ensures clarity and completeness by covering panel requirements, data sources, and interfaces.
+
+## Steps
+
+1. **Define Dashboard Purpose**:
+   - Clearly state the primary goal of the dashboard (e.g., monitoring, analytics, reporting).
+
+2. **List Panels**:
+   - Enumerate all panels to be included in the dashboard.
+   - For each panel, specify:
+     - **Title**: Descriptive name of the panel.
+     - **Purpose**: What the panel aims to display or achieve.
+     - **Data Sources**: Where the data for the panel will come from (e.g., APIs, databases).
+     - **Refresh Rate**: How often the data should be updated (e.g., real-time, hourly).
+
+3. **Specify Data Sources**:
+   - Detail each data source, including:
+     - **Type**: API, database, file, etc.
+     - **Endpoint/Path**: URL or path to access the data.
+     - **Authentication**: Any required credentials or tokens.
+     - **Data Format**: JSON, CSV, etc.
+
+4. **Define Interfaces**:
+   - Describe the interfaces for interacting with the dashboard:
+     - **User Interface**: Layout, navigation, and interactivity.
+     - **Programmatic Interface**: APIs or hooks for external systems.
+
+5. **Include Examples**:
+   - Provide sample configurations or code snippets for common panels (e.g., time-series charts, tables).
+
+## Example
+
+### Panel Specification
+```yaml
+- title: "Daily Active Users"
+  purpose: "Display the number of active users per day"
+  data_sources:
+    - type: "API"
+      endpoint: "https://api.example.com/users/daily"
+      authentication: "Bearer token"
+      data_format: "JSON"
+  refresh_rate: "hourly"
+```
+
+### Data Source Specification
+```yaml
+- name: "User Activity API"
+  type: "REST API"
+  endpoint: "https://api.example.com/users"
+  authentication: "OAuth 2.0"
+  data_format: "JSON"
+```
+
+## Best Practices
+
+- **Modularity**: Keep panel specifications modular for easy updates.
+- **Consistency**: Use consistent naming conventions for panels and data sources.
+- **Documentation**: Maintain detailed documentation for all components

lineage_content_snapshot:
{"SKILL.md": "---\nname: dashboard-domain-specification\ndescription: A reusable template for defining detailed domain specifications for dashboard projects, including panel requirements, data sources, and interfaces.\n---\n\n# Dashboard Domain Specification\n\nThis skill provides a structured approach to creating detailed domain specifications for dashboard projects. It ensures clarity and completeness by covering panel requirements, data sources, and interfaces.\n\n## Steps\n\n1. **Define Dashboard Purpose**:\n   - Clearly state the primary goal of the dashboard (e.g., monitoring, analytics, reporting).\n\n2. **List Panels**:\n   - Enumerate all panels to be included in the dashboard.\n   - For each panel, specify:\n     - **Title**: Descriptive name of the panel.\n     - **Purpose**: What the panel aims to display or achieve.\n     - **Data Sources**: Where the data for the panel will come from (e.g., APIs, databases).\n     - **Refresh Rate**: How often the data should be updated (e.g., real-time, hourly).\n\n3. **Specify Data Sources**:\n   - Detail each data source, including:\n     - **Type**: API, database, file, etc.\n     - **Endpoint/Path**: URL or path to access the data.\n     - **Authentication**: Any required credentials or tokens.\n     - **Data Format**: JSON, CSV, etc.\n\n4. **Define Interfaces**:\n   - Describe the interfaces for interacting with the dashboard:\n     - **User Interface**: Layout, navigation, and interactivity.\n     - **Programmatic Interface**: APIs or hooks for external systems.\n\n5. **Include Examples**:\n   - Provide sample configurations or code snippets for common panels (e.g., time-series charts, tables).\n\n## Example\n\n### Panel Specification\n```yaml\n- title: \"Daily Active Users\"\n  purpose: \"Display the number of active users per day\"\n  data_sources:\n    - type: \"API\"\n      endpoint: \"https://api.example.com/users/daily\"\n      authentication: \"Bearer token\"\n      data_format: \"JSON\"\n  refresh_rate: \"hourly\"\n```\n\n### Data Source Specification\n```yaml\n- name: \"User Activity API\"\n  type: \"REST API\"\n  endpoint: \"https://api.example.com/users\"\n  authentication: \"OAuth 2.0\"\n  data_format: \"JSON\"\n```\n\n## Best Practices\n\n- **Modularity**: Keep panel specifications modular for easy updates.\n- **Consistency**: Use consistent naming conventions for panels and data sources.\n- **Documentation**: Maintain detailed documentation for all components"}


skill_id: panel-component-enhanced__v0_0460a853
name: panel-retry-localstorage
path: example/skills/panel-retry-localstorage/SKILL.md
category: workflow
visibility: private
lineage_origin: derived
lineage_source_task_id: evolution-task-03
lineage_created_at: 2026-03-10T15:20:09.457191
lineage_created_by: openrouter/deepseek/deepseek-chat
first_seen: 2026-03-10T15:20:09.457204
last_updated: 2026-03-10T18:24:30.547887
description:
Create a dashboard panel component using vanilla TypeScript DOM API, following the worldmonitor Panel architecture. Panels have a header with title/count, scrollable content area, loading/error states, and resize handles.

lineage_change_summary:
Enhanced error handling with detailed retry logic and localStorage persistence for panel state management

lineage_content_diff:
--- a/SKILL.md
+++ b/SKILL.md
@@ -1,6 +1,6 @@
 ---
-name: panel-component
-description: Create a dashboard panel component using vanilla TypeScript DOM API, following the worldmonitor Panel architecture. Panels have a header with title/count, scrollable content area, loading/error states, and resize handles.
+name: panel-component-enhanced
+description: Enhanced dashboard panel component with robust error handling, retry logic, and localStorage persistence for state management.
 ---
 
 # Panel Component Pattern
@@ -8,6 +8,11 @@
 Create dashboard panel components using **vanilla TypeScript** (no framework, no JSX). Each panel is a class extending a `Panel` base class.
 
 ## Architecture Overview
+
+### Enhanced Features
+- **Retry Logic**: Automatically retries failed data fetches with exponential backoff.
+- **State Persistence**: Saves panel state (expanded/collapsed, size) to localStorage.
+- **Detailed Error Handling**: Provides more granular error messages and recovery options.
 
 ```
 Panel (base class)
@@ -24,6 +29,51 @@
 
 ## Base Panel Class (Simplified for this project)
 
+```typescript
+export class Panel {
+  // ... existing code ...
+
+  private retryAttempts = 0;
+  private maxRetries = 3;
+  private retryDelay = 1000; // starts at 1s, doubles each retry
+
+  protected async fetchWithRetry(url: string): Promise<any> {
+    while (this.retryAttempts < this.maxRetries) {
+      try {
+        const response = await fetch(url);
+        if (!response.ok) throw new Error(`HTTP ${response.status}`);
+        return await response.json();
+      } catch (error) {
+        this.retryAttempts++;
+        if (this.retryAttempts >= this.maxRetries) {
+          throw new Error(`Failed after ${this.maxRetries} attempts: ${error.message}`);
+        }
+        await new Promise(resolve => setTimeout(resolve, this.retryDelay));
+        this.retryDelay *= 2;
+      }
+    }
+  }
+
+  public saveState(): void {
+    localStorage.setItem(`panelState_${this.panelId}`, JSON.stringify({
+      isExpanded: !this.element.classList.contains('collapsed'),
+      width: this.element.style.width,
+      height: this.element.style.height
+    }));
+  }
+
+  public loadState(): void {
+    const savedState = localStorage.getItem(`panelState_${this.panelId}`);
+    if (savedState) {
+      const { isExpanded, width, height } = JSON.parse(savedState);
+      if (!isExpanded) this.element.classList.add('collapsed');
+      if (width) this.element.style.width = width;
+      if (height) this.element.style.height = height;
+    }
+  }
+}
+```
+
 Create `src/components/Panel.ts`:
 
 ```typescript
@@ -121,6 +171,31 @@
 ```
 
 ## Creating a Concrete Panel (Example: StockPanel)
+
+```typescript
+export class StockPanel extends Panel {
+  // ... existing code ...
+
+  private async fetchData(): Promise<void> {
+    if (this.isFetching) return;
+    this.setFetching(true);
+    try {
+      const quotes = await this.fetchWithRetry('/api/stocks');
+      this.render(quotes);
+      this.setCount(quotes.length);
+      this.saveState();
+    } catch (err) {
+      this.showError(`Failed to load stock data: ${err.message}`, () => {
+        this.retryAttempts = 0;
+        this.retryDelay = 1000;
+        this.fetchData();
+      });
+    } finally {
+      this.setFetching(false);
+    }
+  }
+}
+```
 
 Each panel extends `Panel` and manages its own data fetching + rendering:
 

lineage_content_snapshot:
{"SKILL.md": "---\nname: panel-component-enhanced\ndescription: Enhanced dashboard panel component with robust error handling, retry logic, and localStorage persistence for state management.\n---\n\n# Panel Component Pattern\n\nCreate dashboard panel components using **vanilla TypeScript** (no framework, no JSX). Each panel is a class extending a `Panel` base class.\n\n## Architecture Overview\n\n### Enhanced Features\n- **Retry Logic**: Automatically retries failed data fetches with exponential backoff.\n- **State Persistence**: Saves panel state (expanded/collapsed, size) to localStorage.\n- **Detailed Error Handling**: Provides more granular error messages and recovery options.\n\n```\nPanel (base class)\n├── element: HTMLElement (outer container, .panel)\n│   ├── header: HTMLElement (.panel-header)\n│   │   ├── headerLeft (.panel-header-left)\n│   │   │   ├── title (.panel-title)\n│   │   │   └── newBadge (.panel-new-badge) [optional]\n│   │   ├── statusBadge (.panel-data-badge) [optional]\n│   │   └── countEl (.panel-count) [optional]\n│   ├── content: HTMLElement (.panel-content)\n│   └── resizeHandle (.panel-resize-handle)\n```\n\n## Base Panel Class (Simplified for this project)\n\n```typescript\nexport class Panel {\n  // ... existing code ...\n\n  private retryAttempts = 0;\n  private maxRetries = 3;\n  private retryDelay = 1000; // starts at 1s, doubles each retry\n\n  protected async fetchWithRetry(url: string): Promise<any> {\n    while (this.retryAttempts < this.maxRetries) {\n      try {\n        const response = await fetch(url);\n        if (!response.ok) throw new Error(`HTTP ${response.status}`);\n        return await response.json();\n      } catch (error) {\n        this.retryAttempts++;\n        if (this.retryAttempts >= this.maxRetries) {\n          throw new Error(`Failed after ${this.maxRetries} attempts: ${error.message}`);\n        }\n        await new Promise(resolve => setTimeout(resolve, this.retryDelay));\n        this.retryDelay *= 2;\n      }\n    }\n  }\n\n  public saveState(): void {\n    localStorage.setItem(`panelState_${this.panelId}`, JSON.stringify({\n      isExpanded: !this.element.classList.contains('collapsed'),\n      width: this.element.style.width,\n      height: this.element.style.height\n    }));\n  }\n\n  public loadState(): void {\n    const savedState = localStorage.getItem(`panelState_${this.panelId}`);\n    if (savedState) {\n      const { isExpanded, width, height } = JSON.parse(savedState);\n      if (!isExpanded) this.element.classList.add('collapsed');\n      if (width) this.element.style.width = width;\n      if (height) this.element.style.height = height;\n    }\n  }\n}\n```\n\nCreate `src/components/Panel.ts`:\n\n```typescript\nexport interface PanelOptions {\n  id: string;\n  title: string;\n  showCount?: boolean;\n  className?: string;\n}\n\nexport class Panel {\n  protected element: HTMLElement;\n  protected content: HTMLElement;\n  protected header: HTMLElement;\n  protected countEl: HTMLElement | null = null;\n  protected panelId: string;\n  private _fetching = false;\n\n  constructor(options: PanelOptions) {\n    this.panelId = options.id;\n    this.element = document.createElement('div');\n    this.element.className = `panel ${options.className || ''}`;\n    this.element.dataset.panel = options.id;\n\n    // Header\n    this.header = document.createElement('div');\n    this.header.className = 'panel-header';\n\n    const headerLeft = document.createElement('div');\n    headerLeft.className = 'panel-header-left';\n\n    const title = document.createElement('span');\n    title.className = 'panel-title';\n    title.textContent = options.title;\n    headerLeft.appendChild(title);\n    this.header.appendChild(headerLeft);\n\n    // Count badge (optional)\n    if (options.showCount) {\n      this.countEl = document.createElement('span');\n      this.countEl.className = 'panel-count';\n      this.countEl.textContent = '0';\n      this.header.appendChild(this.countEl);\n    }\n\n    // Content area\n    this.content = document.createElement('div');\n    this.content.className = 'panel-content';\n    this.content.id = `${options.id}Content`;\n\n    this.element.appendChild(this.header);\n    this.element.appendChild(this.content);\n    this.showLoading();\n  }\n\n  public getElement(): HTMLElement { return this.element; }\n\n  public showLoading(message = 'Loading...'): void {\n    this.content.innerHTML = `\n      <div class=\"panel-loading\">\n        <div class=\"panel-loading-spinner\"></div>\n        <div class=\"panel-loading-text\">${message}</div>\n      </div>`;\n  }\n\n  public showError(message = 'Failed to load', onRetry?: () => void): void {\n    this.content.innerHTML = `\n      <div class=\"panel-error-state\">\n        <div class=\"panel-error-msg\">${message}</div>\n        ${onRetry ? '<button class=\"panel-retry-btn\" data-panel-retry>Retry</button>' : ''}\n      </div>`;\n    if (onRetry) {\n      this.content.querySelector('[data-panel-retry]')?.addEventListener('click', onRetry);\n    }\n  }\n\n  public setContent(html: string): void {\n    this.content.innerHTML = html;\n  }\n\n  public setCount(count: number): void {\n    if (this.countEl) this.countEl.textContent = count.toString();\n  }\n\n  public show(): void { this.element.classList.remove('hidden'); }\n  public hide(): void { this.element.classList.add('hidden'); }\n\n  protected setFetching(v: boolean): void { this._fetching = v; }\n  protected get isFetching(): boolean { return this._fetching; }\n\n  public destroy(): void {\n    this.element.remove();\n  }\n}\n```\n\n## Creating a Concrete Panel (Example: StockPanel)\n\n```typescript\nexport class StockPanel extends Panel {\n  // ... existing code ...\n\n  private async fetchData(): Promise<void> {\n    if (this.isFetching) return;\n    this.setFetching(true);\n    try {\n      const quotes = await this.fetchWithRetry('/api/stocks');\n      this.render(quotes);\n      this.setCount(quotes.length);\n      this.saveState();\n    } catch (err) {\n      this.showError(`Failed to load stock data: ${err.message}`, () => {\n        this.retryAttempts = 0;\n        this.retryDelay = 1000;\n        this.fetchData();\n      });\n    } finally {\n      this.setFetching(false);\n    }\n  }\n}\n```\n\nEach panel extends `Panel` and manages its own data fetching + rendering:\n\n```typescript\nimport { Panel } from './Panel';\n\ninterface StockQuote {\n  symbol: string;\n  name: string;\n  price: number | null;\n  change: number | null;\n  sparkline?: number[];\n}\n\nexport class StockPanel extends Panel {\n  private refreshTimer: ReturnType<typeof setInterval> | null = null;\n\n  constructor() {\n    super({ id: 'stocks', title: 'Stock Market', showCount: true });\n    this.fetchData();\n    this.refreshTimer = setInterval(() => this.fetchData(), 60_000);\n  }\n\n  private async fetchData(): Promise<void> {\n    if (this.isFetching) return;\n    this.setFetching(true);\n    try {\n      const quotes = await fetchStockQuotes(); // from data-service\n      this.render(quotes);\n      this.setCount(quotes.length);\n    } catch (err) {\n      this.showError('Failed to load stock data', () => this.fetchData());\n    } finally {\n      this.setFetching(false);\n    }\n  }\n\n  private render(quotes: StockQuote[]): void {\n    const rows = quotes.map(q => `\n      <div class=\"stock-row\">\n        <span class=\"stock-symbol\">${q.symbol}</span>\n        <span class=\"stock-name\">${q.name}</span>\n        <span class=\"stock-price\">${q.price != null ? '$' + q.price.toFixed(2) : '—'}</span>\n        <span class=\"stock-change ${(q.change ?? 0) >= 0 ? 'positive' : 'negative'}\">\n          ${q.change != null ? (q.change >= 0 ? '+' : '') + q.change.toFixed(2) + '%' : '—'}\n        </span>\n      </div>\n    `).join('');\n\n    this.setContent(`<div class=\"stock-list\">${rows}</div>`);\n  }\n\n  public override destroy(): void {\n    if (this.refreshTimer) clearInterval(this.refreshTimer);\n    super.destroy();\n  }\n}\n```\n\n## Key Patterns\n\n1. **Constructor** calls `super()` with panel config, then triggers initial data fetch\n2. **fetchData()** is async, uses `isFetching` guard, shows error on failure with retry\n3. **render()** builds HTML strings and calls `this.setContent(html)`\n4. **destroy()** cleans up timers and event listeners\n5. Use `showLoading()` during initial load (auto-called in constructor)\n6. Use `showError(msg, retryFn)` on failure\n7. Sparkline SVGs use inline `<svg>` with `<polyline>` — see sparkline utility\n\n## Sparkline Utility\n\n```typescript\nexport function miniSparkline(data: number[] | undefined, change: number | null, w = 50, h = 16): string {\n  if (!data || data.length < 2) return '';\n  const min = Math.min(...data);\n  const max = Math.max(...data);\n  const range = max - min || 1;\n  const color = change != null && change >= 0 ? 'var(--green)' : 'var(--red)';\n  const points = data.map((v, i) => {\n    const x = (i / (data.length - 1)) * w;\n    const y = h - ((v - min) / range) * (h - 2) - 1;\n    return `${x.toFixed(1)},${y.toFixed(1)}`;\n  }).join(' ');\n  return `<svg width=\"${w}\" height=\"${h}\" viewBox=\"0 0 ${w} ${h}\"><polyline points=\"${points}\" fill=\"none\" stroke=\"${color}\" stroke-width=\"1.2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>`;\n}\n```\n", "examples/localStorageUtils.ts": ""}


skill_id: panel-component-enhanced-merged__v0_f2660195
name: panel-visual-badges
path: example/skills/panel-visual-badges/SKILL.md
category: workflow
visibility: private
lineage_origin: derived
lineage_source_task_id: evolution-task-08
lineage_created_at: 2026-03-10T15:33:14.674317
lineage_created_by: openrouter/deepseek/deepseek-chat
first_seen: 2026-03-10T15:33:14.674344
last_updated: 2026-03-10T15:33:14.674345
description:
Create a dashboard panel component using vanilla TypeScript DOM API, following the worldmonitor Panel architecture. Panels have a header with title/count, scrollable content area, loading/error states, and resize handles.

lineage_change_summary:
Merged panel component features with UI enhancements to create a unified skill for building visually rich, robust TypeScript UI components with panel architecture.

lineage_content_snapshot:
{"SKILL.md": "---\nname: robust-ui-panel-components\ndescription: Create visually rich, robust TypeScript UI components with panel architecture, combining enhanced features like retry logic, state persistence, and dynamic styling with a systematic workflow for visual enhancements.\n---\n\n# Robust UI Panel Components\n\nBuild visually rich, robust TypeScript UI components using a panel architecture pattern. This skill combines enhanced panel features (retry logic, state persistence) with a systematic workflow for adding visual enhancements (badges, thumbnails, dynamic styling).\n\n## Architecture Overview\n\n### Core Panel Features\n- **Retry Logic**: Automatically retries failed data fetches with exponential backoff.\n- **State Persistence**: Saves panel state (expanded/collapsed, size) to localStorage.\n- **Detailed Error Handling**: Provides granular error messages and recovery options.\n- **Visual Enhancements**: Systematic approach for adding badges, thumbnails, and dynamic styling.\n\n```\nPanel (base class)\n├── element: HTMLElement (outer container, .panel)\n│   ├── header: HTMLElement (.panel-header)\n│   │   ├── headerLeft (.panel-header-left)\n│   │   │   ├── title (.panel-title)\n│   │   │   └── newBadge (.panel-new-badge) [optional]\n│   │   ├── statusBadge (.panel-data-badge) [optional]\n│   │   └── countEl (.panel-count) [optional]\n│   ├── content: HTMLElement (.panel-content)\n│   └── resizeHandle (.panel-resize-handle)\n```\n\n## Implementation Workflow\n\n### 1. Define Constants and Configuration\n\n```typescript\n// Panel configuration\nconst PANEL_DEFAULTS = {\n  maxRetries: 3,\n  initialRetryDelay: 1000, // 1s\n};\n\n// Visual configuration\nconst SOURCE_COLORS: { [key: string]: string } = {\n  'Category A': '#FF6B6B',\n  'Category B': '#4ECDC4',\n  'default': '#95A5A6'\n};\n\nconst THUMBNAIL_SIZE = 48; // pixels\n```\n\n### 2. Base Panel Class with Enhanced Features\n\n```typescript\nexport class Panel {\n  protected element: HTMLElement;\n  protected content: HTMLElement;\n  protected panelId: string;\n  private retryAttempts = 0;\n  private retryDelay = PANEL_DEFAULTS.initialRetryDelay;\n\n  constructor(options: PanelOptions) {\n    // ... existing constructor code ...\n    this.loadState();\n  }\n\n  protected async fetchWithRetry(url: string):<any> {\n    while (this.retryAttempts < PANEL_DEFAULTS.maxRetries) {\n      try {\n        const response = await fetch(url);\n        if (!response.ok) throw new Error(`HTTP ${response.status}`);\n        return await response.json();\n      } catch (error) {\n        this.retryAttempts++;\n        if (this.retryAttempts >= PANEL_DEFAULTS.maxRetries) {\n          throw new Error(`Failed after ${PANEL_DEFAULTS.maxRetries} attempts: ${error.message}`);\n        }\n        await new Promise(resolve => setTimeout(resolve, this.retryDelay));\n        this.retryDelay *= 2;\n      }\n    }\n  }\n\n  public saveState(): void {\n    localStorage.setItem(`panelState_${this.panelId}`, JSON.stringify({\n      isExpanded: !this.element.classList.contains('collapsed'),\n      width: this.element.style.width,\n      height: this.element.style.height\n    }));\n  }\n\n  public loadState(): void {\n    const savedState = localStorage.getItem(`panelState_${this.panelId}`);\n    if (savedState) {\n      const { isExpanded, width, height } = JSON.parse(savedState);\n      if (!isExpanded) this.element.classList.add('collapsed');\n      if (width) this.element.style.width = width;\n      if (height) this.element.style.height = height;\n    }\n  }\n\n  // Visual enhancement helpers\n  protected getColorForSource(source: string): string {\n    return SOURCE_COLORS[source] || SOURCE_COLORS['default'];\n  }\n\n  protected getThumbnailUrl(imageUrl?: string): string {\n    return imageUrl || `https://via.placeholder.com/${THUMBNAIL_SIZE}`;\n  }\n}\n```\n\n### 3. Creating Enhanced Components\n\n```typescript\nexport class NewsPanel extends Panel {\n  private refreshTimer: ReturnType<typeof setInterval> | null = null;\n\n  constructor() {\n    super({ id: 'news', title: 'News Feed', showCount: true });\n    this.fetchData();\n    this.refreshTimer = setInterval(() => this.fetchData(), 60_000);\n  }\n\n  private async fetchData(): Promise<void> {\n    try {\n      const newsItems = await this.fetchWithRetry('/api/news');\n      this.render(newsItems);\n      this.setCount(newsItems.length);\n      this.saveState();\n    } catch (err) {\n      this.showError(`Failed to load news: ${err.message}`, () => {\n        this.retryAttempts = 0;\n        this.retryDelay = PANEL_DEFAULTS.initialRetryDelay;\n        this.fetchData();\n      });\n    }\n  }\n\n  private render(items: NewsItem[]): void {\n    const html = items.map(item => `\n      <div class=\"news-item\">\n        <img src=\"${this.getThumbnailUrl(item.imageUrl)}\" class=\"news-thumbnail\">\n        <div class=\"news-content\">\n          <h3>${item.title}</h3>\n          <span class=\"news-badge\" style=\"background-color: ${this.getColorForSource(item.source)};\">\n            ${item.source}\n          </span>\n          <p>${item.summary}</p>\n        </div>\n      </div>\n    `).join('');\n\n    this.setContent(`<div class=\"news-list\">${html}</div>`);\n  }\n\n  public override destroy(): void {\n    if (this.refreshTimer) clearInterval(this.refreshTimer);\n    super.destroy();\n  }\n}\n```\n\n### 4. Corresponding CSS\n\n```css\n/* Panel structure */\n.panel {\n  border: 1px solid #ddd;\n  border-radius: 8px;\n  overflow: hidden;\n}\n\n/* News item styles */\n.news-item {\n  display: flex;\n  gap: 16px;\n  padding: 12px;\n  border-bottom: 1px solid #eee;\n}\n\n.news-thumbnail {\n  width: 48px;\n  height: 48px;\n  border-radius: 4px;\n  object-fit: cover;\n}\n\n.news-badge {\n  display: inline-block;\n  padding: 2px 8px;\n  border-radius: 4px;\n  font-size: 12px;\n  color: white;\n  margin-right: 8px;\n}\n```\n\n## Key Patterns\n\n1. **Panel Architecture**: Base class handles core functionality (retry logic, state management)\n2. **Visual Enhancements**: Systematic workflow for adding badges, thumbnails, and dynamic styling\n3. **Separation of Concerns**:\n   - TypeScript for logic and data-driven styles\n   - CSS for structural and static styling\n4. **Error Handling**: Built-in retry logic with user-friendly error displays\n5. **State Management**: Automatic persistence of UI state\n\n## Best Practices\n\n1. **Constants First**: Define colors, sizes, and configuration at the top\n2. **Helper Methods**: Create focused methods for visual transformations\n3. **Template Literals**: Build HTML with clear conditional rendering\n4. **CSS Coordination**: Update stylesheets for every new visual element\n5. **Testing**: Verify with edge cases (missing data, network errors)\n"}


skill_id: data-service-merged__v0_0d607f2b
name: panel-data-resilient
path: example/skills/panel-data-resilient/SKILL.md
category: workflow
visibility: private
lineage_origin: derived
lineage_source_task_id: evolution-task-09
lineage_created_at: 2026-03-10T15:36:02.180993
lineage_created_by: openrouter/deepseek/deepseek-chat
first_seen: 2026-03-10T15:36:02.181003
last_updated: 2026-03-10T19:15:35.205548
description:
Create data fetching services with circuit breaker pattern for API resilience. Services handle fetch, cache, retry, and expose typed data to panel components.

lineage_change_summary:
Merged data-service and panel-component into a unified skill for creating resilient, data-driven dashboard panels with integrated API handling and UI components.

lineage_content_snapshot:
{"SKILL.md": "---\nname: resilient-panel\ndescription: Create resilient, data-driven dashboard panels with integrated API handling and UI components, combining circuit breaker patterns with vanilla TypeScript panel architecture.\n---\n\n# Resilient Panel Pattern\n\nThis skill merges the best of `data-service` and `panel-component` to create panels that are both resilient to API failures and easy to implement. Each panel manages its own data fetching with built-in circuit breaker patterns and renders content using vanilla TypeScript.\n\n## Architecture Overview\n\n1. **Service Module**: Each panel has a dedicated service module for data fetching, using circuit breakers for resilience.\n2. **Panel Class**: Extends a base `Panel` class with built-in loading/error states and content rendering.\n3. **Data Flow**: Panels automatically fetch data, handle errors, and retry as needed.\n\n## Implementation\n\n### 1. Circuit Breaker Utility\n\nCreate `src/utils/circuit-breaker.ts` (same as in `data-service` but with enhanced logging):\n\n```typescript\ninterface CircuitState {\n  failures: number;\n  cooldownUntil: number;\n}\n\ninterface CacheEntry<T> {\n  data: T;\n  timestamp: number;\n}\n\nexport interface CircuitBreakerOptions {\n  name: string;\n  maxFailures?: number;\n  cooldownMs?: number;\n  cacheTtlMs?: number;\n}\n\nexport class CircuitBreaker<T> {\n  private state: CircuitState = { failures: 0, cooldownUntil: 0 };\n  private cache: CacheEntry<T> | null = null;\n  private name: string;\n  private maxFailures: number;\n  private cooldownMs: number;\n  private cacheTtlMs: number;\n\n  constructor(options: CircuitBreakerOptions) {\n    this.name = options.name;\n    this.maxFailures = options.maxFailures ?? 2;\n    this.cooldownMs = options.cooldownMs ?? 5 * 60 * 1000;  // 5 minutes\n    this.cacheTtlMs = options.cacheTtlMs ?? 10 * 60 * 1000; // 10 minutes\n  }\n\n  isOnCooldown(): boolean {\n    if (Date.now() < this.state.cooldownUntil) return true;\n    if (this.state.cooldownUntil > 0) {\n      this.state = { failures: 0, cooldownUntil: 0 };\n    }\n    return false;\n  }\n\n  getCached(): T | null {\n    if (this.cache && Date.now() - this.cache.timestamp < this.cacheTtlMs) {\n      return this.cache.data;\n    }\n    return null;\n  }\n\n  recordSuccess(data: T): void {\n    this.state = { failures: 0, cooldownUntil: 0 };\n    this.cache = { data, timestamp: Date.now() };\n  }\n\n  recordFailure(error?: string): void {\n    this.state.failures++;\n    if (this.state.failures >= this.maxFailures) {\n      this.state.cooldownUntil = Date.now() + this.cooldownMs;\n      console.warn(`[${this.name}] Cooldown for ${this.cooldownMs / 1000}s`);\n    }\n  }\n\n  async execute<R extends T>(fn: () => Promise<R>, defaultValue: R): Promise<R> {\n    if (this.isOnCooldown()) {\n      const cached = this.getCached();\n      return (cached as R) ?? defaultValue;\n    }\n    const cached = this.getCached();\n    if (cached !== null) return cached as R;\n    try {\n      const result = await fn();\n      this.recordSuccess(result);\n      return result;\n    } catch (e) {\n      console.error(`[${this.name}] Failed:`, e);\n      this.recordFailure(String(e));\n      return defaultValue;\n    }\n  }\n}\n\nexport function createCircuitBreaker<T>(options: CircuitBreakerOptions): CircuitBreaker<T> {\n  return new CircuitBreaker<T>(options);\n}\n```\n\n### 2. Base Panel Class\n\nCreate `src/components/Panel.ts` with enhanced error handling and retry logic:\n\n```typescript\nexport interface PanelOptions {\n  id: string;\n  title: string;\n  showCount?: boolean;\n  className?: string;\n}\n\nexport class Panel {\n  protected element: HTMLElement;\n  protected content: HTMLElement;\n  protected header: HTMLElement;\n  protected countEl: HTMLElement | null = null;\n  protected panelId: string;\n  private _fetching = false;\n\n  constructor(options: PanelOptions) {\n    this.panelId = options.id;\n    this.element = document.createElement('div');\n    this.element.className = `panel ${options.className || ''}`;\n    this.element.dataset.panel = options.id;\n\n    // Header\n    this.header = document.createElement('div');\n    this.header.className = 'panel-header';\n\n    const headerLeft = document.createElement('div');\n    headerLeft.className = 'panel-header-left';\n\n    const title = document.createElement('span');\n    title.className = 'panel-title';\n    title.textContent = options.title;\n    headerLeft.appendChild(title);\n    this.header.appendChild(headerLeft);\n\n    // Count badge (optional)\n    if (options.showCount) {\n      this.countEl = document.createElement('span');\n      this.countEl.className = 'panel-count';\n      this.countEl.textContent = '0';\n      this.header.appendChild(this.countEl);\n    }\n\n    // Content area\n    this.content = document.createElement('div');\n    this.content.className = 'panel-content';\n    this.content.id = `${options.id}Content`;\n\n    this.element.appendChild(this.header);\n    this.element.appendChild(this.content);\n    this.showLoading();\n  }\n\n  public getElement(): HTMLElement { return this.element; }\n\n  public showLoading(message = 'Loading...'): void {\n    this.content.innerHTML = `\n      <div class=\"panel-loading\">\n        <div class=\"panel-loading-spinner\"></div>\n        <div class=\"panel-loading-text\">${message}</div>\n      </div>`;\n  }\n\n  public showError(message = 'Failed to load', onRetry?: () => void): void {\n    this.content.innerHTML = `\n      <div class=\"panel-error-state\">\n        <div class=\"panel-error-msg\">${message}</div>\n        ${onRetry ? '<button class=\"panel-retry-btn\" data-panel-retry>Retry</button>' : ''}\n      </div>`;\n    if (onRetry) {\n      this.content.querySelector('[data-panel-retry]')?.addEventListener('click', onRetry);\n    }\n  }\n\n  public setContent(html: string): void {\n    this.content.innerHTML = html;\n  }\n\n  public setCount(count: number): void {\n    if (this.countEl) this.countEl.textContent = count.toString();\n  }\n\n  public show(): void { this.element.classList.remove('hidden'); }\n  public hide(): void { this.element.classList.add('hidden'); }\n\n  protected setFetching(v: boolean): void { this._fetching = v; }\n  protected get isFetching(): boolean { return this._fetching; }\n\n  public destroy(): void {\n    this.element.remove();\n  }\n}\n```\n\n### 3. Example: StockPanel with Integrated Service\n\n```typescript\nimport { Panel } from './Panel';\nimport { createCircuitBreaker } from '../utils/circuit-breaker';\n\ninterface StockQuote {\n  symbol: string;\n  name: string;\n  price: number | null;\n  change: number | null;\n  sparkline?: number[];\n}\n\nconst breaker = createCircuitBreaker<StockQuote[]>({\n  name: 'StockMarket',\n  cacheTtlMs: 60_000,\n});\n\nexport class StockPanel extends Panel {\n  private refreshTimer: ReturnType<typeof setInterval> | null = null;\n\n  constructor() {\n    super({ id: 'stocks', title: 'Stock Market', showCount: true });\n    this.fetchData();\n    this.refreshTimer = setInterval(() => this.fetchData(), 60_000);\n  }\n\n  private async fetchData(): Promise<void> {\n    if (this.isFetching) return;\n    this.setFetching(true);\n    try {\n      const quotes = await breaker.execute(async () => {\n        const resp = await fetch('/api/stocks');\n        if (!resp.ok) throw new Error(`HTTP ${resp.status}`);\n        return resp.json();\n      }, []);\n      this.render(quotes);\n      this.setCount(quotes.length);\n    } catch (err) {\n      this.showError('Failed to load stock data', () => this.fetchData());\n    } finally {\n      this.setFetching(false);\n    }\n  }\n\n  private render(quotes: StockQuote[]): void {\n    const rows = quotes.map(q => `\n      <div class=\"stock-row\">\n        <span class=\"stock-symbol\">${q.symbol}</span>\n        <span class=\"stock-name\">${q.name}</span>\n        <span class=\"stock-price\">${q.price != null ? '$' + q.price.toFixed(2) : '—'}</span>\n        <span class=\"stock-change ${(q.change ?? 0) >= 0 ? 'positive' : 'negative'}\">\n          ${q.change != null ? (q.change >= 0 ? '+' : '') + q.change.toFixed(2) + '%' : '—'}\n        </span>\n      </div>\n    `).join('');\n\n    this.setContent(`<div class=\"stock-list\">${rows}</div>`);\n  }\n\n  public override destroy(): void {\n    if (this.refreshTimer) clearInterval(this.refreshTimer);\n    super.destroy();\n  }\n}\n```\n\n## Key Patterns\n\n1. **Integrated Data Handling**: Each panel manages its own data fetching with built-in circuit breakers.\n2. **Resilient UI**: Automatic retry logic and graceful degradation when APIs fail.\n3. **Simple Implementation**: Just extend `Panel` and implement `fetchData()` and `render()`.\n4. **Type Safety**: Strongly typed data interfaces throughout.\n"}


skill_id: data-service-merged-merged__v0_66b2d5a2
name: panel-breaker-stateful
path: example/skills/panel-breaker-stateful/SKILL.md
category: workflow
visibility: private
lineage_origin: derived
lineage_source_task_id: evolution-task-12
lineage_created_at: 2026-03-10T15:41:52.613972
lineage_created_by: openrouter/deepseek/deepseek-chat
first_seen: 2026-03-10T15:41:52.613999
last_updated: 2026-03-10T15:41:52.614000
description:
Create data fetching services with circuit breaker pattern for API resilience. Services handle fetch, cache, retry, and expose typed data to panel components.

lineage_change_summary:
Merged resilient data handling with enhanced panel features to create a unified dashboard panel pattern with circuit breakers, retry logic, and state persistence.

lineage_content_snapshot:
{"SKILL.md": "---\nname: resilient-panel-enhanced\ndescription: Unified dashboard panel pattern combining circuit breaker resilience with enhanced UI features like retry logic, state persistence, and detailed error handling.\n---\n\n# Resilient Dashboard Panel Pattern\n\nThis skill merges the best of `data-service-merged` and `panel-component-enhanced` to create panels that are resilient to API failures while offering enhanced UI features. Each panel manages its own data fetching with built-in circuit breakers and retry logic, and persists its state to localStorage.\n\n## Architecture Overview\n\n1. **Service Module**: Each panel has a dedicated service module for data fetching, using circuit breakers for resilience.\n2. **Panel Class**: Extends a base `Panel` class with built-in loading/error states, retry logic, and state persistence.\n3. **Data Flow**: Panels automatically fetch data, handle errors with retries, and persist UI state.\n\n## Implementation\n\n### 1. Circuit Breaker Utility with Enhanced Logging\n\nCreate `src/utils/circuit-breaker.ts`:\n\n```typescript\ninterface CircuitState {\n  failures: number;\n  cooldownUntil: number;\n}\n\ninterface CacheEntry<T> {\n  data: T;\n  timestamp: number;\n}\n\nexport interface CircuitBreakerOptions {\n  name: string;\n  maxFailures?: number;\n  cooldownMs?: number;\n  cacheTtlMs?: number;\n}\n\nexport class CircuitBreaker<T> {\n  private state: CircuitState = { failures: 0, cooldownUntil: 0 };\n  private cache: CacheEntry<T> | null = null;\n  private name: string;\n  private maxFailures: number;\n  private cooldownMs: number;\n  private cacheTtlMs: number;\n\n  constructor(options: CircuitBreakerOptions) {\n    this.name = options.name;\n    this.maxFailures = options.maxFailures ?? 2;\n    this.cooldownMs = options.cooldownMs ?? 5 * 60 * 1000;  // 5 minutes\n    this.cacheTtlMs = options.cacheTtlMs ?? 10 * 60 * 1000; // 10 minutes\n  }\n\n  isOnCooldown(): boolean {\n    if (Date.now() < this.state.cooldownUntil) return true;\n    if (this.state.cooldownUntil > 0) {\n      this.state = { failures: 0, cooldownUntil: 0 };\n    }\n    return false;\n  }\n\n  getCached(): T | null {\n    if (this.cache && Date.now() - this.cache.timestamp < this.cacheTtlMs) {\n      return this.cache.data;\n    }\n    return null;\n  }\n\n  recordSuccess(data: T): void {\n    this.state = { failures: 0, cooldownUntil: 0 };\n    this.cache = { data, timestamp: Date.now() };\n    console.log(`[${this.name}] Successfully fetched data`);\n  }\n\n  recordFailure(error?: string): void {\n    this.state.failures++;\n    if (this.state.failures >= this.maxFailures) {\n      this.state.cooldownUntil = Date.now() + this.cooldownMs;\n      console.warn(`[${this.name}] Cooldown for ${this.cooldownMs / 1000}s`);\n    }\n    console.error(`[${this.name}] Failure ${this.state.failures}/${this.maxFailures}:`, error);\n  }\n\n  async execute<R extends T>(fn: () => Promise<R>, defaultValue: R): Promise<R> {\n    if (this.isOnCooldown()) {\n      const cached = this.getCached();\n      return (cached as R) ?? defaultValue;\n    }\n    const cached = this.getCached();\n    if (cached !== null) return cached as R;\n    try {\n      const result = await fn();\n      this.recordSuccess(result);\n      return result;\n    } catch (e) {\n      this.recordFailure(String(e));\n      return defaultValue;\n    }\n  }\n}\n\nexport function createCircuitBreaker<T>(options: CircuitBreakerOptions): CircuitBreaker<T> {\n  return new CircuitBreaker<T>(options);\n}\n```\n\n### 2. Base Panel Class with Retry Logic and State Persistence\n\nCreate `src/components/Panel.ts`:\n\n```typescript\nexport interface PanelOptions {\n  id: string;\n  title: string;\n  showCount?: boolean;\n  className?: string;\n}\n\nexport class Panel {\n  protected element: HTMLElement;\n  protected content: HTMLElement;\n  protected header: HTMLElement;\n  protected countEl: HTMLElement | null = null;\n  protected panelId: string;\n  private _fetching = false;\n  private retryAttempts = 0;\n  private maxRetries = 3;\n  private retryDelay = 1000;\n\n  constructor(options: PanelOptions) {\n    this.panelId = options.id;\n    this.element = document.createElement('div');\n    this.element.className = `panel ${options.className || ''}`;\n    this.element.dataset.panel = options.id;\n\n    // Header\n    this.header = document.createElement('div');\n    this.header.className = 'panel-header';\n\n    const headerLeft = document.createElement('div');\n    headerLeft.className = 'panel-header-left';\n\n    const title = document.createElement('span');\n    title.className = 'panel-title';\n    title.textContent = options.title;\n    headerLeft.appendChild(title);\n    this.header.appendChild(headerLeft);\n\n    if (options.showCount) {\n      this.countEl = document.createElement('span');\n      this.countEl.className = 'panel-count';\n      this.countEl.textContent = '0';\n      this.header.appendChild(this.countEl);\n    }\n\n    this.content = document.createElement('div');\n    this.content.className = 'panel-content';\n    this.content.id = `${options.id}Content`;\n\n    this.element.appendChild(this.header);\n    this.element.appendChild(this.content);\n    this.loadState();\n    this.showLoading();\n  }\n\n  public getElement(): HTMLElement { return this.element; }\n\n  public showLoading(message = 'Loading...'): void {\n    this.content.innerHTML = `\n      <div class=\"panel-loading\">\n        <div class=\"panel-loading-spinner\"></div>\n        <div class=\"panel-loading-text\">${message}</div>\n      </div>`;\n  }\n\n  public showError(message = 'Failed to load', onRetry?: () => void): void {\n    this.content.innerHTML = `\n      <div class=\"panel-error-state\">\n        <div class=\"panel-error-msg\">${message}</div>\n        ${onRetry ? '<button class=\"panel-retry-btn\" data-panel-retry>Retry</button>' : ''}\n      </div>`;\n    if (onRetry) {\n      this.content.querySelector('[data-panel-retry]')?.addEventListener('click', onRetry);\n    }\n  }\n\n  public setContent(html: string): void {\n    this.content.innerHTML = html;\n  }\n\n  public setCount(count: number): void {\n    if (this.countEl) this.countEl.textContent = count.toString();\n  }\n\n  public show(): void { this.element.classList.remove('hidden'); }\n  public hide(): void { this.element.classList.add('hidden'); }\n\n  protected setFetching(v: boolean): void { this._fetching = v; }\n  protected get isFetching(): boolean { return this._fetching; }\n\n  protected async fetchWithRetry(url: string): Promise<any> {\n    while (this.retryAttempts < this.maxRetries) {\n      try {\n        const response = await fetch(url);\n        if (!response.ok) throw new Error(`HTTP ${response.status}`);\n        return await response.json();\n      } catch (error) {\n        this.retryAttempts++;\n        if (this.retryAttempts >= this.maxRetries) {\n          throw new Error(`Failed after ${this.maxRetries} attempts: ${error.message}`);\n        }\n        await new Promise(resolve => setTimeout(resolve, this.retryDelay));\n        this.retryDelay *= 2;\n      }\n    }\n  }\n\n  public saveState(): void {\n    localStorage.setItem(`panelState_${this.panelId}`, JSON.stringify({\n      isExpanded: !this.element.classList.contains('collapsed'),\n      width: this.element.style.width,\n      height: this.element.style.height\n    }));\n  }\n\n  public loadState(): void {\n    const savedState = localStorage.getItem(`panelState_${this.panelId}`);\n    if (savedState) {\n      const { isExpanded, width, height } = JSON.parse(savedState);\n      if (!isExpanded) this.element.classList.add('collapsed');\n      if (width) this.element.style.width = width;\n      if (height) this.element.style.height = height;\n    }\n  }\n\n  public destroy(): void {\n    this.element.remove();\n  }\n}\n```\n\n### 3. Example: StockPanel with Integrated Service\n\n```typescript\nimport { Panel } from './Panel';\nimport { createCircuitBreaker } from '../utils/circuit-breaker';\n\ninterface StockQuote {\n  symbol: string;\n  name: string;\n  price: number | null;\n  change: number | null;\n  sparkline?: number[];\n}\n\nconst breaker = createCircuitBreaker<StockQuote[]>({\n  name: 'StockMarket',\n  cacheTtlMs: 60_000,\n});\n\nexport class StockPanel extends Panel {\n  private refreshTimer: ReturnType<typeof setInterval> | null = null;\n\n  constructor() {\n    super({ id: 'stocks', title: 'Stock Market', showCount: true });\n    this.fetchData();\n    this.refreshTimer = setInterval(() => this.fetchData(), 60_000);\n  }\n\n  private async fetchData(): Promise<void> {\n    if (this.isFetching) return;\n    this.setFetching(true);\n    try {\n      const quotes = await breaker.execute(async () => {\n        const resp = await fetch('/api/stocks');\n        if (!resp.ok) throw new Error(`HTTP ${resp.status}`);\n        return resp.json();\n      }, []);\n      this.render(quotes);\n      this.setCount(quotes.length);\n      this.saveState();\n    } catch (err) {\n      this.showError(`Failed to load stock data: ${err.message}`, () => {\n        this.retryAttempts = 0;\n        this.retryDelay = 1000;\n        this.fetchData();\n      });\n    } finally {\n      this.setFetching(false);\n    }\n  }\n\n  private render(quotes: StockQuote[]): void {\n    const rows = quotes.map(q => `\n      <div class=\"stock-row\">\n        <span class=\"stock-symbol\">${q.symbol}</span>\n        <span class=\"stock-name\">${q.name}</span>\n        <span class=\"stock-price\">${q.price != null ? '$' + q.price.toFixed(2) : '—'}</span>\n        <span class=\"stock-change ${(q.change ?? 0) >= 0 ? 'positive' : 'negative'}\">\n          ${q.change != null ? (q.change >= 0 ? '+' : '') + q.change.toFixed(2) + '%' : '—'}\n        </span>\n      </div>\n    `).join('');\n\n    this.setContent(`<div class=\"stock-list\">${rows}</div>`);\n  }\n\n  public override destroy(): void {\n    if (this.refreshTimer) clearInterval(this.refreshTimer);\n    super.destroy();\n  }\n}\n```\n\n## Key Patterns\n\n1. **Resilient Data Fetching**: Circuit breakers prevent cascading failures while retry logic handles transient errors.\n2. **State Persistence**: Panel state (size, expansion) is saved to localStorage.\n3. **Detailed Error Handling**: Users see specific error messages and can retry failed operations.\n4. **Type Safety**: Strongly typed interfaces throughout the codebase.\n"}


skill_id: panel-component-merged__v0_1464da16
name: data-driven-panel
path: example/skills/data-driven-panel/SKILL.md
category: workflow
visibility: private
lineage_origin: derived
lineage_source_task_id: evolution-task-13
lineage_created_at: 2026-03-10T15:44:34.113093
lineage_created_by: openrouter/deepseek/deepseek-chat
first_seen: 2026-03-10T15:44:34.113163
last_updated: 2026-03-10T15:44:34.113165
lineage_change_summary:
Merged panel-component and data-service into a unified skill for creating data-driven panel components with built-in resilient data fetching.

lineage_content_snapshot:
{"SKILL.md": "---\nname: data-driven-panel\ndescription: Create dashboard panel components with integrated resilient data services, combining UI construction and data fetching into a unified pattern.\n---\n\n# Data-Driven Panel Pattern\n\nCreate dashboard panels that seamlessly integrate UI construction with resilient data fetching. Each panel is a self-contained component that manages its own data lifecycle.\n\n## Architecture Overview\n\n```\nPanel (base class)\n├── element: HTMLElement (outer container)\n│   ├── header (title, status indicators)\n│   └── content (data display area)\n└── DataService (internal)\n    ├── Circuit Breaker (failure handling)\n    └── Caching layer\n```\n\n## Implementation\n\n### 1. Base Panel with Integrated Data Service\n\nCreate `src/components/DataPanel.ts`:\n\n```typescript\nimport { createCircuitBreaker } from '../utils/circuit-breaker';\n\nexport interface PanelOptions<T> {\n  id: string;\n  title: string;\n  showCount?: boolean;\n  className?: string;\n  dataService: () => Promise<T>;\n  defaultValue: T;\n  serviceOptions?: {\n    maxFailures?: number;\n    cooldownMs?: number;\n    cacheTtlMs?: number;\n  };\n}\n\nexport abstract class DataPanel<T> {\n  protected element: HTMLElement;\n  protected content: HTMLElement;\n  private breaker: CircuitBreaker<T>;\n  private refreshTimer: ReturnType<typeof setInterval> | null = null;\n\n  constructor(protected options: PanelOptions<T>) {\n    this.breaker = createCircuitBreaker<T>({\n      name: options.id,\n      maxFailures: options.serviceOptions?.maxFailures ?? 2,\n      cooldownMs: options.serviceOptions?.cooldownMs ?? 300_000,\n      cacheTtlMs: options.serviceOptions?.cacheTtlMs ?? 60_000,\n    });\n\n    this.element = document.createElement('div');\n    this.element.className = `panel ${options.className || ''}`;\n    this.element.dataset.panel = options.id;\n\n    // Initialize UI\n    this.initUI();\n    this.fetchData();\n  }\n\n  private initUI(): void {\n    // Header setup\n    const header = document.createElement('div');\n    header.className = 'panel-header';\n    \n    const title = document.createElement('span');\n    title.className = 'panel-title';\n    title.textContent = this.options.title;\n    header.appendChild(title);\n\n    // Content area\n    this.content = document.createElement('div');\n    this.content.className = 'panel-content';\n\n    this.element.appendChild(header);\n    this.element.appendChild(this.content);\n    this.showLoading();\n  }\n\n  protected async fetchData(): Promise<void> {\n    try {\n      const data = await this.breaker.execute(\n        this.options.dataService,\n        this.options.defaultValue\n      );\n      this.render(data);\n    } catch (err) {\n      this.showError('Failed to load data', () => this.fetchData());\n    }\n  }\n\n  protected abstract render(data: T): void;\n\n  public showLoading(message = 'Loading...'): void {\n    this.content.innerHTML = `<div class=\"loading\">${message}</div>`;\n  }\n\n  public showError(message: string, retryHandler: () => void): void {\n    this.content.innerHTML = `\n      <div class=\"error\">\n        <p>${message}</p>\n        <button class=\"retry-btn\">Retry</button>\n      </div>`;\n    this.content.querySelector('.retry-btn')?.addEventListener('click', retryHandler);\n  }\n\n  public setAutoRefresh(intervalMs: number): void {\n    this.refreshTimer = setInterval(() => this.fetchData(), intervalMs);\n  }\n\n  public destroy(): void {\n    if (this.refreshTimer) clearInterval(this.refreshTimer);\n    this.element.remove();\n  }\n}\n```\n\n### 2. Concrete Panel Implementation Example\n\n```typescript\nimport { DataPanel } from './DataPanel';\n\ninterface StockData {\n  quotes: StockQuote[];\n  updatedAt: string;\n}\n\nexport class StockPanel extends DataPanel<StockData> {\n  constructor() {\n    super({\n      id: 'stocks',\n      title: 'Stock Market',\n      dataService: fetchStockData,\n      defaultValue: { quotes: [], updatedAt: new Date().toISOString() },\n      serviceOptions: {\n        cacheTtlMs: 30_000 // Refresh cache every 30 seconds\n      }\n    });\n    this.setAutoRefresh(60_000); // Auto-refresh every minute\n  }\n\n  protected render(data: StockData): void {\n    const html = data.quotes.map(quote => `\n      <div class=\"stock-item\">\n        <span class=\"symbol\">${quote.symbol}</span>\n        <span class=\"price\">$${quote.price?.toFixed(2) || '—'}</span>\n      </div>\n    `).join('');\n    \n    this.content.innerHTML = `\n      <div class=\"stock-list\">\n        ${html}\n        <div class=\"updated\">Updated: ${new Date(data.updatedAt).toLocaleTimeString()}</div>\n      </div>`;\n  }\n}\n```\n\n### 3. Data Service Implementation\n\n```typescript\n// src/services/stockService.ts\ninterface StockQuote {\n  symbol: string;\n  price: number | null;\n}\n\nexport async function fetchStockData(): Promise<StockData> {\n  const response = await fetch('/api/stocks');\n  if (!response.ok) throw new Error(`HTTP ${response.status}`);\n  const quotes: StockQuote[] = await response.json();\n  return {\n    quotes,\n    updatedAt: new Date().toISOString()\n  };\n}\n```\n\n## Key Benefits\n\n1. **Unified Pattern**: Combines UI and data logic in one coherent pattern\n2. **Resilient Data Fetching**: Built-in circuit breaker and caching\n3. **Simplified Implementation**: Concrete panels only need to implement rendering\n4. **Consistent Behavior**: All panels share the same loading/error states\n5. **Flexible Configuration**: Service options customizable per panel\n\n## Best Practices\n\n1. **Service Composition**: Create small, focused services for each data type\n2. **Default Values**: Always provide meaningful fallback data\n3. **Error Boundaries**: Use the built-in error handling for consistency\n4. **Memory Management**: Clean up timers and event listeners in destroy()\n5. **Type Safety**: Maintain strong typing throughout the data flow\n"}


skill_id: panel-component-enhanced_4b417d__v0_353c6767
name: panel-event-forms
path: example/skills/panel-event-forms/SKILL.md
category: workflow
visibility: private
lineage_origin: derived
lineage_source_task_id: evolution-task-14
lineage_created_at: 2026-03-10T15:47:33.862570
lineage_created_by: openrouter/deepseek/deepseek-chat
first_seen: 2026-03-10T15:47:33.862582
last_updated: 2026-03-10T15:47:33.862583
lineage_change_summary:
Enhanced panel-component skill with detailed event handling and form validation examples in a new FinancePanel implementation.

lineage_content_diff:
--- a/SKILL.md
+++ b/SKILL.md
@@ -1,11 +1,11 @@
 ---
-name: panel-component
-description: Create a dashboard panel component using vanilla TypeScript DOM API, following the worldmonitor Panel architecture. Panels have a header with title/count, scrollable content area, loading/error states, and resize handles.
+name: panel-component-enhanced
+description: Create dashboard panel components with advanced event handling and form validation using vanilla TypeScript DOM API, following the worldmonitor Panel architecture.
 ---
 
-# Panel Component Pattern
-
-Create dashboard panel components using **vanilla TypeScript** (no framework, no JSX). Each panel is a class extending a `Panel` base class.
+# Enhanced Panel Component Pattern
+
+Create dashboard panel components using **vanilla TypeScript** (no framework, no JSX). Each panel is a class extending a `Panel` base class, now with enhanced event handling and form validation examples.
 
 ## Architecture Overview
 
@@ -83,7 +83,7 @@
 
   public showLoading(message = 'Loading...'): void {
     this.content.innerHTML = `
-      <div class="panel-loading">
+     <div class="panel-loading">
         <div class="panel-loading-spinner"></div>
         <div class="panel-loading-text">${message}</div>
       </div>`;
@@ -120,61 +120,142 @@
 }
 ```
 
-## Creating a Concrete Panel (Example: StockPanel)
-
-Each panel extends `Panel` and manages its own data fetching + rendering:
+## Creating a Concrete Panel (Example: FinancePanel)
+
+Each panel extends `Panel` and manages its own data fetching + rendering, now with enhanced event handling and form validation:
 
 ```typescript
 import { Panel } from './Panel';
 
-interface StockQuote {
-  symbol: string;
-  name: string;
-  price: number | null;
-  change: number | null;
-  sparkline?: number[];
-}
-
-export class StockPanel extends Panel {
-  private refreshTimer: ReturnType<typeof setInterval> | null = null;
+interface Transaction {
+  id: string;
+  amount: number;
+  description: string;
+  date: string;
+  category: string;
+}
+
+export class FinancePanel extends Panel {
+  private transactions: Transaction[] = [];
+  private balance: number = 0;
 
   constructor() {
-    super({ id: 'stocks', title: 'Stock Market', showCount: true });
-    this.fetchData();
-    this.refreshTimer = setInterval(() => this.fetchData(), 60_000);
-  }
-
-  private async fetchData(): Promise<void> {
-    if (this.isFetching) return;
-    this.setFetching(true);
-    try {
-      const quotes = await fetchStockQuotes(); // from data-service
-      this.render(quotes);
-      this.setCount(quotes.length);
-    } catch (err) {
-      this.showError('Failed to load stock data', () => this.fetchData());
-    } finally {
-      this.setFetching(false);
-    }
-  }
-
-  private render(quotes: StockQuote[]): void {
-    const rows = quotes.map(q => `
-      <div class="stock-row">
-        <span class="stock-symbol">${q.symbol}</span>
-        <span class="stock-name">${q.name}</span>
-        <span class="stock-price">${q.price != null ? '$' + q.price.toFixed(2) : '—'}</span>
-        <span class="stock-change ${(q.change ?? 0) >= 0 ? 'positive' : 'negative'}">
-          ${q.change != null ? (q.change >= 0 ? '+' : '') + q.change.toFixed(2) + '%' : '—'}
+    super({ id: 'finance', title: 'Finance Tracker', showCount: true });
+    this.loadTransactions();
+    this.render();
+  }
+
+  private loadTransactions(): void {
+    const saved = localStorage.getItem('finance-transactions');
+    if (saved) {
+      this.transactions = JSON.parse(saved);
+      this.balance = this.transactions.reduce((sum, t) => sum + t.amount, 0);
+    }
+  }
+
+  private saveTransactions(): void {
+    localStorage.setItem('finance-transactions', JSON.stringify(this.transactions));
+  }
+
+  private render(): void {
+    const formHtml = `
+      <form id="financeForm" class="finance-form">
+        <div class="form-group">
+          <label for="amount">Amount</label>
+          <input type="number" id="amount" name="amount" required>
+          <div class="error-message" id="amountError"></div>
+        </div>
+        <div class="form-group">
+          <label for="description">Description</label>
+          <input type="text" id="description" name="description" required>
+          <div class="error-message" id="descriptionError"></div>
+        </div>
+        <button type="submit">Add Transaction</button>
+      </form>
+    `;
+
+    const transactionsHtml = this.transactions.map(t => `
+      <div class="transaction">
+        <span class="transaction-date">${t.date}</span>
+        <span class="transaction-description">${t.description}</span>
+        <span class="transaction-amount ${t.amount >= 0 ? 'positive' : 'negative'}">
+          ${t.amount >= 0 ? '+' : ''}${t.amount.toFixed(2)}
         </span>
       </div>
     `).join('');
 
-    this.setContent(`<div class="stock-list">${rows}</div>`);
+    this.setContent(`
+      <div class="finance-container">
+        <div class="balance">Balance: $${this.balance.toFixed(2)}</div>
+        ${formHtml}
+        <div class="transactions">${transactionsHtml}</div>
+      </div>
+    `);
+
+    this.setupForm();
+  }
+
+  private setupForm(): void {
+    const form = this.content.querySelector('#financeForm') as HTMLFormElement;
+    if (!form) return;
+
+    form.addEventListener('submit', (e) => {
+      e.preventDefault();
+      if (this.validateForm()) {
+        this.addTransaction();
+      }
+    });
+  }
+
+  private validateForm(): boolean {
+    let isValid = true;
+    const amountInput = this.content.querySelector('#amount') as HTMLInputElement;
+    const descriptionInput = this.content.querySelector('#description') as HTMLInputElement;
+    const amountError = this.content.querySelector('#amountError') as HTMLElement;
+    const descriptionError = this.content.querySelector('#descriptionError') as HTMLElement;
+
+    // Reset errors
+    amountError.textContent = '';
+    descriptionError.textContent = '';
+
+    // Validate amount
+    if (!amountInput.value) {
+      amountError.textContent = 'Amount is required';
+      isValid = false;
+    } else if (isNaN(parseFloat(amountInput.value))) {
+      amountError.textContent = 'Amount must be a number';
+      isValid = false;
+    }
+
+    // Validate description
+    if (!descriptionInput.value) {
+      descriptionError.textContent = 'Description is required';
+      isValid = false;
+    }
+
+    return isValid;
+  }
+
+  private addTransaction(): void {
+    const amountInput = this.content.querySelector('#amount') as HTMLInputElement;
+    const descriptionInput = this.content.querySelector('#description') as HTMLInputElement;
+
+    const transaction: Transaction = {
+      id: Date.now().toString(),
+      amount: parseFloat(amountInput.value),
+      description: descriptionInput.value,
+      date: new Date().toLocaleDateString(),
+      category: 'General',
+    };
+
+    this.transactions.push(transaction);
+    this.balance += transaction.amount;
+    this.saveTransactions();
+    this.render();
   }
 
   public override destroy(): void {
-    if (this.refreshTimer) clearInterval(this.refreshTimer);
+    // Clean up any event listeners
     super.destroy();
   }
 }
@@ -182,13 +263,12 @@
 
 ## Key Patterns
 
-1. **Constructor** calls `super()` with panel config, then triggers initial data fetch
-2. **fetchData()** is async, uses `isFetching` guard, shows error on failure with retry
-3. **render()** builds HTML strings and calls `this.setContent(html)`
-4. **destroy()** cleans up timers and event listeners
-5. Use `showLoading()` during initial load (auto-called in constructor)
-6. Use `showError(msg, retryFn)` on failure
-7. Sparkline SVGs use inline `<svg>` with `<polyline>` — see sparkline utility
+1. **Constructor** calls `super()` with panel config, then loads initial data
+2. **Form Handling** includes validation and submission logic
+3. **Event Listeners** are properly set up and cleaned up in `destroy()`
+4. **Local Storage** is used for persistent data
+5. **Validation** provides user feedback on form errors
+6. **Dynamic Updates** reflect changes immediately in the UI
 
 ## Sparkline Utility
 

lineage_content_snapshot:
{"SKILL.md": "---\nname: panel-component-enhanced\ndescription: Create dashboard panel components with advanced event handling and form validation using vanilla TypeScript DOM API, following the worldmonitor Panel architecture.\n---\n\n# Enhanced Panel Component Pattern\n\nCreate dashboard panel components using **vanilla TypeScript** (no framework, no JSX). Each panel is a class extending a `Panel` base class, now with enhanced event handling and form validation examples.\n\n## Architecture Overview\n\n```\nPanel (base class)\n├── element: HTMLElement (outer container, .panel)\n│   ├── header: HTMLElement (.panel-header)\n│   │   ├── headerLeft (.panel-header-left)\n│   │   │   ├── title (.panel-title)\n│   │   │   └── newBadge (.panel-new-badge) [optional]\n│   │   ├── statusBadge (.panel-data-badge) [optional]\n│   │   └── countEl (.panel-count) [optional]\n│   ├── content: HTMLElement (.panel-content)\n│   └── resizeHandle (.panel-resize-handle)\n```\n\n## Base Panel Class (Simplified for this project)\n\nCreate `src/components/Panel.ts`:\n\n```typescript\nexport interface PanelOptions {\n  id: string;\n  title: string;\n  showCount?: boolean;\n  className?: string;\n}\n\nexport class Panel {\n  protected element: HTMLElement;\n  protected content: HTMLElement;\n  protected header: HTMLElement;\n  protected countEl: HTMLElement | null = null;\n  protected panelId: string;\n  private _fetching = false;\n\n  constructor(options: PanelOptions) {\n    this.panelId = options.id;\n    this.element = document.createElement('div');\n    this.element.className = `panel ${options.className || ''}`;\n    this.element.dataset.panel = options.id;\n\n    // Header\n    this.header = document.createElement('div');\n    this.header.className = 'panel-header';\n\n    const headerLeft = document.createElement('div');\n    headerLeft.className = 'panel-header-left';\n\n    const title = document.createElement('span');\n    title.className = 'panel-title';\n    title.textContent = options.title;\n    headerLeft.appendChild(title);\n    this.header.appendChild(headerLeft);\n\n    // Count badge (optional)\n    if (options.showCount) {\n      this.countEl = document.createElement('span');\n      this.countEl.className = 'panel-count';\n      this.countEl.textContent = '0';\n      this.header.appendChild(this.countEl);\n    }\n\n    // Content area\n    this.content = document.createElement('div');\n    this.content.className = 'panel-content';\n    this.content.id = `${options.id}Content`;\n\n    this.element.appendChild(this.header);\n    this.element.appendChild(this.content);\n    this.showLoading();\n  }\n\n  public getElement(): HTMLElement { return this.element; }\n\n  public showLoading(message = 'Loading...'): void {\n    this.content.innerHTML = `\n     <div class=\"panel-loading\">\n        <div class=\"panel-loading-spinner\"></div>\n        <div class=\"panel-loading-text\">${message}</div>\n      </div>`;\n  }\n\n  public showError(message = 'Failed to load', onRetry?: () => void): void {\n    this.content.innerHTML = `\n      <div class=\"panel-error-state\">\n        <div class=\"panel-error-msg\">${message}</div>\n        ${onRetry ? '<button class=\"panel-retry-btn\" data-panel-retry>Retry</button>' : ''}\n      </div>`;\n    if (onRetry) {\n      this.content.querySelector('[data-panel-retry]')?.addEventListener('click', onRetry);\n    }\n  }\n\n  public setContent(html: string): void {\n    this.content.innerHTML = html;\n  }\n\n  public setCount(count: number): void {\n    if (this.countEl) this.countEl.textContent = count.toString();\n  }\n\n  public show(): void { this.element.classList.remove('hidden'); }\n  public hide(): void { this.element.classList.add('hidden'); }\n\n  protected setFetching(v: boolean): void { this._fetching = v; }\n  protected get isFetching(): boolean { return this._fetching; }\n\n  public destroy(): void {\n    this.element.remove();\n  }\n}\n```\n\n## Creating a Concrete Panel (Example: FinancePanel)\n\nEach panel extends `Panel` and manages its own data fetching + rendering, now with enhanced event handling and form validation:\n\n```typescript\nimport { Panel } from './Panel';\n\ninterface Transaction {\n  id: string;\n  amount: number;\n  description: string;\n  date: string;\n  category: string;\n}\n\nexport class FinancePanel extends Panel {\n  private transactions: Transaction[] = [];\n  private balance: number = 0;\n\n  constructor() {\n    super({ id: 'finance', title: 'Finance Tracker', showCount: true });\n    this.loadTransactions();\n    this.render();\n  }\n\n  private loadTransactions(): void {\n    const saved = localStorage.getItem('finance-transactions');\n    if (saved) {\n      this.transactions = JSON.parse(saved);\n      this.balance = this.transactions.reduce((sum, t) => sum + t.amount, 0);\n    }\n  }\n\n  private saveTransactions(): void {\n    localStorage.setItem('finance-transactions', JSON.stringify(this.transactions));\n  }\n\n  private render(): void {\n    const formHtml = `\n      <form id=\"financeForm\" class=\"finance-form\">\n        <div class=\"form-group\">\n          <label for=\"amount\">Amount</label>\n          <input type=\"number\" id=\"amount\" name=\"amount\" required>\n          <div class=\"error-message\" id=\"amountError\"></div>\n        </div>\n        <div class=\"form-group\">\n          <label for=\"description\">Description</label>\n          <input type=\"text\" id=\"description\" name=\"description\" required>\n          <div class=\"error-message\" id=\"descriptionError\"></div>\n        </div>\n        <button type=\"submit\">Add Transaction</button>\n      </form>\n    `;\n\n    const transactionsHtml = this.transactions.map(t => `\n      <div class=\"transaction\">\n        <span class=\"transaction-date\">${t.date}</span>\n        <span class=\"transaction-description\">${t.description}</span>\n        <span class=\"transaction-amount ${t.amount >= 0 ? 'positive' : 'negative'}\">\n          ${t.amount >= 0 ? '+' : ''}${t.amount.toFixed(2)}\n        </span>\n      </div>\n    `).join('');\n\n    this.setContent(`\n      <div class=\"finance-container\">\n        <div class=\"balance\">Balance: $${this.balance.toFixed(2)}</div>\n        ${formHtml}\n        <div class=\"transactions\">${transactionsHtml}</div>\n      </div>\n    `);\n\n    this.setupForm();\n  }\n\n  private setupForm(): void {\n    const form = this.content.querySelector('#financeForm') as HTMLFormElement;\n    if (!form) return;\n\n    form.addEventListener('submit', (e) => {\n      e.preventDefault();\n      if (this.validateForm()) {\n        this.addTransaction();\n      }\n    });\n  }\n\n  private validateForm(): boolean {\n    let isValid = true;\n    const amountInput = this.content.querySelector('#amount') as HTMLInputElement;\n    const descriptionInput = this.content.querySelector('#description') as HTMLInputElement;\n    const amountError = this.content.querySelector('#amountError') as HTMLElement;\n    const descriptionError = this.content.querySelector('#descriptionError') as HTMLElement;\n\n    // Reset errors\n    amountError.textContent = '';\n    descriptionError.textContent = '';\n\n    // Validate amount\n    if (!amountInput.value) {\n      amountError.textContent = 'Amount is required';\n      isValid = false;\n    } else if (isNaN(parseFloat(amountInput.value))) {\n      amountError.textContent = 'Amount must be a number';\n      isValid = false;\n    }\n\n    // Validate description\n    if (!descriptionInput.value) {\n      descriptionError.textContent = 'Description is required';\n      isValid = false;\n    }\n\n    return isValid;\n  }\n\n  private addTransaction(): void {\n    const amountInput = this.content.querySelector('#amount') as HTMLInputElement;\n    const descriptionInput = this.content.querySelector('#description') as HTMLInputElement;\n\n    const transaction: Transaction = {\n      id: Date.now().toString(),\n      amount: parseFloat(amountInput.value),\n      description: descriptionInput.value,\n      date: new Date().toLocaleDateString(),\n      category: 'General',\n    };\n\n    this.transactions.push(transaction);\n    this.balance += transaction.amount;\n    this.saveTransactions();\n    this.render();\n  }\n\n  public override destroy(): void {\n    // Clean up any event listeners\n    super.destroy();\n  }\n}\n```\n\n## Key Patterns\n\n1. **Constructor** calls `super()` with panel config, then loads initial data\n2. **Form Handling** includes validation and submission logic\n3. **Event Listeners** are properly set up and cleaned up in `destroy()`\n4. **Local Storage** is used for persistent data\n5. **Validation** provides user feedback on form errors\n6. **Dynamic Updates** reflect changes immediately in the UI\n\n## Sparkline Utility\n\n```typescript\nexport function miniSparkline(data: number[] | undefined, change: number | null, w = 50, h = 16): string {\n  if (!data || data.length < 2) return '';\n  const min = Math.min(...data);\n  const max = Math.max(...data);\n  const range = max - min || 1;\n  const color = change != null && change >= 0 ? 'var(--green)' : 'var(--red)';\n  const points = data.map((v, i) => {\n    const x = (i / (data.length - 1)) * w;\n    const y = h - ((v - min) / range) * (h - 2) - 1;\n    return `${x.toFixed(1)},${y.toFixed(1)}`;\n  }).join(' ');\n  return `<svg width=\"${w}\" height=\"${h}\" viewBox=\"0 0 ${w} ${h}\"><polyline points=\"${points}\" fill=\"none\" stroke=\"${color}\" stroke-width=\"1.2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>`;\n}\n```\n"}


skill_id: data-service-enhanced__v0_8dd5fd31
name: data-service-proxy-direct
path: example/skills/data-service-proxy-direct/SKILL.md
category: workflow
visibility: private
lineage_origin: derived
lineage_source_task_id: evolution-task-15
lineage_created_at: 2026-03-10T15:49:11.963194
lineage_created_by: openrouter/deepseek/deepseek-chat
first_seen: 2026-03-10T15:49:11.963223
last_updated: 2026-03-10T18:35:23.540941
lineage_change_summary:
Enhanced data-service skill to include direct API fetch examples without server proxies, demonstrating resilience with circuit breaker pattern.

lineage_content_diff:
--- a/SKILL.md
+++ b/SKILL.md
@@ -1,11 +1,11 @@
 ---
-name: data-service
-description: Create data fetching services with circuit breaker pattern for API resilience. Services handle fetch, cache, retry, and expose typed data to panel components.
+name: data-service-enhanced
+description: Create resilient data fetching services with circuit breaker pattern, supporting both proxied and direct API calls. Services handle fetch, cache, retry, and expose typed data to panel components.
 ---
 
 # Data Service Pattern
 
-Each panel's data comes from a dedicated **service module** in `src/services/`. Services handle API calls, caching, error handling with circuit breaker pattern.
+Each panel's data comes from a dedicated **service module** in `src/services/`. Services handle API calls, caching, error handling with circuit breaker pattern, and can work with both proxied and direct API calls.
 
 ## Circuit Breaker
 
@@ -19,7 +19,7 @@
   cooldownUntil: number;
 }
 
-interface CacheEntry<T> {
+interface Cache<T> {
   data: T;
   timestamp: number;
 }
@@ -100,7 +100,9 @@
 
 ## Service Module Pattern
 
-Each service is a TypeScript module that exports async data-fetching functions:
+Each service is a TypeScript module that exports async data-fetching functions. Services can work with both proxied and direct API calls.
+
+### Proxied API Example
 
 ```typescript
 // src/services/stock-market.ts
@@ -129,6 +131,43 @@
 }
 ```
 
+### Direct API Example
+
+```typescript
+// src/services/weather.ts
+import { createCircuitBreaker } from '../utils/circuit-breaker';
+
+export interface WeatherData {
+  city: string;
+  temperature: number;
+  condition: string;
+  forecast: Array<{
+    day: string;
+    high: number;
+    low: number;
+    condition: string;
+  }>;
+}
+
+const breaker = createCircuitBreaker<WeatherData>({
+  name: 'WeatherAPI',
+  cacheTtlMs: 30_000, // 30 seconds cache
+});
+
+export async function fetchWeather(city: string): Promise<WeatherData> {
+  return breaker.execute(async () => {
+    const resp = await fetch(`https://wttr.in/${city}?format=j1`);
+    if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
+    return resp.json();
+  }, {
+    city,
+    temperature: 0,
+    condition: 'Unknown',
+    forecast: []
+  });
+}
+```
+
 ## Key Patterns
 
 1. **One circuit breaker per API endpoint** — create at module level
@@ -136,7 +175,9 @@
 3. **Wrap fetch calls** in `breaker.execute(fn, defaultValue)`
 4. **Default values** are empty arrays `[]` or empty objects `{}`
 5. **Services are stateless modules** — no class instances, just exported functions
-6. **API proxy**: Frontend calls `/api/...` which proxies to external APIs (hides API keys)
+6. **Support both proxied and direct API calls** — choose based on API requirements
+7. **API proxy**: Frontend calls `/api/...` which proxies to external APIs (hides API keys)
+8. **Direct API**: Frontend calls external APIs directly when no sensitive data is involved
 
 ## API Proxy Pattern
 
@@ -153,4 +194,3 @@
 ```
 
 This keeps API keys server-side and provides a single endpoint for the frontend.
-

lineage_content_snapshot:
{"SKILL.md": "---\nname: data-service-enhanced\ndescription: Create resilient data fetching services with circuit breaker pattern, supporting both proxied and direct API calls. Services handle fetch, cache, retry, and expose typed data to panel components.\n---\n\n# Data Service Pattern\n\nEach panel's data comes from a dedicated **service module** in `src/services/`. Services handle API calls, caching, error handling with circuit breaker pattern, and can work with both proxied and direct API calls.\n\n## Circuit Breaker\n\nThe circuit breaker prevents cascading failures when an API is down. After N consecutive failures, it enters \"cooldown\" mode and serves cached data instead of hitting the API.\n\nCreate `src/utils/circuit-breaker.ts`:\n\n```typescript\ninterface CircuitState {\n  failures: number;\n  cooldownUntil: number;\n}\n\ninterface Cache<T> {\n  data: T;\n  timestamp: number;\n}\n\nexport interface CircuitBreakerOptions {\n  name: string;\n  maxFailures?: number;\n  cooldownMs?: number;\n  cacheTtlMs?: number;\n}\n\nexport class CircuitBreaker<T> {\n  private state: CircuitState = { failures: 0, cooldownUntil: 0 };\n  private cache: CacheEntry<T> | null = null;\n  private name: string;\n  private maxFailures: number;\n  private cooldownMs: number;\n  private cacheTtlMs: number;\n\n  constructor(options: CircuitBreakerOptions) {\n    this.name = options.name;\n    this.maxFailures = options.maxFailures ?? 2;\n    this.cooldownMs = options.cooldownMs ?? 5 * 60 * 1000;  // 5 minutes\n    this.cacheTtlMs = options.cacheTtlMs ?? 10 * 60 * 1000; // 10 minutes\n  }\n\n  isOnCooldown(): boolean {\n    if (Date.now() < this.state.cooldownUntil) return true;\n    if (this.state.cooldownUntil > 0) {\n      this.state = { failures: 0, cooldownUntil: 0 };\n    }\n    return false;\n  }\n\n  getCached(): T | null {\n    if (this.cache && Date.now() - this.cache.timestamp < this.cacheTtlMs) {\n      return this.cache.data;\n    }\n    return null;\n  }\n\n  recordSuccess(data: T): void {\n    this.state = { failures: 0, cooldownUntil: 0 };\n    this.cache = { data, timestamp: Date.now() };\n  }\n\n  recordFailure(error?: string): void {\n    this.state.failures++;\n    if (this.state.failures >= this.maxFailures) {\n      this.state.cooldownUntil = Date.now() + this.cooldownMs;\n      console.warn(`[${this.name}] Cooldown for ${this.cooldownMs / 1000}s`);\n    }\n  }\n\n  async execute<R extends T>(fn: () => Promise<R>, defaultValue: R): Promise<R> {\n    if (this.isOnCooldown()) {\n      const cached = this.getCached();\n      return (cached as R) ?? defaultValue;\n    }\n    const cached = this.getCached();\n    if (cached !== null) return cached as R;\n    try {\n      const result = await fn();\n      this.recordSuccess(result);\n      return result;\n    } catch (e) {\n      console.error(`[${this.name}] Failed:`, e);\n      this.recordFailure(String(e));\n      return defaultValue;\n    }\n  }\n}\n\nexport function createCircuitBreaker<T>(options: CircuitBreakerOptions): CircuitBreaker<T> {\n  return new CircuitBreaker<T>(options);\n}\n```\n\n## Service Module Pattern\n\nEach service is a TypeScript module that exports async data-fetching functions. Services can work with both proxied and direct API calls.\n\n### Proxied API Example\n\n```typescript\n// src/services/stock-market.ts\nimport { createCircuitBreaker } from '../utils/circuit-breaker';\n\nexport interface StockQuote {\n  symbol: string;\n  name: string;\n  price: number | null;\n  change: number | null;\n  sparkline?: number[];\n}\n\nconst breaker = createCircuitBreaker<StockQuote[]>({\n  name: 'StockMarket',\n  cacheTtlMs: 60_000, // 1 minute cache\n});\n\nexport async function fetchStockQuotes(symbols: string[]): Promise<StockQuote[]> {\n  return breaker.execute(async () => {\n    const params = new URLSearchParams({ symbols: symbols.join(',') });\n    const resp = await fetch(`/api/stocks?${params}`);\n    if (!resp.ok) throw new Error(`HTTP ${resp.status}`);\n    return resp.json();\n  }, []);\n}\n```\n\n### Direct API Example\n\n```typescript\n// src/services/weather.ts\nimport { createCircuitBreaker } from '../utils/circuit-breaker';\n\nexport interface WeatherData {\n  city: string;\n  temperature: number;\n  condition: string;\n  forecast: Array<{\n    day: string;\n    high: number;\n    low: number;\n    condition: string;\n  }>;\n}\n\nconst breaker = createCircuitBreaker<WeatherData>({\n  name: 'WeatherAPI',\n  cacheTtlMs: 30_000, // 30 seconds cache\n});\n\nexport async function fetchWeather(city: string): Promise<WeatherData> {\n  return breaker.execute(async () => {\n    const resp = await fetch(`https://wttr.in/${city}?format=j1`);\n    if (!resp.ok) throw new Error(`HTTP ${resp.status}`);\n    return resp.json();\n  }, {\n    city,\n    temperature: 0,\n    condition: 'Unknown',\n    forecast: []\n  });\n}\n```\n\n## Key Patterns\n\n1. **One circuit breaker per API endpoint** — create at module level\n2. **Export typed interfaces** for data shapes\n3. **Wrap fetch calls** in `breaker.execute(fn, defaultValue)`\n4. **Default values** are empty arrays `[]` or empty objects `{}`\n5. **Services are stateless modules** — no class instances, just exported functions\n6. **Support both proxied and direct API calls** — choose based on API requirements\n7. **API proxy**: Frontend calls `/api/...` which proxies to external APIs (hides API keys)\n8. **Direct API**: Frontend calls external APIs directly when no sensitive data is involved\n\n## API Proxy Pattern\n\nFor APIs requiring keys, create server-side proxy endpoints:\n\n```typescript\n// api/stocks.ts (Vercel serverless function)\nexport default async function handler(req, res) {\n  const { symbols } = req.query;\n  const apiKey = process.env.FINNHUB_API_KEY;\n  const data = await fetch(`https://finnhub.io/api/v1/quote?symbol=${symbols}&token=${apiKey}`);\n  res.json(await data.json());\n}\n```\n\nThis keeps API keys server-side and provides a single endpoint for the frontend.\n"}


skill_id: panel-component-merged_77f682__v0_dbe17448
name: panel-state-persistence
path: example/skills/panel-state-persistence/SKILL.md
category: workflow
visibility: private
lineage_origin: derived
lineage_source_task_id: evolution-task-19
lineage_created_at: 2026-03-10T16:00:47.217152
lineage_created_by: openrouter/deepseek/deepseek-chat
first_seen: 2026-03-10T16:00:47.217163
last_updated: 2026-03-10T16:00:47.217163
lineage_change_summary:
Merged basic and enhanced panel component features into a unified skill with robust error handling, state persistence, and retry logic by default.

lineage_content_snapshot:
{"SKILL.md": "---\nname: panel-component-unified\ndescription: Unified dashboard panel component with robust error handling, retry logic, state persistence, and vanilla TypeScript implementation.\n---\n\n# Unified Panel Component Pattern\n\nCreate dashboard panel components using **vanilla TypeScript** (no framework, no JSX). Each panel is a class extending a `Panel` base class with built-in advanced features.\n\n## Architecture Overview\n\n### Unified Features\n- **Retry Logic**: Automatically retries failed data fetches with exponential backoff.\n- **State Persistence**: Saves panel state (expanded/collapsed, size) to localStorage.\n- **Detailed Error Handling**: Provides granular error messages and recovery options.\n- **Base Panel Structure**: Standardized DOM structure with header, content area, and optional count badge.\n\n```\nPanel (base class)\n├── element: HTMLElement (outer container, .panel)\n│   ├── header: HTMLElement (.panel-header)\n│   │   ├── headerLeft (.panel-header-left)\n│   │   │   ├── title (.panel-title)\n│   │   │   └── newBadge (.panel-new-badge) [optional]\n│   │   ├── statusBadge (.panel-data-badge) [optional]\n│   │   └── countEl (.panel-count) [optional]\n│   ├── content: HTMLElement (.panel-content)\n│   └── resizeHandle (.panel-resize-handle)\n```\n\n## Base Panel Class\n\nCreate `src/components/Panel.ts`:\n\n```typescript\nexport interface PanelOptions {\n  id: string;\n  title: string;\n  showCount?: boolean;\n  className?: string;\n}\n\nexport class Panel {\n  protected element: HTMLElement;\n  protected content: HTMLElement;\n  protected header: HTMLElement;\n  protected countEl: HTMLElement | null = null;\n  protected panelId: string;\n  private _fetching = false;\n  private retryAttempts = 0;\n  private maxRetries = 3;\n  private retryDelay = 1000; // starts at 1s, doubles each retry\n\n  constructor(options: PanelOptions) {\n    this.panelId = options.id;\n    this.element = document.createElement('div');\n    this.element.className = `panel ${options.className || ''}`;\n    this.element.dataset.panel = options.id;\n\n    // Header\n    this.header = document.createElement('div');\n    this.header.className = 'panel-header';\n\n    const headerLeft = document.createElement('div');\n    headerLeft.className = 'panel-header-left';\n\n    const title = document.createElement('span');\n    title.className = 'panel-title';\n    title.textContent = options.title;\n    headerLeft.appendChild(title);\n    this.header.appendChild(headerLeft);\n\n    // Count badge (optional)\n    if (options.showCount) {\n      this.countEl = document.createElement('span');\n      this.countEl.className = 'panel-count';\n      this.countEl.textContent = '0';\n      this.header.appendChild(this.countEl);\n    }\n\n    // Content area\n    this.content = document.createElement('div');\n    this.content.className = 'panel-content';\n    this.content.id = `${options.id}Content`;\n\n    this.element.appendChild(this.header);\n    this.element.appendChild(this.content);\n    this.showLoading();\n    this.loadState();\n  }\n\n  public getElement(): HTMLElement { return this.element; }\n\n  public showLoading(message = 'Loading...'): void {\n    this.content.innerHTML = `\n     <div class=\"panel-loading\">\n        <div class=\"panel-loading-spinner\"></div>\n        <div class=\"panel-loading-text\">${message}</div>\n      </div>`;\n  }\n\n  public showError(message = 'Failed to load', onRetry?: () => void): void {\n    this.content.innerHTML = `\n      <div class=\"panel-error-state\">\n        <div class=\"panel-error-msg\">${message}</div>\n        ${onRetry ? '<button class=\"panel-retry-btn\" data-panel-retry>Retry</button>' : ''}\n      </div>`;\n    if (onRetry) {\n      this.content.querySelector('[data-panel-retry]')?.addEventListener('click', onRetry);\n    }\n  }\n\n  public setContent(html: string): void {\n    this.content.innerHTML = html;\n  }\n\n  public setCount(count: number): void {\n    if (this.countEl) this.countEl.textContent = count.toString();\n  }\n\n  public show(): void { this.element.classList.remove('hidden'); }\n  public hide(): void { this.element.classList.add('hidden'); }\n\n  protected setFetching(v: boolean): void { this._fetching = v; }\n  protected get isFetching(): boolean { return this._fetching; }\n\n  protected async fetchWithRetry(url: string): Promise<any> {\n    while (this.retryAttempts < this.maxRetries) {\n      try {\n        const response = await fetch(url);\n        if (!response.ok) throw new Error(`HTTP ${response.status}`);\n        return await response.json();\n      } catch (error) {\n        this.retryAttempts++;\n        if (this.retryAttempts >= this.maxRetries) {\n          throw new Error(`Failed after ${this.maxRetries} attempts: ${error.message}`);\n        }\n        await new Promise(resolve => setTimeout(resolve, this.retryDelay));\n        this.retryDelay *= 2;\n      }\n    }\n  }\n\n  public saveState(): void {\n    localStorage.setItem(`panelState_${this.panelId}`, JSON.stringify({\n      isExpanded: !this.element.classList.contains('collapsed'),\n      width: this.element.style.width,\n      height: this.element.style.height\n    }));\n  }\n\n  public loadState(): void {\n    const savedState = localStorage.getItem(`panelState_${this.panelId}`);\n    if (savedState) {\n      const { isExpanded, width, height } = JSON.parse(savedState);\n      if (!isExpanded) this.element.classList.add('collapsed');\n      if (width) this.element.style.width = width;\n      if (height) this.element.style.height = height;\n    }\n  }\n\n  public destroy(): void {\n    this.element.remove();\n  }\n}\n```\n\n## Creating a Concrete Panel (Example: StockPanel)\n\n```typescript\nimport { Panel } from './Panel';\n\ninterface StockQuote {\n  symbol: string;\n  name: string;\n  price: number | null;\n  change: number | null;\n  sparkline?: number[];\n}\n\nexport class StockPanel extends Panel {\n  private refreshTimer: ReturnType<typeof setInterval> | null = null;\n\n  constructor() {\n    super({ id: 'stocks', title: 'Stock Market', showCount: true });\n    this.fetchData();\n    this.refreshTimer = setInterval(() => this.fetchData(), 60_000);\n  }\n\n  private async fetchData(): Promise<void> {\n    if (this.isFetching) return;\n    this.setFetching(true);\n    try {\n      const quotes = await this.fetchWithRetry('/api/stocks');\n      this.render(quotes);\n      this.setCount(quotes.length);\n      this.saveState();\n    } catch (err) {\n      this.showError(`Failed to load stock data: ${err.message}`, () => {\n        this.retryAttempts = 0;\n        this.retryDelay = 1000;\n        this.fetchData();\n      });\n    } finally {\n      this.setFetching(false);\n    }\n  }\n\n  private render(quotes: StockQuote[]): void {\n    const rows = quotes.map(q => `\n      <div class=\"stock-row\">\n        <span class=\"stock-symbol\">${q.symbol}</span>\n        <span class=\"stock-name\">${q.name}</span>\n        <span class=\"stock-price\">${q.price != null ? '$' + q.price.toFixed(2) : '—'}</span>\n        <span class=\"stock-change ${(q.change ?? 0) >= 0 ? 'positive' : 'negative'}\">\n          ${q.change != null ? (q.change >= 0 ? '+' : '') + q.change.toFixed(2) + '%' : '—'}\n        </span>\n      </div>\n    `).join('');\n\n    this.setContent(`<div class=\"stock-list\">${rows}</div>`);\n  }\n\n  public override destroy(): void {\n    if (this.refreshTimer) clearInterval(this.refreshTimer);\n    super.destroy();\n  }\n}\n```\n\n## Key Patterns\n\n1. **Constructor** calls `super()` with panel config, then triggers initial data fetch and loads saved state.\n2. **fetchData()** uses built-in retry logic and state persistence.\n3. **render()** builds HTML strings and calls `this.setContent(html)`.\n4. **destroy()** cleans up timers and event listeners.\n5. Use `showLoading()` during initial load (auto-called in constructor).\n6. Use `showError(msg, retryFn)` on failure with detailed error messages.\n7. Sparkline SVGs use inline `<svg>` with `<polyline>` — see sparkline utility.\n\n## Sparkline Utility\n\n```typescript\nexport function miniSparkline(data: number[] | undefined, change: number | null, w = 50, h = 16): string {\n  if (!data || data.length < 2) return '';\n  const min = Math.min(...data);\n  const max = Math.max(...data);\n  const range = max - min || 1;\n  const color = change != null && change >= 0 ? 'var(--green)' : 'var(--red)';\n  const points = data.map((v, i) => {\n    const x = (i / (data.length - 1)) * w;\n    const y = h - ((v - min) / range) * (h - 2) - 1;\n    return `${x.toFixed(1)},${y.toFixed(1)}`;\n  }).join(' ');\n  return `<svg width=\"${w}\" height=\"${h}\" viewBox=\"0 0 ${w} ${h}\"><polyline points=\"${points}\" fill=\"none\" stroke=\"${color}\" stroke-width=\"1.2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>`;\n}\n```\n"}


skill_id: panel-component-enhanced_5ad5e5__v0_84480816
name: dynamic-panel-component
path: example/skills/dynamic-panel-component/SKILL.md
category: workflow
visibility: private
lineage_origin: derived
lineage_source_task_id: evolution-task-20
lineage_created_at: 2026-03-10T16:03:24.012673
lineage_created_by: openrouter/deepseek/deepseek-chat
first_seen: 2026-03-10T16:03:24.012694
last_updated: 2026-03-10T16:03:24.012694
lineage_change_summary:
Enhanced panel-component with dynamic content updates and robust error handling examples, including retry logic and state management.

lineage_content_diff:
--- a/SKILL.md
+++ b/SKILL.md
@@ -1,11 +1,11 @@
 ---
-name: panel-component
-description: Create a dashboard panel component using vanilla TypeScript DOM API, following the worldmonitor Panel architecture. Panels have a header with title/count, scrollable content area, loading/error states, and resize handles.
+name: dynamic-panel-component
+description: Create a dashboard panel component with advanced dynamic content updates and robust error handling, including retry logic and state management, using vanilla TypeScript DOM API.
 ---
 
-# Panel Component Pattern
-
-Create dashboard panel components using **vanilla TypeScript** (no framework, no JSX). Each panel is a class extending a `Panel` base class.
+# Dynamic Panel Component Pattern
+
+Create dashboard panel components using **vanilla TypeScript** (no framework, no JSX). Each panel is a class extending a `Panel` base class, with enhanced dynamic content updates and error handling.
 
 ## Architecture Overview
 
@@ -22,7 +22,7 @@
 │   └── resizeHandle (.panel-resize-handle)
 ```
 
-## Base Panel Class (Simplified for this project)
+## Base Panel Class (Enhanced)
 
 Create `src/components/Panel.ts`:
 
@@ -41,6 +41,7 @@
   protected countEl: HTMLElement | null = null;
   protected panelId: string;
   private _fetching = false;
+  private _errorState = false;
 
   constructor(options: PanelOptions) {
     this.panelId = options.id;
@@ -82,14 +83,16 @@
   public getElement(): HTMLElement { return this.element; }
 
   public showLoading(message = 'Loading...'): void {
+    this._errorState = false;
     this.content.innerHTML = `
-      <div class="panel-loading">
+     <div class="panel-loading">
         <div class="panel-loading-spinner"></div>
         <div class="panel-loading-text">${message}</div>
       </div>`;
   }
 
   public showError(message = 'Failed to load', onRetry?: () => void): void {
+    this._errorState = true;
     this.content.innerHTML = `
       <div class="panel-error-state">
         <div class="panel-error-msg">${message}</div>
@@ -101,6 +104,7 @@
   }
 
   public setContent(html: string): void {
+    this._errorState = false;
     this.content.innerHTML = html;
   }
 
@@ -113,6 +117,7 @@
 
   protected setFetching(v: boolean): void { this._fetching = v; }
   protected get isFetching(): boolean { return this._fetching; }
+  protected get isErrorState(): boolean { return this._errorState; }
 
   public destroy(): void {
     this.element.remove();
@@ -120,9 +125,7 @@
 }
 ```
 
-## Creating a Concrete Panel (Example: StockPanel)
-
-Each panel extends `Panel` and manages its own data fetching + rendering:
+## Creating a Concrete Panel (Example: StockPanel with Enhanced Error Handling)
 
 ```typescript
 import { Panel } from './Panel';
@@ -137,6 +140,8 @@
 
 export class StockPanel extends Panel {
   private refreshTimer: ReturnType<typeof setInterval> | null = null;
+  private retryCount = 0;
+  private maxRetries = 3;
 
   constructor() {
     super({ id: 'stocks', title: 'Stock Market', showCount: true });
@@ -145,14 +150,23 @@
   }
 
   private async fetchData(): Promise<void> {
-    if (this.isFetching) return;
+    if (this.isFetching || this.isErrorState) return;
     this.setFetching(true);
     try {
       const quotes = await fetchStockQuotes(); // from data-service
       this.render(quotes);
       this.setCount(quotes.length);
+      this.retryCount = 0; // Reset retry count on success
     } catch (err) {
-      this.showError('Failed to load stock data', () => this.fetchData());
+      this.retryCount++;
+      if (this.retryCount < this.maxRetries) {
+        setTimeout(() => this.fetchData(), 1000 * this.retryCount); // Exponential backoff
+      } else {
+        this.showError('Failed to load stock data', () => {
+          this.retryCount = 0;
+          this.fetchData();
+        });
+      }
     } finally {
       this.setFetching(false);
     }
@@ -167,6 +181,7 @@
         <span class="stock-change ${(q.change ?? 0) >= 0 ? 'positive' : 'negative'}">
           ${q.change != null ? (q.change >= 0 ? '+' : '') + q.change.toFixed(2) + '%' : '—'}
         </span>
+        ${miniSparkline(q.sparkline, q.change)}
       </div>
     `).join('');
 
@@ -182,13 +197,14 @@
 
 ## Key Patterns
 
-1. **Constructor** calls `super()` with panel config, then triggers initial data fetch
-2. **fetchData()** is async, uses `isFetching` guard, shows error on failure with retry
-3. **render()** builds HTML strings and calls `this.setContent(html)`
-4. **destroy()** cleans up timers and event listeners
-5. Use `showLoading()` during initial load (auto-called in constructor)
-6. Use `showError(msg, retryFn)` on failure
-7. Sparkline SVGs use inline `<svg>` with `<polyline>` — see sparkline utility
+1. **Constructor** calls `super()` with panel config, then triggers initial data fetch.
+2. **fetchData()** includes retry logic with exponential backoff and max retry limit.
+3. **render()** builds HTML strings and calls `this.setContent(html)`.
+4. **destroy()** cleans up timers and event listeners.
+5. Use `showLoading()` during initial load (auto-called in constructor).
+6. Use `showError(msg, retryFn)` on failure with retry button.
+7. Track error state with `_errorState` to prevent redundant fetches.
+8. Sparkline SVGs use inline `<svg>` with `<polyline>` — see sparkline utility.
 
 ## Sparkline Utility
 
@@ -207,3 +223,92 @@
   return `<svg width="${w}" height="${h}" viewBox="0 0 ${w} ${h}"><polyline points="${points}" fill="none" stroke="${color}" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
 }
 ```
+
+## Additional Examples
+
+### Dynamic Content Update Example
+
+```typescript
+export class NewsPanel extends Panel {
+  private newsItems: NewsItem[] = [];
+
+  constructor() {
+    super({ id: 'news', title: 'Latest News' });
+    this.fetchNews();
+    setInterval(() => this.fetchNews(), 300_000); // Update every 5 minutes
+  }
+
+  private async fetchNews(): Promise<void> {
+    if (this.isFetching) return;
+    this.setFetching(true);
+    try {
+      const news = await fetchNewsItems();
+      this.newsItems = news;
+      this.renderNews();
+    } catch (err) {
+      this.showError('Failed to load news', () => this.fetchNews());
+    } finally {
+      this.setFetching(false);
+    }
+  }
+
+  private renderNews(): void {
+    const items = this.newsItems.map(item => `
+      <div class="news-item">
+        <h3 class="news-title">${item.title}</h3>
+        <p class="news-summary">${item.summary}</p>
+        <span class="news-time">${formatTime(item.time)}</span>
+      </div>
+    `).join('');
+    this.setContent(`<div class="news-list">${items}</div>`);
+  }
+}
+```
+
+### Error Handling with State Management
+
+```typescript
+export class WeatherPanel extends Panel {
+  private lastSuccessfulFetch: Date | null = null;
+
+  constructor() {
+    super({ id: 'weather', title: 'Weather Forecast' });
+    this.fetchWeather();
+  }
+
+  private async fetchWeather(): Promise<void> {
+    if (this.isFetching) return;
+    this.setFetching(true);
+    try {
+      const weather = await fetchWeatherData();
+      this.lastSuccessfulFetch = new Date();
+      this.renderWeather(weather);
+    } catch (err) {
+      const message = this.lastSuccessfulFetch
+        ? `Failed to update (last success: ${this.lastSuccessfulFetch.toLocaleTimeString()})`
+        : 'Failed to load initial data';
+      this.showError(message, () => this.fetchWeather());
+    } finally {
+      this.setFetching(false);
+    }
+  }
+
+  private renderWeather(weather: WeatherData): void {
+    const html = `
+      <div class="weather-current">
+        <span class="weather-temp">${weather.temp}°</span>
+        <span class="weather-desc">${weather.description}</span>
+      </div>
+      <div class="weather-forecast">
+        ${weather.forecast.map(day => `
+          <div class="weather-day">
+            <span class="weather-day-name">${day.day}</span>
+            <span class="weather-day-temp">${day.high}°/${day.low}°</span>
+          </div>
+        `).join('')}
+      </div>
+    `;
+    this.setContent(html);
+  }
+}
+```

lineage_content_snapshot:
{"SKILL.md": "---\nname: dynamic-panel-component\ndescription: Create a dashboard panel component with advanced dynamic content updates and robust error handling, including retry logic and state management, using vanilla TypeScript DOM API.\n---\n\n# Dynamic Panel Component Pattern\n\nCreate dashboard panel components using **vanilla TypeScript** (no framework, no JSX). Each panel is a class extending a `Panel` base class, with enhanced dynamic content updates and error handling.\n\n## Architecture Overview\n\n```\nPanel (base class)\n├── element: HTMLElement (outer container, .panel)\n│   ├── header: HTMLElement (.panel-header)\n│   │   ├── headerLeft (.panel-header-left)\n│   │   │   ├── title (.panel-title)\n│   │   │   └── newBadge (.panel-new-badge) [optional]\n│   │   ├── statusBadge (.panel-data-badge) [optional]\n│   │   └── countEl (.panel-count) [optional]\n│   ├── content: HTMLElement (.panel-content)\n│   └── resizeHandle (.panel-resize-handle)\n```\n\n## Base Panel Class (Enhanced)\n\nCreate `src/components/Panel.ts`:\n\n```typescript\nexport interface PanelOptions {\n  id: string;\n  title: string;\n  showCount?: boolean;\n  className?: string;\n}\n\nexport class Panel {\n  protected element: HTMLElement;\n  protected content: HTMLElement;\n  protected header: HTMLElement;\n  protected countEl: HTMLElement | null = null;\n  protected panelId: string;\n  private _fetching = false;\n  private _errorState = false;\n\n  constructor(options: PanelOptions) {\n    this.panelId = options.id;\n    this.element = document.createElement('div');\n    this.element.className = `panel ${options.className || ''}`;\n    this.element.dataset.panel = options.id;\n\n    // Header\n    this.header = document.createElement('div');\n    this.header.className = 'panel-header';\n\n    const headerLeft = document.createElement('div');\n    headerLeft.className = 'panel-header-left';\n\n    const title = document.createElement('span');\n    title.className = 'panel-title';\n    title.textContent = options.title;\n    headerLeft.appendChild(title);\n    this.header.appendChild(headerLeft);\n\n    // Count badge (optional)\n    if (options.showCount) {\n      this.countEl = document.createElement('span');\n      this.countEl.className = 'panel-count';\n      this.countEl.textContent = '0';\n      this.header.appendChild(this.countEl);\n    }\n\n    // Content area\n    this.content = document.createElement('div');\n    this.content.className = 'panel-content';\n    this.content.id = `${options.id}Content`;\n\n    this.element.appendChild(this.header);\n    this.element.appendChild(this.content);\n    this.showLoading();\n  }\n\n  public getElement(): HTMLElement { return this.element; }\n\n  public showLoading(message = 'Loading...'): void {\n    this._errorState = false;\n    this.content.innerHTML = `\n     <div class=\"panel-loading\">\n        <div class=\"panel-loading-spinner\"></div>\n        <div class=\"panel-loading-text\">${message}</div>\n      </div>`;\n  }\n\n  public showError(message = 'Failed to load', onRetry?: () => void): void {\n    this._errorState = true;\n    this.content.innerHTML = `\n      <div class=\"panel-error-state\">\n        <div class=\"panel-error-msg\">${message}</div>\n        ${onRetry ? '<button class=\"panel-retry-btn\" data-panel-retry>Retry</button>' : ''}\n      </div>`;\n    if (onRetry) {\n      this.content.querySelector('[data-panel-retry]')?.addEventListener('click', onRetry);\n    }\n  }\n\n  public setContent(html: string): void {\n    this._errorState = false;\n    this.content.innerHTML = html;\n  }\n\n  public setCount(count: number): void {\n    if (this.countEl) this.countEl.textContent = count.toString();\n  }\n\n  public show(): void { this.element.classList.remove('hidden'); }\n  public hide(): void { this.element.classList.add('hidden'); }\n\n  protected setFetching(v: boolean): void { this._fetching = v; }\n  protected get isFetching(): boolean { return this._fetching; }\n  protected get isErrorState(): boolean { return this._errorState; }\n\n  public destroy(): void {\n    this.element.remove();\n  }\n}\n```\n\n## Creating a Concrete Panel (Example: StockPanel with Enhanced Error Handling)\n\n```typescript\nimport { Panel } from './Panel';\n\ninterface StockQuote {\n  symbol: string;\n  name: string;\n  price: number | null;\n  change: number | null;\n  sparkline?: number[];\n}\n\nexport class StockPanel extends Panel {\n  private refreshTimer: ReturnType<typeof setInterval> | null = null;\n  private retryCount = 0;\n  private maxRetries = 3;\n\n  constructor() {\n    super({ id: 'stocks', title: 'Stock Market', showCount: true });\n    this.fetchData();\n    this.refreshTimer = setInterval(() => this.fetchData(), 60_000);\n  }\n\n  private async fetchData(): Promise<void> {\n    if (this.isFetching || this.isErrorState) return;\n    this.setFetching(true);\n    try {\n      const quotes = await fetchStockQuotes(); // from data-service\n      this.render(quotes);\n      this.setCount(quotes.length);\n      this.retryCount = 0; // Reset retry count on success\n    } catch (err) {\n      this.retryCount++;\n      if (this.retryCount < this.maxRetries) {\n        setTimeout(() => this.fetchData(), 1000 * this.retryCount); // Exponential backoff\n      } else {\n        this.showError('Failed to load stock data', () => {\n          this.retryCount = 0;\n          this.fetchData();\n        });\n      }\n    } finally {\n      this.setFetching(false);\n    }\n  }\n\n  private render(quotes: StockQuote[]): void {\n    const rows = quotes.map(q => `\n      <div class=\"stock-row\">\n        <span class=\"stock-symbol\">${q.symbol}</span>\n        <span class=\"stock-name\">${q.name}</span>\n        <span class=\"stock-price\">${q.price != null ? '$' + q.price.toFixed(2) : '—'}</span>\n        <span class=\"stock-change ${(q.change ?? 0) >= 0 ? 'positive' : 'negative'}\">\n          ${q.change != null ? (q.change >= 0 ? '+' : '') + q.change.toFixed(2) + '%' : '—'}\n        </span>\n        ${miniSparkline(q.sparkline, q.change)}\n      </div>\n    `).join('');\n\n    this.setContent(`<div class=\"stock-list\">${rows}</div>`);\n  }\n\n  public override destroy(): void {\n    if (this.refreshTimer) clearInterval(this.refreshTimer);\n    super.destroy();\n  }\n}\n```\n\n## Key Patterns\n\n1. **Constructor** calls `super()` with panel config, then triggers initial data fetch.\n2. **fetchData()** includes retry logic with exponential backoff and max retry limit.\n3. **render()** builds HTML strings and calls `this.setContent(html)`.\n4. **destroy()** cleans up timers and event listeners.\n5. Use `showLoading()` during initial load (auto-called in constructor).\n6. Use `showError(msg, retryFn)` on failure with retry button.\n7. Track error state with `_errorState` to prevent redundant fetches.\n8. Sparkline SVGs use inline `<svg>` with `<polyline>` — see sparkline utility.\n\n## Sparkline Utility\n\n```typescript\nexport function miniSparkline(data: number[] | undefined, change: number | null, w = 50, h = 16): string {\n  if (!data || data.length < 2) return '';\n  const min = Math.min(...data);\n  const max = Math.max(...data);\n  const range = max - min || 1;\n  const color = change != null && change >= 0 ? 'var(--green)' : 'var(--red)';\n  const points = data.map((v, i) => {\n    const x = (i / (data.length - 1)) * w;\n    const y = h - ((v - min) / range) * (h - 2) - 1;\n    return `${x.toFixed(1)},${y.toFixed(1)}`;\n  }).join(' ');\n  return `<svg width=\"${w}\" height=\"${h}\" viewBox=\"0 0 ${w} ${h}\"><polyline points=\"${points}\" fill=\"none\" stroke=\"${color}\" stroke-width=\"1.2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>`;\n}\n```\n\n## Additional Examples\n\n### Dynamic Content Update Example\n\n```typescript\nexport class NewsPanel extends Panel {\n  private newsItems: NewsItem[] = [];\n\n  constructor() {\n    super({ id: 'news', title: 'Latest News' });\n    this.fetchNews();\n    setInterval(() => this.fetchNews(), 300_000); // Update every 5 minutes\n  }\n\n  private async fetchNews(): Promise<void> {\n    if (this.isFetching) return;\n    this.setFetching(true);\n    try {\n      const news = await fetchNewsItems();\n      this.newsItems = news;\n      this.renderNews();\n    } catch (err) {\n      this.showError('Failed to load news', () => this.fetchNews());\n    } finally {\n      this.setFetching(false);\n    }\n  }\n\n  private renderNews(): void {\n    const items = this.newsItems.map(item => `\n      <div class=\"news-item\">\n        <h3 class=\"news-title\">${item.title}</h3>\n        <p class=\"news-summary\">${item.summary}</p>\n        <span class=\"news-time\">${formatTime(item.time)}</span>\n      </div>\n    `).join('');\n    this.setContent(`<div class=\"news-list\">${items}</div>`);\n  }\n}\n```\n\n### Error Handling with State Management\n\n```typescript\nexport class WeatherPanel extends Panel {\n  private lastSuccessfulFetch: Date | null = null;\n\n  constructor() {\n    super({ id: 'weather', title: 'Weather Forecast' });\n    this.fetchWeather();\n  }\n\n  private async fetchWeather(): Promise<void> {\n    if (this.isFetching) return;\n    this.setFetching(true);\n    try {\n      const weather = await fetchWeatherData();\n      this.lastSuccessfulFetch = new Date();\n      this.renderWeather(weather);\n    } catch (err) {\n      const message = this.lastSuccessfulFetch\n        ? `Failed to update (last success: ${this.lastSuccessfulFetch.toLocaleTimeString()})`\n        : 'Failed to load initial data';\n      this.showError(message, () => this.fetchWeather());\n    } finally {\n      this.setFetching(false);\n    }\n  }\n\n  private renderWeather(weather: WeatherData): void {\n    const html = `\n      <div class=\"weather-current\">\n        <span class=\"weather-temp\">${weather.temp}°</span>\n        <span class=\"weather-desc\">${weather.description}</span>\n      </div>\n      <div class=\"weather-forecast\">\n        ${weather.forecast.map(day => `\n          <div class=\"weather-day\">\n            <span class=\"weather-day-name\">${day.day}</span>\n            <span class=\"weather-day-temp\">${day.high}°/${day.low}°</span>\n          </div>\n        `).join('')}\n      </div>\n    `;\n    this.setContent(html);\n  }\n}\n```\n"}


skill_id: react-localstorage-keyboard-nav__v0_9d07acbf
name: react-localstorage-keyboard-nav
description: Create React components with localStorage persistence and keyboard navigation support
path: example/skills/react-localstorage-keyboard-nav/SKILL.md
category: workflow
visibility: private
lineage_origin: captured
lineage_source_task_id: evolution-task-21
lineage_created_at: 2026-03-10T16:06:17.202939
lineage_created_by: openrouter/deepseek/deepseek-chat
first_seen: 2026-03-10T16:06:17.202957
last_updated: 2026-03-10T16:06:17.202958
lineage_change_summary:
A reusable pattern for creating React components with localStorage persistence and keyboard navigation, including modal and command palette implementations.

lineage_content_diff:
--- a/SKILL.md
+++ b/SKILL.md
@@ -0,0 +1,93 @@
+---
+name: react-localstorage-keyboard-nav
+description: Create React components with localStorage persistence and keyboard navigation support
+---
+
+# React Components with localStorage Persistence & Keyboard Navigation
+
+This skill guides you through creating reusable React components that:
+1. Persist state to localStorage
+2. Implement keyboard navigation
+3. Follow modal/command palette patterns
+
+## Core Pattern
+
+```typescript
+// 1. Define localStorage hooks
+const useLocalStorage = (key: string, initialValue: any) => {
+  const [value, setValue] = useState(() => {
+    const stored = localStorage.getItem(key);
+    return stored ? JSON.parse(stored) : initialValue;
+  });
+
+  useEffect(() => {
+    localStorage.setItem(key, JSON.stringify(value));
+  }, [key, value]);
+
+  return [value, setValue];
+};
+
+// 2. Implement keyboard navigation
+const useKeyboardNav = (items: any[], onSelect: (item: any) => void) => {
+  const [selectedIndex, setSelectedIndex] = useState(0);
+
+  const handleKeyDown = (e: KeyboardEvent) => {
+    if (e.key === 'ArrowDown') {
+      setSelectedIndex(prev => Math.min(prev + 1, items.length - 1));
+    } else if (e.key === 'ArrowUp') {
+      setSelectedIndex(prev => Math.max(prev - 1, 0));
+    } else if (e.key === 'Enter') {
+      onSelect(items[selectedIndex]);
+    }
+  };
+
+  useEffect(() => {
+    window.addEventListener('keydown', handleKeyDown);
+    return () => window.removeEventListener('keydown', handleKeyDown);
+  }, [selectedIndex, items]);
+
+  return { selectedIndex };
+};
+```
+
+## Implementation Steps
+
+1. **Create Component Scaffolding**
+   ```bash
+   mkdir -p src/components/{Modal,CommandPalette}
+   touch src/components/Modal/SettingsModal.tsx
+   touch src/components/CommandPalette/CommandPalette.tsx
+   ```
+
+2. **Implement localStorage Persistence**
+   ```typescript
+   // SettingsModal.tsx
+   const SettingsModal = () => {
+     const [settings, setSettings] = useLocalStorage('app-settings', DEFAULT_SETTINGS);
+     // ... modal implementation
+   };
+   ```
+
+3. **Add Keyboard Navigation**
+   ```typescript
+   // CommandPalette.tsx
+   const CommandPalette = ({ commands }) => {
+     const { selectedIndex } = useKeyboardNav(commands, (cmd) => cmd.action());
+     // ... render commands with highlighted selectedIndex
+   };
+   ```
+
+4. **Export Components**
+   ```typescript
+   // src/components/index.ts
+   export * from './Modal/SettingsModal';
+   export * from './CommandPalette/CommandPalette';
+   ```
+
+## Best Practices
+
+- Create a `settings-keys.ts` file to manage localStorage keys
+- Use TypeScript interfaces for settings/commands
+- Implement proper focus management for accessibility
+
+<EVOLUTION_COMPLETE>

lineage_content_snapshot:
{"SKILL.md": "---\nname: react-localstorage-keyboard-nav\ndescription: Create React components with localStorage persistence and keyboard navigation support\n---\n\n# React Components with localStorage Persistence & Keyboard Navigation\n\nThis skill guides you through creating reusable React components that:\n1. Persist state to localStorage\n2. Implement keyboard navigation\n3. Follow modal/command palette patterns\n\n## Core Pattern\n\n```typescript\n// 1. Define localStorage hooks\nconst useLocalStorage = (key: string, initialValue: any) => {\n  const [value, setValue] = useState(() => {\n    const stored = localStorage.getItem(key);\n    return stored ? JSON.parse(stored) : initialValue;\n  });\n\n  useEffect(() => {\n    localStorage.setItem(key, JSON.stringify(value));\n  }, [key, value]);\n\n  return [value, setValue];\n};\n\n// 2. Implement keyboard navigation\nconst useKeyboardNav = (items: any[], onSelect: (item: any) => void) => {\n  const [selectedIndex, setSelectedIndex] = useState(0);\n\n  const handleKeyDown = (e: KeyboardEvent) => {\n    if (e.key === 'ArrowDown') {\n      setSelectedIndex(prev => Math.min(prev + 1, items.length - 1));\n    } else if (e.key === 'ArrowUp') {\n      setSelectedIndex(prev => Math.max(prev - 1, 0));\n    } else if (e.key === 'Enter') {\n      onSelect(items[selectedIndex]);\n    }\n  };\n\n  useEffect(() => {\n    window.addEventListener('keydown', handleKeyDown);\n    return () => window.removeEventListener('keydown', handleKeyDown);\n  }, [selectedIndex, items]);\n\n  return { selectedIndex };\n};\n```\n\n## Implementation Steps\n\n1. **Create Component Scaffolding**\n   ```bash\n   mkdir -p src/components/{Modal,CommandPalette}\n   touch src/components/Modal/SettingsModal.tsx\n   touch src/components/CommandPalette/CommandPalette.tsx\n   ```\n\n2. **Implement localStorage Persistence**\n   ```typescript\n   // SettingsModal.tsx\n   const SettingsModal = () => {\n     const [settings, setSettings] = useLocalStorage('app-settings', DEFAULT_SETTINGS);\n     // ... modal implementation\n   };\n   ```\n\n3. **Add Keyboard Navigation**\n   ```typescript\n   // CommandPalette.tsx\n   const CommandPalette = ({ commands }) => {\n     const { selectedIndex } = useKeyboardNav(commands, (cmd) => cmd.action());\n     // ... render commands with highlighted selectedIndex\n   };\n   ```\n\n4. **Export Components**\n   ```typescript\n   // src/components/index.ts\n   export * from './Modal/SettingsModal';\n   export * from './CommandPalette/CommandPalette';\n   ```\n\n## Best Practices\n\n- Create a `settings-keys.ts` file to manage localStorage keys\n- Use TypeScript interfaces for settings/commands\n- Implement proper focus management for accessibility\n\n<EVOLUTION_COMPLETE>"}


skill_id: verify-directory-structure__v0_ac1cc7f4
name: verify-directory-structure
description: Diagnose and resolve file access issues by first verifying the directory structure.
path: example/skills/verify-directory-structure/SKILL.md
category: workflow
visibility: private
lineage_origin: captured
lineage_source_task_id: evolution-task-22
lineage_created_at: 2026-03-10T16:15:36.373142
lineage_created_by: openrouter/deepseek/deepseek-chat
first_seen: 2026-03-10T16:15:36.373170
last_updated: 2026-03-10T22:20:21.749418
lineage_change_summary:
Capture the pattern of diagnosing and resolving file access issues by first verifying directory structure.

lineage_content_diff:
--- a/SKILL.md
+++ b/SKILL.md
@@ -0,0 +1,29 @@
+---
+name: verify-directory-structure
+description: Diagnose and resolve file access issues by first verifying the directory structure.
+---
+
+# Verify Directory Structure
+
+This skill provides a systematic approach to diagnosing and resolving file access issues by first verifying the directory structure. It is particularly useful when dealing with file operations, script executions, or any task that involves accessing files.
+
+## Steps
+
+1. **Identify the Target Path**: Determine the file or directory path that is causing the access issue.
+2. **Check Directory Existence**: Use the `list_dir` tool to verify if the directory exists and inspect its contents.
+3. **Verify Permissions**: Ensure that the user has the necessary permissions to access the directory and its contents.
+4. **Resolve Path Issues**: If the directory does not exist, create it using appropriate commands or scripts. If permissions are insufficient, adjust them accordingly.
+5. **Retry the Operation**: After resolving the directory structure issues, retry the original operation.
+
+## Example
+
+```bash
+# Example of using the list_dir tool to verify directory structure
+list_dir --path "/path/to/directory"
+```
+
+## Best Practices
+
+- Always verify the directory structure before performing file operations.
+- Use absolute paths to avoid ambiguity.
+- Log the directory structure for debugging purposes if issues persist.

lineage_content_snapshot:
{"SKILL.md": "---\nname: verify-directory-structure\ndescription: Diagnose and resolve file access issues by first verifying the directory structure.\n---\n\n# Verify Directory Structure\n\nThis skill provides a systematic approach to diagnosing and resolving file access issues by first verifying the directory structure. It is particularly useful when dealing with file operations, script executions, or any task that involves accessing files.\n\n## Steps\n\n1. **Identify the Target Path**: Determine the file or directory path that is causing the access issue.\n2. **Check Directory Existence**: Use the `list_dir` tool to verify if the directory exists and inspect its contents.\n3. **Verify Permissions**: Ensure that the user has the necessary permissions to access the directory and its contents.\n4. **Resolve Path Issues**: If the directory does not exist, create it using appropriate commands or scripts. If permissions are insufficient, adjust them accordingly.\n5. **Retry the Operation**: After resolving the directory structure issues, retry the original operation.\n\n## Example\n\n```bash\n# Example of using the list_dir tool to verify directory structure\nlist_dir --path \"/path/to/directory\"\n```\n\n## Best Practices\n\n- Always verify the directory structure before performing file operations.\n- Use absolute paths to avoid ambiguity.\n- Log the directory structure for debugging purposes if issues persist."}


skill_id: write-file-special-chars-workaround__v0_2f19eace
name: write-special-chars-heredoc
path: example/skills/write-special-chars-heredoc/SKILL.md
category: tool_guide
visibility: private
lineage_origin: captured
lineage_source_task_id: evolution-v2-task-01
lineage_created_at: 2026-03-10T18:10:41.223191
lineage_created_by: openrouter/anthropic/claude-sonnet-4.6
first_seen: 2026-03-10T18:10:41.223208
last_updated: 2026-03-10T18:45:52.043210
description:
Workaround for write_file failures caused by special characters (apostrophes, backticks, template literals) by using a Python heredoc script executed via shell.

lineage_change_summary:
When write_file fails with special characters, use a Python heredoc workaround by writing a small Python script to /tmp/ and executing it to reliably handle TypeScript content with apostrophes, backticks, and template literals.

lineage_content_diff:
--- a/SKILL.md
+++ b/SKILL.md
@@ -0,0 +1,139 @@
+---
+name: write-file-special-chars-workaround
+description: Workaround for write_file failures caused by special characters (apostrophes, backticks, template literals) by using a Python heredoc script executed via shell.
+---
+
+# Write File Special Characters Workaround
+
+## Problem
+
+The `write_file` tool may fail with `[ERROR] unknown error` when file content
+contains special characters such as:
+
+- Apostrophes / single quotes (`'`)
+- Backticks (`` ` ``)
+- Template literals (`` `${variable}` ``) common in TypeScript/JavaScript
+- Complex combinations of quotes and escape sequences
+
+This is especially common when writing TypeScript, JavaScript, shell scripts,
+or any source file with string interpolation syntax.
+
+## Solution: Python Heredoc Workaround
+
+Write a small Python script to `/tmp/` that uses triple-quoted strings to
+embed the file content, then execute it with `python3`.
+
+### Step-by-Step
+
+1. **Compose a Python writer script** using triple-quoted strings (`"""..."""`).
+2. **Write the Python script itself to `/tmp/`** — its content is plain enough
+   that `write_file` will succeed (no problematic characters in the wrapper).
+3. **Execute** the script with `run_shell`: `python3 /tmp/write_<name>.py`
+
+### Escaping Rules Inside the Python Script
+
+| Character | How to handle |
+|-----------|--------------|
+| Backslash `\` | Escape as `\\` |
+| Triple double-quote `"""` | Escape as `\"\"\"` or use `'''` strings instead |
+| Everything else (backticks, `$`, `'`, `{}`) | No escaping needed |
+
+### Template
+
+```python
+#!/usr/bin/env python3
+content = """
+<YOUR FILE CONTENT HERE>
+""".lstrip("\n")
+
+with open("/path/to/target/file.ts", "w") as f:
+    f.write(content)
+
+print("File written successfully.")
+```
+
+### Concrete Example
+
+Suppose you need to write a TypeScript file with template literals and
+apostrophes that causes `write_file` to fail:
+
+**Step 1 — Write the Python helper to `/tmp/`:**
+
+Use `write_file` with path `/tmp/write_greeting.py` and content:
+
+```python
+#!/usr/bin/env python3
+content = """
+export function greet(name: string): string {
+  const msg = `Hello, ${name}! It's a great day.`;
+  console.log(`Greeting: ${msg}`);
+  return msg;
+}
+""".lstrip("\n")
+
+with open("/app/src/greeting.ts", "w") as f:
+    f.write(content)
+
+print("Written: /app/src/greeting.ts")
+```
+
+**Step 2 — Execute the helper:**
+
+```bash
+python3 /tmp/write_greeting.py
+```
+
+**Step 3 — Verify:**
+
+```bash
+cat /app/src/greeting.ts
+```
+
+## When to Use This Pattern
+
+- `write_file` returns `[ERROR] unknown error` for a specific file.
+- The file contains TypeScript/JavaScript template literals.
+- The file contains mixed quote styles or shell-like special characters.
+- Any time direct file writing fails and content cannot be easily sanitized.
+
+## Multiple Files
+
+For writing several problematic files in one pass, consolidate them into a
+single Python script:
+
+```python
+#!/usr/bin/env python3
+import os
+
+files = {
+    "/app/src/component.tsx": """
+import React from 'react';
+
+const App = () => (
+  <div className={`container`}>
+    <h1>It's working!</h1>
+  </div>
+);
+
+export default App;
+""".lstrip("\n"),
+
+    "/app/src/utils.ts": """
+export const format = (val: number) => `Value: ${val.toFixed(2)}`;
+""".lstrip("\n"),
+}
+
+for path, content in files.items():
+    os.makedirs(os.path.dirname(path), exist_ok=True)
+    with open(path, "w") as f:
+        f.write(content)
+    print(f"Written: {path}")
+```
+
+## Key Advantages
+
+- **No shell escaping required** — Python triple-quoted strings are literal.
+- **Handles all TypeScript syntax** — template literals, generics, JSX, etc.
+- **Atomic** — the target file is only created if the Python script succeeds.
+- **Debuggable** — the intermediate `.py` file can be inspected if needed.
+- **Cleanup (optional):** `rm /tmp/write_*.py` after all files are written.

lineage_content_snapshot:
{"SKILL.md": "---\nname: write-file-special-chars-workaround\ndescription: Workaround for write_file failures caused by special characters (apostrophes, backticks, template literals) by using a Python heredoc script executed via shell.\n---\n\n# Write File Special Characters Workaround\n\n## Problem\n\nThe `write_file` tool may fail with `[ERROR] unknown error` when file content\ncontains special characters such as:\n\n- Apostrophes / single quotes (`'`)\n- Backticks (`` ` ``)\n- Template literals (`` `${variable}` ``) common in TypeScript/JavaScript\n- Complex combinations of quotes and escape sequences\n\nThis is especially common when writing TypeScript, JavaScript, shell scripts,\nor any source file with string interpolation syntax.\n\n## Solution: Python Heredoc Workaround\n\nWrite a small Python script to `/tmp/` that uses triple-quoted strings to\nembed the file content, then execute it with `python3`.\n\n### Step-by-Step\n\n1. **Compose a Python writer script** using triple-quoted strings (`\"\"\"...\"\"\"`).\n2. **Write the Python script itself to `/tmp/`** — its content is plain enough\n   that `write_file` will succeed (no problematic characters in the wrapper).\n3. **Execute** the script with `run_shell`: `python3 /tmp/write_<name>.py`\n\n### Escaping Rules Inside the Python Script\n\n| Character | How to handle |\n|-----------|--------------|\n| Backslash `\\` | Escape as `\\\\` |\n| Triple double-quote `\"\"\"` | Escape as `\\\"\\\"\\\"` or use `'''` strings instead |\n| Everything else (backticks, `$`, `'`, `{}`) | No escaping needed |\n\n### Template\n\n```python\n#!/usr/bin/env python3\ncontent = \"\"\"\n<YOUR FILE CONTENT HERE>\n\"\"\".lstrip(\"\\n\")\n\nwith open(\"/path/to/target/file.ts\", \"w\") as f:\n    f.write(content)\n\nprint(\"File written successfully.\")\n```\n\n### Concrete Example\n\nSuppose you need to write a TypeScript file with template literals and\napostrophes that causes `write_file` to fail:\n\n**Step 1 — Write the Python helper to `/tmp/`:**\n\nUse `write_file` with path `/tmp/write_greeting.py` and content:\n\n```python\n#!/usr/bin/env python3\ncontent = \"\"\"\nexport function greet(name: string): string {\n  const msg = `Hello, ${name}! It's a great day.`;\n  console.log(`Greeting: ${msg}`);\n  return msg;\n}\n\"\"\".lstrip(\"\\n\")\n\nwith open(\"/app/src/greeting.ts\", \"w\") as f:\n    f.write(content)\n\nprint(\"Written: /app/src/greeting.ts\")\n```\n\n**Step 2 — Execute the helper:**\n\n```bash\npython3 /tmp/write_greeting.py\n```\n\n**Step 3 — Verify:**\n\n```bash\ncat /app/src/greeting.ts\n```\n\n## When to Use This Pattern\n\n- `write_file` returns `[ERROR] unknown error` for a specific file.\n- The file contains TypeScript/JavaScript template literals.\n- The file contains mixed quote styles or shell-like special characters.\n- Any time direct file writing fails and content cannot be easily sanitized.\n\n## Multiple Files\n\nFor writing several problematic files in one pass, consolidate them into a\nsingle Python script:\n\n```python\n#!/usr/bin/env python3\nimport os\n\nfiles = {\n    \"/app/src/component.tsx\": \"\"\"\nimport React from 'react';\n\nconst App = () => (\n  <div className={`container`}>\n    <h1>It's working!</h1>\n  </div>\n);\n\nexport default App;\n\"\"\".lstrip(\"\\n\"),\n\n    \"/app/src/utils.ts\": \"\"\"\nexport const format = (val: number) => `Value: ${val.toFixed(2)}`;\n\"\"\".lstrip(\"\\n\"),\n}\n\nfor path, content in files.items():\n    os.makedirs(os.path.dirname(path), exist_ok=True)\n    with open(path, \"w\") as f:\n        f.write(content)\n    print(f\"Written: {path}\")\n```\n\n## Key Advantages\n\n- **No shell escaping required** — Python triple-quoted strings are literal.\n- **Handles all TypeScript syntax** — template literals, generics, JSX, etc.\n- **Atomic** — the target file is only created if the Python script succeeds.\n- **Debuggable** — the intermediate `.py` file can be inspected if needed.\n- **Cleanup (optional):** `rm /tmp/write_*.py` after all files are written."}


skill_id: create-full-stack-panel-feature__v1_efa89c56
name: create-full-stack-panel-feature
path: example/skills/create-full-stack-panel-feature/SKILL.md
category: workflow
visibility: private
lineage_origin: fixed
lineage_source_task_id: evolution-v2-task-01
lineage_created_at: 2026-03-10T18:10:59.534352
lineage_created_by: openrouter/anthropic/claude-sonnet-4.6
first_seen: 2026-03-10T18:10:59.534365
last_updated: 2026-03-10T18:35:14.036008
description:
Multi-layer workflow for creating complete panel features by analyzing existing patterns and generating coordinated server routes, services, UI components, registration, and styling files.

lineage_change_summary:
Added a framework detection step before template selection and a vanilla TypeScript variant for UI components using DOM API patterns (Panel base class, createElement, setContent) instead of React hooks.

lineage_content_diff:
--- a/SKILL.md
+++ b/SKILL.md
@@ -84,6 +84,30 @@
 4. **Panel registration** (e.g., update `client/config/panel-registry.js`)
 5. **Styling** (e.g., `client/styles/system-health-panel.css`)
 6. **Type definitions** (if TypeScript, e.g., `types/system-health.ts`)
+
+### 4. Create Files in Order
+
+### 3b. Detect Project Framework
+
+**Before selecting templates in Step 4, determine the frontend framework in use:**
+
+```bash
+# 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:**
+- If `react` or `react-dom` is present in `package.json` → use the **React/JSX template** (Step 4C-React).
+- If components are `.ts` files extending a `Panel` base class → use the **Vanilla TS template** (Step 4C-Vanilla).
+- When in doubt, read one or two existing component files to confirm before proceeding.
 
 ### 4. Create Files in Order
 
@@ -126,7 +150,9 @@
 module.exports = new FeatureService();
 ```
 
-#### C. UI Component
+#### C-React. UI Component (React/JSX projects)
+
+*Use this template only when React is confirmed in Step 3b.*
 
 ```javascript
 // client/components/{Feature}Panel.jsx
@@ -164,7 +190,72 @@
 export default FeaturePanel;
 ```
 
+#### C-Vanilla. UI Component (Vanilla TypeScript / Panel base-class projects)
+
+*Use this template when components extend a Panel base class (no React).*
+
+```typescript
+// 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:**
+- Extends `Panel` base class; uses `setContent()` / `showError()` / `setTitle()` / `setCount()` instead of React state.
+- DOM construction via `document.createElement` / `innerHTML` instead of JSX.
+- No `useState` / `useEffect`; lifecycle is `onLoad()` + `destroy()`.
+- File extension is `.ts`, not `.jsx`/`.tsx`.
+- Registration passes a factory function or class constructor, not a JSX component.
+
 #### D. Panel Registration
+
+**React projects** — import the component directly:
 
 ```javascript
 // client/config/panel-registry.js (update existing)
@@ -182,6 +273,24 @@
 ];
 ```
 
+**Vanilla TS projects** — register via factory:
+
+```typescript
+// 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'
+  }
+];
+```
+
 #### E. Styling
 
 ```css
@@ -250,4 +359,4 @@
 - Test files for each layer
 - Documentation files
 
-**For TypeScript projects**: Add `.d.ts` or `.ts` type definition files+**For TypeScript projects**: Add `.d.ts` or `.ts` type definition files

lineage_content_snapshot:
{"SKILL.md": "---\nname: create-full-stack-panel-feature\ndescription: Multi-layer workflow for creating complete panel features by analyzing existing patterns and generating coordinated server routes, services, UI components, registration, and styling files.\n---\n\n# Create Full-Stack Panel Feature\n\nThis skill guides you through creating a complete, pattern-consistent panel feature across all application layers: backend API routes, service logic, frontend components, registration, and styling.\n\n## When to Use\n\n- Adding a new panel type to a dashboard or admin interface\n- Creating a feature that spans multiple architectural layers\n- Ensuring consistency with existing codebase patterns across the stack\n\n## Workflow Steps\n\n### 1. Identify Pattern Files\n\nLocate existing examples for each layer you need to implement:\n\n```bash\n# Find existing server routes\nfind . -name \"*route*.js\" -o -name \"*routes*.js\" | grep -E \"(panel|dashboard)\"\n\n# Find service files\nfind . -name \"*service*.js\" | grep -E \"(panel|dashboard)\"\n\n# Find UI components\nfind . -name \"*.jsx\" -o -name \"*.tsx\" | grep -i panel\n\n# Find registration/config files\nfind . -name \"*config*.js\" -o -name \"*registry*.js\"\n\n# Find styling files\nfind . -name \"*.css\" -o -name \"*.scss\" | grep -i panel\n```\n\n### 2. Read and Analyze Patterns\n\nRead 1-2 representative examples from **each layer** to understand:\n\n- Naming conventions (e.g., `MetricsPanel`, `metrics-service.js`)\n- Code structure and organization\n- Import/export patterns\n- Registration mechanisms\n- API endpoint patterns\n- Error handling approaches\n- Styling conventions\n\n**Example analysis checklist:**\n\n```\nServer Route Pattern:\n- ✓ Endpoint naming: /api/panels/{type}\n- ✓ Authentication middleware\n- ✓ Response format: { success, data, error }\n\nService Pattern:\n- ✓ Export structure: class vs functions\n- ✓ Data transformation logic\n- ✓ Error propagation\n\nComponent Pattern:\n- ✓ Props interface\n- ✓ State management (hooks/class)\n- ✓ Data fetching approach\n- ✓ Loading/error states\n\nRegistration Pattern:\n- ✓ Registry file location\n- ✓ Registration format (array/object)\n- ✓ Required metadata fields\n```\n\n### 3. Plan Your Feature Files\n\nBased on patterns, list the files you need to create:\n\n**Typical full-stack panel structure:**\n1. **Server route** (e.g., `server/routes/system-health-route.js`)\n2. **Service layer** (e.g., `server/services/system-health-service.js`)\n3. **UI component** (e.g., `client/components/SystemHealthPanel.jsx`)\n4. **Panel registration** (e.g., update `client/config/panel-registry.js`)\n5. **Styling** (e.g., `client/styles/system-health-panel.css`)\n6. **Type definitions** (if TypeScript, e.g., `types/system-health.ts`)\n\n### 4. Create Files in Order\n\n### 3b. Detect Project Framework\n\n**Before selecting templates in Step 4, determine the frontend framework in use:**\n\n```bash\n# Check for React\ngrep -s \"react\" package.json | head -5\n\n# Check for TypeScript without React (vanilla TS)\nls client/components/*.ts 2>/dev/null | head -3\nls client/panels/*.ts 2>/dev/null | head -3\n\n# Check for a Panel base class (common in vanilla TS dashboards)\ngrep -r \"class.*Panel\" client/ --include=\"*.ts\" -l | head -3\ngrep -r \"extends Panel\" client/ --include=\"*.ts\" -l | head -3\n```\n\n**Decision rule:**\n- If `react` or `react-dom` is present in `package.json` → use the **React/JSX template** (Step 4C-React).\n- If components are `.ts` files extending a `Panel` base class → use the **Vanilla TS template** (Step 4C-Vanilla).\n- When in doubt, read one or two existing component files to confirm before proceeding.\n\n### 4. Create Files in Order\n\nGenerate files in dependency order (backend → frontend → registration):\n\n#### A. Server Route\n\n```javascript\n// server/routes/{feature}-route.js\nconst express = require('express');\nconst router = express.Router();\nconst featureService = require('../services/{feature}-service');\n\nrouter.get('/api/panels/{feature}', async (req, res) => {\n  try {\n    const data = await featureService.getData();\n    res.json({ success: true, data });\n  } catch (error) {\n    res.status(500).json({ success: false, error: error.message });\n  }\n});\n\nmodule.exports = router;\n```\n\n#### B. Service Layer\n\n```javascript\n// server/services/{feature}-service.js\nclass FeatureService {\n  async getData() {\n    // Implementation following existing service patterns\n    // - Data fetching\n    // - Business logic\n    // - Data transformation\n    return processedData;\n  }\n}\n\nmodule.exports = new FeatureService();\n```\n\n#### C-React. UI Component (React/JSX projects)\n\n*Use this template only when React is confirmed in Step 3b.*\n\n```javascript\n// client/components/{Feature}Panel.jsx\nimport React, { useState, useEffect } from 'react';\nimport './styles/{feature}-panel.css';\n\nconst FeaturePanel = () => {\n  const [data, setData] = useState(null);\n  const [loading, setLoading] = useState(true);\n  const [error, setError] = useState(null);\n\n  useEffect(() => {\n    fetch('/api/panels/{feature}')\n      .then(res => res.json())\n      .then(result => {\n        setData(result.data);\n        setLoading(false);\n      })\n      .catch(err => {\n        setError(err.message);\n        setLoading(false);\n      });\n  }, []);\n\n  if (loading) return <div>Loading...</div>;\n  if (error) return <div>Error: {error}</div>;\n\n  return (\n    <div className=\"{feature}-panel\">\n      {/* Render data following UI patterns */}\n    </div>\n  );\n};\n\nexport default FeaturePanel;\n```\n\n#### C-Vanilla. UI Component (Vanilla TypeScript / Panel base-class projects)\n\n*Use this template when components extend a Panel base class (no React).*\n\n```typescript\n// client/panels/{Feature}Panel.ts\nimport { Panel } from '../core/Panel';\n\nexport class FeaturePanel extends Panel {\n  private intervalId: number | null = null;\n\n  constructor(id: string) {\n    super(id);\n    this.setTitle('Feature Display Name');\n  }\n\n  async onLoad(): Promise<void> {\n    if (this.isFetching) return;\n    this.isFetching = true;\n    try {\n      const res = await fetch('/api/panels/{feature}', {\n        headers: { Authorization: `Bearer ${localStorage.getItem('token') ?? ''}` }\n      });\n      if (!res.ok) throw new Error(`HTTP ${res.status}`);\n      const { data } = await res.json();\n      this.render(data);\n    } catch (err) {\n      this.showError(err instanceof Error ? err.message : String(err), () => this.onLoad());\n    } finally {\n      this.isFetching = false;\n    }\n  }\n\n  private render(data: unknown): void {\n    const container = document.createElement('div');\n    container.className = '{feature}-panel';\n    // Build DOM nodes from data instead of JSX:\n    // const item = document.createElement('p');\n    // item.textContent = String(data);\n    // container.appendChild(item);\n    this.setContent(container);\n  }\n\n  // Call this to start polling (optional)\n  startPolling(intervalMs = 60_000): void {\n    this.onLoad();\n    this.intervalId = window.setInterval(() => this.onLoad(), intervalMs);\n  }\n\n  destroy(): void {\n    if (this.intervalId !== null) clearInterval(this.intervalId);\n    super.destroy();\n  }\n}\n```\n\n**Key differences from React template:**\n- Extends `Panel` base class; uses `setContent()` / `showError()` / `setTitle()` / `setCount()` instead of React state.\n- DOM construction via `document.createElement` / `innerHTML` instead of JSX.\n- No `useState` / `useEffect`; lifecycle is `onLoad()` + `destroy()`.\n- File extension is `.ts`, not `.jsx`/`.tsx`.\n- Registration passes a factory function or class constructor, not a JSX component.\n\n#### D. Panel Registration\n\n**React projects** — import the component directly:\n\n```javascript\n// client/config/panel-registry.js (update existing)\nimport FeaturePanel from '../components/FeaturePanel';\n\nexport const panels = [\n  // ... existing panels\n  {\n    id: '{feature}',\n    name: 'Feature Display Name',\n    component: FeaturePanel,\n    icon: 'icon-name',\n    category: 'appropriate-category'\n  }\n];\n```\n\n**Vanilla TS projects** — register via factory:\n\n```typescript\n// client/config/panel-registry.ts (update existing)\nimport { FeaturePanel } from '../panels/FeaturePanel';\n\nexport const panels = [\n  // ... existing panels\n  {\n    id: '{feature}',\n    name: 'Feature Display Name',\n    factory: (id: string) => new FeaturePanel(id),\n    icon: 'icon-name',\n    category: 'appropriate-category'\n  }\n];\n```\n\n#### E. Styling\n\n```css\n/* client/styles/{feature}-panel.css */\n.{feature}-panel {\n  /* Follow existing panel styling conventions */\n  padding: 1rem;\n  border-radius: 4px;\n}\n\n.{feature}-panel__header {\n  /* Consistent header styling */\n}\n```\n\n### 5. Verify Pattern Consistency\n\nAfter creating all files, check:\n\n- [ ] Naming follows project conventions across all layers\n- [ ] Import/export statements are consistent\n- [ ] Error handling matches existing patterns\n- [ ] API response format is uniform\n- [ ] Component structure mirrors other panels\n- [ ] Registration metadata is complete\n- [ ] CSS class naming follows BEM or project convention\n\n### 6. Test Integration Points\n\nVerify that:\n\n```bash\n# Server route is registered\ngrep -r \"require.*{feature}-route\" server/\n\n# Component is imported in registry\ngrep -r \"import.*{Feature}Panel\" client/config/\n\n# Styles are imported\ngrep -r \"import.*{feature}-panel.css\" client/\n```\n\n## Key Principles\n\n1. **Pattern before implementation**: Always read existing code first\n2. **Maintain consistency**: Match naming, structure, and style exactly\n3. **Complete the stack**: Don't leave layers incomplete\n4. **Follow the chain**: Backend → Service → Frontend → Registration → Styling\n5. **Verify integration**: Ensure all pieces connect properly\n\n## Common Pitfalls\n\n- Creating component before verifying API endpoint works\n- Inconsistent naming across layers (camelCase vs kebab-case)\n- Forgetting to register the panel in the appropriate config\n- Missing error handling in any layer\n- Skipping styling, leading to broken UI\n\n## Variations\n\n**For simple features**: May omit service layer if route logic is trivial\n\n**For complex features**: May need additional files:\n- Database migrations/models\n- Redux actions/reducers (if using Redux)\n- Test files for each layer\n- Documentation files\n\n**For TypeScript projects**: Add `.d.ts` or `.ts` type definition files\n"}


skill_id: panel-component-enhanced_a8e7c7__v0_b625d6f9
name: panel-component-xss-safe
path: example/skills/panel-component-xss-safe/SKILL.md
category: reference
visibility: private
lineage_origin: derived
lineage_source_task_id: evolution-v2-task-01
lineage_created_at: 2026-03-10T18:11:17.234474
lineage_created_by: openrouter/anthropic/claude-sonnet-4.6
first_seen: 2026-03-10T18:11:17.234490
last_updated: 2026-03-10T18:11:17.234492
lineage_change_summary:
Added XSS-safe rendering pattern with an `esc()` helper function and updated all innerHTML interpolation examples to escape untrusted external API data.

lineage_content_diff:
--- a/SKILL.md
+++ b/SKILL.md
@@ -1,11 +1,13 @@
 ---
-name: panel-component
-description: Create a dashboard panel component using vanilla TypeScript DOM API, following the worldmonitor Panel architecture. Panels have a header with title/count, scrollable content area, loading/error states, and resize handles.
----
-
-# Panel Component Pattern
+name: panel-component-xss-safe
+description: Create a dashboard panel component using vanilla TypeScript DOM API, following the worldmonitor Panel architecture. Panels have a header with title/count, scrollable content area, loading/error states, and resize handles. Includes XSS-safe rendering pattern with esc() helper for safely interpolating untrusted external API data into innerHTML.
+---
+
+# Panel Component Pattern (XSS-Safe)
 
 Create dashboard panel components using **vanilla TypeScript** (no framework, no JSX). Each panel is a class extending a `Panel` base class.
+
+> **Security note:** Panels that consume external API data (calendar events, news feeds, stock names, user-supplied content, etc.) MUST escape all untrusted values before injecting them into `innerHTML`. Use the `esc()` helper documented below.
 
 ## Architecture Overview
 
@@ -22,7 +24,63 @@
 │   └── resizeHandle (.panel-resize-handle)
 ```
 
-## Base Panel Class (Simplified for this project)
+## XSS-Safe Rendering: the `esc()` Helper
+
+**Always use `esc()` when interpolating untrusted data into an HTML string.**  
+Untrusted data includes anything from external APIs: event titles, locations, URLs, names, descriptions, symbols, etc.
+
+Create `src/utils/esc.ts`:
+
+```typescript
+/**
+ * Escapes a value for safe interpolation into an innerHTML string.
+ * Converts &, <, >, ", and ' to their HTML entity equivalents.
+ *
+ * Usage:
+ *   this.setContent(`<div class="title">${esc(item.title)}</div>`);
+ *
+ * Do NOT use for:
+ *   - href/src attributes with user-controlled URLs — validate scheme instead
+ *   - CSS values — use a separate sanitizer
+ */
+export function esc(value: unknown): string {
+  return String(value ?? '')
+    .replace(/&/g, '&amp;')
+    .replace(/</g, '&lt;')
+    .replace(/>/g, '&gt;')
+    .replace(/"/g, '&quot;')
+    .replace(/'/g, '&#39;');
+}
+```
+
+### When to use `esc()`
+
+| Data source | Safe? | Action |
+|---|---|---|
+| Hardcoded string literal in source | ✅ Safe | No escaping needed |
+| Enum / controlled constant | ✅ Safe | No escaping needed |
+| External API string field (title, name, body…) | ❌ Unsafe | **Use `esc()`** |
+| User input / localStorage value | ❌ Unsafe | **Use `esc()`** |
+| Number/boolean (rendered as text) | ✅ Safe | `String(n)` is fine |
+| URL from external API | ⚠️ Unsafe | Validate scheme + `esc()` |
+
+### URL Safety
+
+For URLs from external sources, validate the scheme before injecting:
+
+```typescript
+/** Returns the URL only if it uses http or https; otherwise returns '#'. */
+export function safeUrl(raw: unknown): string {
+  const s = String(raw ?? '').trim();
+  return /^https?:\/\//i.test(s) ? s : '#';
+}
+```
+
+Usage: `<a href="${safeUrl(item.url)}">${esc(item.title)}</a>`
+
+---
+
+## Base Panel Class
 
 Create `src/components/Panel.ts`:
 
@@ -57,7 +115,7 @@
 
     const title = document.createElement('span');
     title.className = 'panel-title';
-    title.textContent = options.title;
+    title.textContent = options.title;  // textContent is always safe
     headerLeft.appendChild(title);
     this.header.appendChild(headerLeft);
 
@@ -82,6 +140,7 @@
   public getElement(): HTMLElement { return this.element; }
 
   public showLoading(message = 'Loading...'): void {
+    // message is a hardcoded string — no escaping needed here
     this.content.innerHTML = `
       <div class="panel-loading">
         <div class="panel-loading-spinner"></div>
@@ -90,6 +149,7 @@
   }
 
   public showError(message = 'Failed to load', onRetry?: () => void): void {
+    // message is a hardcoded string — no escaping needed here
     this.content.innerHTML = `
       <div class="panel-error-state">
         <div class="panel-error-msg">${message}</div>
@@ -120,12 +180,15 @@
 }
 ```
 
-## Creating a Concrete Panel (Example: StockPanel)
-
-Each panel extends `Panel` and manages its own data fetching + rendering:
+---
+
+## Creating a Concrete Panel (Example: StockPanel — XSS-safe)
+
+Import `esc` and wrap every API-derived string field:
 
 ```typescript
 import { Panel } from './Panel';
+import { esc } from '../utils/esc';
 
 interface StockQuote {
   symbol: string;
@@ -161,14 +224,17 @@
   private render(quotes: StockQuote[]): void {
     const rows = quotes.map(q => `
       <div class="stock-row">
-        <span class="stock-symbol">${q.symbol}</span>
-        <span class="stock-name">${q.name}</span>
-        <span class="stock-price">${q.price != null ? '$' + q.price.toFixed(2) : '—'}</span>
+        <span class="stock-symbol">${esc(q.symbol)}</span>
+        <span class="stock-name">${esc(q.name)}</span>
+        <span class="stock-price">${q.price != null ? '$' + q.price.toFixed(2) : '&mdash;'}</span>
         <span class="stock-change ${(q.change ?? 0) >= 0 ? 'positive' : 'negative'}">
-          ${q.change != null ? (q.change >= 0 ? '+' : '') + q.change.toFixed(2) + '%' : '—'}
+          ${q.change != null ? (q.change >= 0 ? '+' : '') + q.change.toFixed(2) + '%' : '&mdash;'}
         </span>
       </div>
     `).join('');
+    //                ^^^         ^^^
+    // symbol and name come from an external API — always escape them.
+    // price and change are numbers rendered via .toFixed() — safe without esc().
 
     this.setContent(`<div class="stock-list">${rows}</div>`);
   }
@@ -180,17 +246,94 @@
 }
 ```
 
+## Creating a Concrete Panel (Example: EventPanel — calendar/news data)
+
+Calendar and news panels are highest-risk because titles, locations, and URLs all come from untrusted sources:
+
+```typescript
+import { Panel } from './Panel';
+import { esc, safeUrl } from '../utils/esc';
+
+interface CalendarEvent {
+  id: string;
+  title: string;       // untrusted — user-created content
+  location?: string;   // untrusted
+  url?: string;        // untrusted — must validate scheme
+  startTime: Date;
+}
+
+export class SchedulePanel extends Panel {
+  private refreshTimer: ReturnType<typeof setInterval> | null = null;
+
+  constructor() {
+    super({ id: 'schedule', title: "Today's Schedule", showCount: true });
+    this.fetchData();
+    this.refreshTimer = setInterval(() => this.fetchData(), 5 * 60_000);
+  }
+
+  private async fetchData(): Promise<void> {
+    if (this.isFetching) return;
+    this.setFetching(true);
+    try {
+      const events = await fetchCalendarEvents();
+      this.render(events);
+      this.setCount(events.length);
+    } catch (err) {
+      this.showError('Failed to load calendar', () => this.fetchData());
+    } finally {
+      this.setFetching(false);
+    }
+  }
+
+  private render(events: CalendarEvent[]): void {
+    if (events.length === 0) {
+      this.setContent('<div class="schedule-empty">No events today</div>');
+      return;
+    }
+
+    const rows = events.map(ev => `
+      <div class="schedule-row">
+        <span class="schedule-time">${esc(formatTime(ev.startTime))}</span>
+        <span class="schedule-title">
+          ${ev.url
+            ? `<a href="${safeUrl(ev.url)}" target="_blank" rel="noopener noreferrer">${esc(ev.title)}</a>`
+            : esc(ev.title)
+          }
+        </span>
+        ${ev.location ? `<span class="schedule-location">${esc(ev.location)}</span>` : ''}
+      </div>
+    `).join('');
+    //              ^^^              ^^^              ^^^
+    // title, location, url — ALL from external API, ALL escaped.
+    // safeUrl() prevents javascript: / data: scheme injection in href.
+
+    this.setContent(`<div class="schedule-list">${rows}</div>`);
+  }
+
+  public override destroy(): void {
+    if (this.refreshTimer) clearInterval(this.refreshTimer);
+    super.destroy();
+  }
+}
+```
+
+---
+
 ## Key Patterns
 
 1. **Constructor** calls `super()` with panel config, then triggers initial data fetch
 2. **fetchData()** is async, uses `isFetching` guard, shows error on failure with retry
-3. **render()** builds HTML strings and calls `this.setContent(html)`
+3. **render()** builds HTML strings with `esc()` around every API-derived string, then calls `this.setContent(html)`
 4. **destroy()** cleans up timers and event listeners
 5. Use `showLoading()` during initial load (auto-called in constructor)
-6. Use `showError(msg, retryFn)` on failure
-7. Sparkline SVGs use inline `<svg>` with `<polyline>` — see sparkline utility
+6. Use `showError(msg, retryFn)` on failure — `msg` should be a hardcoded string, not API data
+7. **Import `esc` from `../utils/esc`** in every panel that consumes external data
+8. Numbers and booleans rendered via `.toFixed()` / `.toString()` / template arithmetic are safe — no `esc()` needed
+9. Use `textContent` instead of `innerHTML` for single text nodes when convenient — it is always safe
 
 ## Sparkline Utility
+
+Sparklines use computed numbers only — no escaping needed:
 
 ```typescript
 export function miniSparkline(data: number[] | undefined, change: number | null, w = 50, h = 16): string {
@@ -207,3 +350,13 @@
   return `<svg width="${w}" height="${h}" viewBox="0 0 ${w} ${h}"><polyline points="${points}" fill="none" stroke="${color}" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
 }
 ```
+
+---
+
+## Checklist Before Submitting a Panel
+
+- [ ] Every `string` field from an external API is wrapped in `esc()` before HTML interpolation
+- [ ] Every URL from an external API is passed through `safeUrl()` before use in `href`/`src`
+- [ ] `showError()` is only called with hardcoded messages (never raw API error bodies)
+- [ ] `esc` and `safeUrl` are imported from `src/utils/esc`
+- [ ] Numbers / computed values rendered with `.toFixed()` or arithmetic are left unwrapped (they cannot contain HTML)

--- a/src/utils/esc.ts
+++ b/src/utils/esc.ts
@@ -0,0 +1,31 @@
+/**
+ * Escapes a value for safe interpolation into an innerHTML string.
+ * Converts &, <, >, ", and ' to their HTML entity equivalents.
+ *
+ * Usage:
+ *   this.setContent(`<div class="title">${esc(item.title)}</div>`);
+ *
+ * Do NOT use for:
+ *   - href/src attributes with user-controlled URLs — validate scheme instead (use safeUrl)
+ *   - CSS values — use a separate sanitizer
+ */
+export function esc(value: unknown): string {
+  return String(value ?? '')
+    .replace(/&/g, '&amp;')
+    .replace(/</g, '&lt;')
+    .replace(/>/g, '&gt;')
+    .replace(/"/g, '&quot;')
+    .replace(/'/g, '&#39;');
+}
+
+/**
+ * Returns the URL only if it uses the http or https scheme.
+ * Prevents javascript: and data: URL injection in href/src attributes.
+ *
+ * Usage:
+ *   `<a href="${safeUrl(item.url)}">${esc(item.title)}</a>`
+ */
+export function safeUrl(raw: unknown): string {
+  const s = String(raw ?? '').trim();
+  return /^https?:\/\//i.test(s) ? s : '#';
+}

lineage_content_snapshot:
{"SKILL.md": "---\nname: panel-component-xss-safe\ndescription: Create a dashboard panel component using vanilla TypeScript DOM API, following the worldmonitor Panel architecture. Panels have a header with title/count, scrollable content area, loading/error states, and resize handles. Includes XSS-safe rendering pattern with esc() helper for safely interpolating untrusted external API data into innerHTML.\n---\n\n# Panel Component Pattern (XSS-Safe)\n\nCreate dashboard panel components using **vanilla TypeScript** (no framework, no JSX). Each panel is a class extending a `Panel` base class.\n\n> **Security note:** Panels that consume external API data (calendar events, news feeds, stock names, user-supplied content, etc.) MUST escape all untrusted values before injecting them into `innerHTML`. Use the `esc()` helper documented below.\n\n## Architecture Overview\n\n```\nPanel (base class)\n├── element: HTMLElement (outer container, .panel)\n│   ├── header: HTMLElement (.panel-header)\n│   │   ├── headerLeft (.panel-header-left)\n│   │   │   ├── title (.panel-title)\n│   │   │   └── newBadge (.panel-new-badge) [optional]\n│   │   ├── statusBadge (.panel-data-badge) [optional]\n│   │   └── countEl (.panel-count) [optional]\n│   ├── content: HTMLElement (.panel-content)\n│   └── resizeHandle (.panel-resize-handle)\n```\n\n## XSS-Safe Rendering: the `esc()` Helper\n\n**Always use `esc()` when interpolating untrusted data into an HTML string.**  \nUntrusted data includes anything from external APIs: event titles, locations, URLs, names, descriptions, symbols, etc.\n\nCreate `src/utils/esc.ts`:\n\n```typescript\n/**\n * Escapes a value for safe interpolation into an innerHTML string.\n * Converts &, <, >, \", and ' to their HTML entity equivalents.\n *\n * Usage:\n *   this.setContent(`<div class=\"title\">${esc(item.title)}</div>`);\n *\n * Do NOT use for:\n *   - href/src attributes with user-controlled URLs — validate scheme instead\n *   - CSS values — use a separate sanitizer\n */\nexport function esc(value: unknown): string {\n  return String(value ?? '')\n    .replace(/&/g, '&amp;')\n    .replace(/</g, '&lt;')\n    .replace(/>/g, '&gt;')\n    .replace(/\"/g, '&quot;')\n    .replace(/'/g, '&#39;');\n}\n```\n\n### When to use `esc()`\n\n| Data source | Safe? | Action |\n|---|---|---|\n| Hardcoded string literal in source | ✅ Safe | No escaping needed |\n| Enum / controlled constant | ✅ Safe | No escaping needed |\n| External API string field (title, name, body…) | ❌ Unsafe | **Use `esc()`** |\n| User input / localStorage value | ❌ Unsafe | **Use `esc()`** |\n| Number/boolean (rendered as text) | ✅ Safe | `String(n)` is fine |\n| URL from external API | ⚠️ Unsafe | Validate scheme + `esc()` |\n\n### URL Safety\n\nFor URLs from external sources, validate the scheme before injecting:\n\n```typescript\n/** Returns the URL only if it uses http or https; otherwise returns '#'. */\nexport function safeUrl(raw: unknown): string {\n  const s = String(raw ?? '').trim();\n  return /^https?:\\/\\//i.test(s) ? s : '#';\n}\n```\n\nUsage: `<a href=\"${safeUrl(item.url)}\">${esc(item.title)}</a>`\n\n---\n\n## Base Panel Class\n\nCreate `src/components/Panel.ts`:\n\n```typescript\nexport interface PanelOptions {\n  id: string;\n  title: string;\n  showCount?: boolean;\n  className?: string;\n}\n\nexport class Panel {\n  protected element: HTMLElement;\n  protected content: HTMLElement;\n  protected header: HTMLElement;\n  protected countEl: HTMLElement | null = null;\n  protected panelId: string;\n  private _fetching = false;\n\n  constructor(options: PanelOptions) {\n    this.panelId = options.id;\n    this.element = document.createElement('div');\n    this.element.className = `panel ${options.className || ''}`;\n    this.element.dataset.panel = options.id;\n\n    // Header\n    this.header = document.createElement('div');\n    this.header.className = 'panel-header';\n\n    const headerLeft = document.createElement('div');\n    headerLeft.className = 'panel-header-left';\n\n    const title = document.createElement('span');\n    title.className = 'panel-title';\n    title.textContent = options.title;  // textContent is always safe\n    headerLeft.appendChild(title);\n    this.header.appendChild(headerLeft);\n\n    // Count badge (optional)\n    if (options.showCount) {\n      this.countEl = document.createElement('span');\n      this.countEl.className = 'panel-count';\n      this.countEl.textContent = '0';\n      this.header.appendChild(this.countEl);\n    }\n\n    // Content area\n    this.content = document.createElement('div');\n    this.content.className = 'panel-content';\n    this.content.id = `${options.id}Content`;\n\n    this.element.appendChild(this.header);\n    this.element.appendChild(this.content);\n    this.showLoading();\n  }\n\n  public getElement(): HTMLElement { return this.element; }\n\n  public showLoading(message = 'Loading...'): void {\n    // message is a hardcoded string — no escaping needed here\n    this.content.innerHTML = `\n      <div class=\"panel-loading\">\n        <div class=\"panel-loading-spinner\"></div>\n        <div class=\"panel-loading-text\">${message}</div>\n      </div>`;\n  }\n\n  public showError(message = 'Failed to load', onRetry?: () => void): void {\n    // message is a hardcoded string — no escaping needed here\n    this.content.innerHTML = `\n      <div class=\"panel-error-state\">\n        <div class=\"panel-error-msg\">${message}</div>\n        ${onRetry ? '<button class=\"panel-retry-btn\" data-panel-retry>Retry</button>' : ''}\n      </div>`;\n    if (onRetry) {\n      this.content.querySelector('[data-panel-retry]')?.addEventListener('click', onRetry);\n    }\n  }\n\n  public setContent(html: string): void {\n    this.content.innerHTML = html;\n  }\n\n  public setCount(count: number): void {\n    if (this.countEl) this.countEl.textContent = count.toString();\n  }\n\n  public show(): void { this.element.classList.remove('hidden'); }\n  public hide(): void { this.element.classList.add('hidden'); }\n\n  protected setFetching(v: boolean): void { this._fetching = v; }\n  protected get isFetching(): boolean { return this._fetching; }\n\n  public destroy(): void {\n    this.element.remove();\n  }\n}\n```\n\n---\n\n## Creating a Concrete Panel (Example: StockPanel — XSS-safe)\n\nImport `esc` and wrap every API-derived string field:\n\n```typescript\nimport { Panel } from './Panel';\nimport { esc } from '../utils/esc';\n\ninterface StockQuote {\n  symbol: string;\n  name: string;\n  price: number | null;\n  change: number | null;\n  sparkline?: number[];\n}\n\nexport class StockPanel extends Panel {\n  private refreshTimer: ReturnType<typeof setInterval> | null = null;\n\n  constructor() {\n    super({ id: 'stocks', title: 'Stock Market', showCount: true });\n    this.fetchData();\n    this.refreshTimer = setInterval(() => this.fetchData(), 60_000);\n  }\n\n  private async fetchData(): Promise<void> {\n    if (this.isFetching) return;\n    this.setFetching(true);\n    try {\n      const quotes = await fetchStockQuotes(); // from data-service\n      this.render(quotes);\n      this.setCount(quotes.length);\n    } catch (err) {\n      this.showError('Failed to load stock data', () => this.fetchData());\n    } finally {\n      this.setFetching(false);\n    }\n  }\n\n  private render(quotes: StockQuote[]): void {\n    const rows = quotes.map(q => `\n      <div class=\"stock-row\">\n        <span class=\"stock-symbol\">${esc(q.symbol)}</span>\n        <span class=\"stock-name\">${esc(q.name)}</span>\n        <span class=\"stock-price\">${q.price != null ? '$' + q.price.toFixed(2) : '&mdash;'}</span>\n        <span class=\"stock-change ${(q.change ?? 0) >= 0 ? 'positive' : 'negative'}\">\n          ${q.change != null ? (q.change >= 0 ? '+' : '') + q.change.toFixed(2) + '%' : '&mdash;'}\n        </span>\n      </div>\n    `).join('');\n    //                ^^^         ^^^\n    // symbol and name come from an external API — always escape them.\n    // price and change are numbers rendered via .toFixed() — safe without esc().\n\n    this.setContent(`<div class=\"stock-list\">${rows}</div>`);\n  }\n\n  public override destroy(): void {\n    if (this.refreshTimer) clearInterval(this.refreshTimer);\n    super.destroy();\n  }\n}\n```\n\n## Creating a Concrete Panel (Example: EventPanel — calendar/news data)\n\nCalendar and news panels are highest-risk because titles, locations, and URLs all come from untrusted sources:\n\n```typescript\nimport { Panel } from './Panel';\nimport { esc, safeUrl } from '../utils/esc';\n\ninterface CalendarEvent {\n  id: string;\n  title: string;       // untrusted — user-created content\n  location?: string;   // untrusted\n  url?: string;        // untrusted — must validate scheme\n  startTime: Date;\n}\n\nexport class SchedulePanel extends Panel {\n  private refreshTimer: ReturnType<typeof setInterval> | null = null;\n\n  constructor() {\n    super({ id: 'schedule', title: \"Today's Schedule\", showCount: true });\n    this.fetchData();\n    this.refreshTimer = setInterval(() => this.fetchData(), 5 * 60_000);\n  }\n\n  private async fetchData(): Promise<void> {\n    if (this.isFetching) return;\n    this.setFetching(true);\n    try {\n      const events = await fetchCalendarEvents();\n      this.render(events);\n      this.setCount(events.length);\n    } catch (err) {\n      this.showError('Failed to load calendar', () => this.fetchData());\n    } finally {\n      this.setFetching(false);\n    }\n  }\n\n  private render(events: CalendarEvent[]): void {\n    if (events.length === 0) {\n      this.setContent('<div class=\"schedule-empty\">No events today</div>');\n      return;\n    }\n\n    const rows = events.map(ev => `\n      <div class=\"schedule-row\">\n        <span class=\"schedule-time\">${esc(formatTime(ev.startTime))}</span>\n        <span class=\"schedule-title\">\n          ${ev.url\n            ? `<a href=\"${safeUrl(ev.url)}\" target=\"_blank\" rel=\"noopener noreferrer\">${esc(ev.title)}</a>`\n            : esc(ev.title)\n          }\n        </span>\n        ${ev.location ? `<span class=\"schedule-location\">${esc(ev.location)}</span>` : ''}\n      </div>\n    `).join('');\n    //              ^^^              ^^^              ^^^\n    // title, location, url — ALL from external API, ALL escaped.\n    // safeUrl() prevents javascript: / data: scheme injection in href.\n\n    this.setContent(`<div class=\"schedule-list\">${rows}</div>`);\n  }\n\n  public override destroy(): void {\n    if (this.refreshTimer) clearInterval(this.refreshTimer);\n    super.destroy();\n  }\n}\n```\n\n---\n\n## Key Patterns\n\n1. **Constructor** calls `super()` with panel config, then triggers initial data fetch\n2. **fetchData()** is async, uses `isFetching` guard, shows error on failure with retry\n3. **render()** builds HTML strings with `esc()` around every API-derived string, then calls `this.setContent(html)`\n4. **destroy()** cleans up timers and event listeners\n5. Use `showLoading()` during initial load (auto-called in constructor)\n6. Use `showError(msg, retryFn)` on failure — `msg` should be a hardcoded string, not API data\n7. **Import `esc` from `../utils/esc`** in every panel that consumes external data\n8. Numbers and booleans rendered via `.toFixed()` / `.toString()` / template arithmetic are safe — no `esc()` needed\n9. Use `textContent` instead of `innerHTML` for single text nodes when convenient — it is always safe\n\n## Sparkline Utility\n\nSparklines use computed numbers only — no escaping needed:\n\n```typescript\nexport function miniSparkline(data: number[] | undefined, change: number | null, w = 50, h = 16): string {\n  if (!data || data.length < 2) return '';\n  const min = Math.min(...data);\n  const max = Math.max(...data);\n  const range = max - min || 1;\n  const color = change != null && change >= 0 ? 'var(--green)' : 'var(--red)';\n  const points = data.map((v, i) => {\n    const x = (i / (data.length - 1)) * w;\n    const y = h - ((v - min) / range) * (h - 2) - 1;\n    return `${x.toFixed(1)},${y.toFixed(1)}`;\n  }).join(' ');\n  return `<svg width=\"${w}\" height=\"${h}\" viewBox=\"0 0 ${w} ${h}\"><polyline points=\"${points}\" fill=\"none\" stroke=\"${color}\" stroke-width=\"1.2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>`;\n}\n```\n\n---\n\n## Checklist Before Submitting a Panel\n\n- [ ] Every `string` field from an external API is wrapped in `esc()` before HTML interpolation\n- [ ] Every URL from an external API is passed through `safeUrl()` before use in `href`/`src`\n- [ ] `showError()` is only called with hardcoded messages (never raw API error bodies)\n- [ ] `esc` and `safeUrl` are imported from `src/utils/esc`\n- [ ] Numbers / computed values rendered with `.toFixed()` or arithmetic are left unwrapped (they cannot contain HTML)\n", "src/utils/esc.ts": "/**\n * Escapes a value for safe interpolation into an innerHTML string.\n * Converts &, <, >, \", and ' to their HTML entity equivalents.\n *\n * Usage:\n *   this.setContent(`<div class=\"title\">${esc(item.title)}</div>`);\n *\n * Do NOT use for:\n *   - href/src attributes with user-controlled URLs — validate scheme instead (use safeUrl)\n *   - CSS values — use a separate sanitizer\n */\nexport function esc(value: unknown): string {\n  return String(value ?? '')\n    .replace(/&/g, '&amp;')\n    .replace(/</g, '&lt;')\n    .replace(/>/g, '&gt;')\n    .replace(/\"/g, '&quot;')\n    .replace(/'/g, '&#39;');\n}\n\n/**\n * Returns the URL only if it uses the http or https scheme.\n * Prevents javascript: and data: URL injection in href/src attributes.\n *\n * Usage:\n *   `<a href=\"${safeUrl(item.url)}\">${esc(item.title)}</a>`\n */\nexport function safeUrl(raw: unknown): string {\n  const s = String(raw ?? '').trim();\n  return /^https?:\\/\\//i.test(s) ? s : '#';\n}\n"}


skill_id: typescript-typecheck-fallback__v0_4993ee44
name: typescript-typecheck-fallback
path: example/skills/typescript-typecheck-fallback/SKILL.md
category: workflow
visibility: private
lineage_origin: captured
lineage_source_task_id: evolution-v2-task-02
lineage_created_at: 2026-03-10T18:15:38.414176
lineage_created_by: openrouter/anthropic/claude-sonnet-4.6
first_seen: 2026-03-10T18:15:38.414188
last_updated: 2026-03-10T18:15:38.414188
description:
Reliably run TypeScript type-checking (npx tsc --noEmit) by using shell_agent as a fallback when run_shell fails due to environment or timeout issues.

lineage_change_summary:
When `run_shell` fails for TypeScript compilation checks, fall back to `shell_agent` with a natural-language task to reliably execute `npx tsc --noEmit` and report errors.

lineage_content_diff:
--- a/SKILL.md
+++ b/SKILL.md
@@ -0,0 +1,120 @@
+---
+name: typescript-typecheck-fallback
+description: Reliably run TypeScript type-checking (npx tsc --noEmit) by using shell_agent as a fallback when run_shell fails due to environment or timeout issues.
+---
+
+# TypeScript Type-Check with shell_agent Fallback
+
+When verifying TypeScript compilation as part of a build or validation workflow, `run_shell` may fail with unknown errors due to shell environment issues, missing PATH entries, timeout constraints, or other transient problems. This skill describes a two-step pattern: attempt the check with `run_shell` first, then fall back to `shell_agent` if it fails.
+
+---
+
+## When to Use
+
+- After creating or modifying TypeScript source files and you need to confirm there are no type errors.
+- As part of a CI-like verification step inside an autonomous workflow.
+- Any time `run_shell` returns an unexpected or unclear error for a `tsc` invocation.
+
+---
+
+## Step-by-Step Pattern
+
+### Step 1 — Attempt with `run_shell` (fast path)
+
+Try the direct command first. This is fast and gives immediate output when the environment is well-configured.
+
+```
+run_shell: npx tsc --noEmit
+```
+
+**Evaluate the result:**
+- ✅ **Exit code 0, no errors** → Type-check passed. Done.
+- ❌ **Exit code non-zero with TypeScript diagnostics** → Real type errors exist; surface them and fix.
+- ⚠️ **Unknown error, timeout, or no useful output** → Proceed to Step 2.
+
+---
+
+### Step 2 — Fall back to `shell_agent` (reliable path)
+
+When `run_shell` fails in an ambiguous or environmental way, delegate to `shell_agent` with a natural-language task. `shell_agent` can autonomously:
+- Locate the correct `npx`/`tsc` binary
+- Retry on transient failures
+- Navigate PATH or permission issues
+- Return a structured summary of any type errors
+
+**Task string to pass to `shell_agent`:**
+
+```
+Run `npx tsc --noEmit` in the project root and report all TypeScript type errors found. If there are no errors, confirm the compilation succeeded. If tsc is not available, attempt to install TypeScript locally with `npm install --save-dev typescript` first.
+```
+
+**Evaluate the result:**
+- ✅ **"No errors found" / exit 0** → Type-check passed.
+- ❌ **Type errors listed** → Collect and fix the reported diagnostics, then re-run.
+- ⚠️ **`tsconfig.json` missing** → Ensure the file exists in the project root before re-running.
+
+---
+
+## Decision Flowchart
+
+```
+run_shell("npx tsc --noEmit")
+        │
+        ├─ success (exit 0) ──────────────────► ✅ PASS
+        │
+        ├─ type errors reported ──────────────► ❌ Fix errors, retry
+        │
+        └─ unknown/env error ─────────────────► shell_agent(natural-language task)
+                                                        │
+                                                        ├─ success ───► ✅ PASS
+                                                        └─ errors  ───► ❌ Fix & retry
+```
+
+---
+
+## Example Implementation
+
+```python
+# Pseudo-code for an autonomous agent loop
+
+result = run_shell("npx tsc --noEmit", timeout=60)
+
+if result.exit_code == 0:
+    print("TypeScript check passed.")
+
+elif result.exit_code != 0 and result.stderr contains "TS\d+":
+    # Real TypeScript diagnostics — surface and fix them
+    raise TypeScriptError(result.stderr)
+
+else:
+    # Ambiguous failure — delegate to shell_agent
+    shell_agent(
+        task="Run `npx tsc --noEmit` in the project root and report all TypeScript "
+             "type errors found. If there are no errors, confirm compilation succeeded. "
+             "If tsc is not available, install it first with `npm install --save-dev typescript`."
+    )
+```
+
+---
+
+## Tips
+
+- **Always have a `tsconfig.json`**: `tsc --noEmit` requires a valid config. If none exists, generate one first with `npx tsc --init`.
+- **Set a reasonable timeout**: TypeScript compilation on large projects can take 30–120 seconds. Give `run_shell` at least `timeout=90`.
+- **Scope to changed files when possible**: Use `--project tsconfig.json` or a path glob to limit what tsc checks if only a subset of files changed.
+- **Prefer `shell_agent` for first-time setups**: If the project environment is freshly created and dependencies may not be fully installed, start with `shell_agent` directly to avoid a predictable `run_shell` failure.
+
+---
+
+## Generalizing Beyond TypeScript
+
+This same fallback pattern applies to any build/lint/type-check command that may be sensitive to shell environment:
+
+| Tool | Primary `run_shell` command | `shell_agent` fallback task |
+|---|---|---|
+| TypeScript | `npx tsc --noEmit` | "Run npx tsc --noEmit and report errors" |
+| ESLint | `npx eslint src/` | "Run npx eslint on the src directory and report lint errors" |
+| Python mypy | `python -m mypy src/` | "Run mypy on the src directory and report type errors" |
+| Go build | `go build ./...` | "Run go build and report compilation errors" |
+
+The core principle: **`run_shell` for speed; `shell_agent` for resilience.**

lineage_content_snapshot:
{"SKILL.md": "---\nname: typescript-typecheck-fallback\ndescription: Reliably run TypeScript type-checking (npx tsc --noEmit) by using shell_agent as a fallback when run_shell fails due to environment or timeout issues.\n---\n\n# TypeScript Type-Check with shell_agent Fallback\n\nWhen verifying TypeScript compilation as part of a build or validation workflow, `run_shell` may fail with unknown errors due to shell environment issues, missing PATH entries, timeout constraints, or other transient problems. This skill describes a two-step pattern: attempt the check with `run_shell` first, then fall back to `shell_agent` if it fails.\n\n---\n\n## When to Use\n\n- After creating or modifying TypeScript source files and you need to confirm there are no type errors.\n- As part of a CI-like verification step inside an autonomous workflow.\n- Any time `run_shell` returns an unexpected or unclear error for a `tsc` invocation.\n\n---\n\n## Step-by-Step Pattern\n\n### Step 1 — Attempt with `run_shell` (fast path)\n\nTry the direct command first. This is fast and gives immediate output when the environment is well-configured.\n\n```\nrun_shell: npx tsc --noEmit\n```\n\n**Evaluate the result:**\n- ✅ **Exit code 0, no errors** → Type-check passed. Done.\n- ❌ **Exit code non-zero with TypeScript diagnostics** → Real type errors exist; surface them and fix.\n- ⚠️ **Unknown error, timeout, or no useful output** → Proceed to Step 2.\n\n---\n\n### Step 2 — Fall back to `shell_agent` (reliable path)\n\nWhen `run_shell` fails in an ambiguous or environmental way, delegate to `shell_agent` with a natural-language task. `shell_agent` can autonomously:\n- Locate the correct `npx`/`tsc` binary\n- Retry on transient failures\n- Navigate PATH or permission issues\n- Return a structured summary of any type errors\n\n**Task string to pass to `shell_agent`:**\n\n```\nRun `npx tsc --noEmit` in the project root and report all TypeScript type errors found. If there are no errors, confirm the compilation succeeded. If tsc is not available, attempt to install TypeScript locally with `npm install --save-dev typescript` first.\n```\n\n**Evaluate the result:**\n- ✅ **\"No errors found\" / exit 0** → Type-check passed.\n- ❌ **Type errors listed** → Collect and fix the reported diagnostics, then re-run.\n- ⚠️ **`tsconfig.json` missing** → Ensure the file exists in the project root before re-running.\n\n---\n\n## Decision Flowchart\n\n```\nrun_shell(\"npx tsc --noEmit\")\n        │\n        ├─ success (exit 0) ──────────────────► ✅ PASS\n        │\n        ├─ type errors reported ──────────────► ❌ Fix errors, retry\n        │\n        └─ unknown/env error ─────────────────► shell_agent(natural-language task)\n                                                        │\n                                                        ├─ success ───► ✅ PASS\n                                                        └─ errors  ───► ❌ Fix & retry\n```\n\n---\n\n## Example Implementation\n\n```python\n# Pseudo-code for an autonomous agent loop\n\nresult = run_shell(\"npx tsc --noEmit\", timeout=60)\n\nif result.exit_code == 0:\n    print(\"TypeScript check passed.\")\n\nelif result.exit_code != 0 and result.stderr contains \"TS\\d+\":\n    # Real TypeScript diagnostics — surface and fix them\n    raise TypeScriptError(result.stderr)\n\nelse:\n    # Ambiguous failure — delegate to shell_agent\n    shell_agent(\n        task=\"Run `npx tsc --noEmit` in the project root and report all TypeScript \"\n             \"type errors found. If there are no errors, confirm compilation succeeded. \"\n             \"If tsc is not available, install it first with `npm install --save-dev typescript`.\"\n    )\n```\n\n---\n\n## Tips\n\n- **Always have a `tsconfig.json`**: `tsc --noEmit` requires a valid config. If none exists, generate one first with `npx tsc --init`.\n- **Set a reasonable timeout**: TypeScript compilation on large projects can take 30–120 seconds. Give `run_shell` at least `timeout=90`.\n- **Scope to changed files when possible**: Use `--project tsconfig.json` or a path glob to limit what tsc checks if only a subset of files changed.\n- **Prefer `shell_agent` for first-time setups**: If the project environment is freshly created and dependencies may not be fully installed, start with `shell_agent` directly to avoid a predictable `run_shell` failure.\n\n---\n\n## Generalizing Beyond TypeScript\n\nThis same fallback pattern applies to any build/lint/type-check command that may be sensitive to shell environment:\n\n| Tool | Primary `run_shell` command | `shell_agent` fallback task |\n|---|---|---|\n| TypeScript | `npx tsc --noEmit` | \"Run npx tsc --noEmit and report errors\" |\n| ESLint | `npx eslint src/` | \"Run npx eslint on the src directory and report lint errors\" |\n| Python mypy | `python -m mypy src/` | \"Run mypy on the src directory and report type errors\" |\n| Go build | `go build ./...` | \"Run go build and report compilation errors\" |\n\nThe core principle: **`run_shell` for speed; `shell_agent` for resilience.**"}


skill_id: data-service-enhanced_52e5ae__v0_50dbdfbd
name: data-service-optional-params
path: example/skills/data-service-optional-params/SKILL.md
category: reference
visibility: private
lineage_origin: derived
lineage_source_task_id: evolution-v2-task-02
lineage_created_at: 2026-03-10T18:15:51.450862
lineage_created_by: openrouter/anthropic/claude-sonnet-4.6
first_seen: 2026-03-10T18:15:51.451058
last_updated: 2026-03-10T18:15:51.451061
lineage_change_summary:
Added first-class documentation of the optional query-parameter construction pattern using URLSearchParams with conditional appending, making optional-filter API calls an explicit, reusable pattern in the skill.

lineage_content_diff:
--- a/SKILL.md
+++ b/SKILL.md
@@ -1,6 +1,6 @@
 ---
-name: data-service
-description: Create data fetching services with circuit breaker pattern for API resilience. Services handle fetch, cache, retry, and expose typed data to panel components.
+name: data-service-optional-params
+description: Create data fetching services with circuit breaker pattern for API resilience, including a first-class pattern for building optional query parameters via URLSearchParams. Services handle fetch, cache, retry, optional filters, and expose typed data to panel components.
 ---
 
 # Data Service Pattern
@@ -129,6 +129,112 @@
 }
 ```
 
+## Optional Query Parameter Pattern
+
+When a service function accepts **optional filter arguments** (e.g. `owner?`, `repo?`, `branch?`), build the query string conditionally using `URLSearchParams` and only append a key when the value is defined. Never include `undefined` or empty-string values in the query string — omit the key entirely.
+
+### Template
+
+```typescript
+export async function fetchItems(
+  required: string,
+  owner?: string,
+  repo?: string,
+  branch?: string,
+): Promise<Item[]> {
+  return breaker.execute(async () => {
+    // 1. Start with required params
+    const params = new URLSearchParams({ required });
+
+    // 2. Conditionally append optional params
+    if (owner)  params.set('owner',  owner);
+    if (repo)   params.set('repo',   repo);
+    if (branch) params.set('branch', branch);
+
+    // 3. Append query string only when there are params
+    const qs = params.toString();
+    const url = qs ? `/api/items?${qs}` : '/api/items';
+
+    const resp = await fetch(url);
+    if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
+    return resp.json();
+  }, []);
+}
+```
+
+### Real-world example — GitHub workflow runs
+
+```typescript
+// src/services/code-status.ts
+import { createCircuitBreaker } from '../utils/circuit-breaker';
+
+export interface WorkflowRun {
+  id: number;
+  name: string;
+  status: string;
+  conclusion: string | null;
+  created_at: string;
+  html_url: string;
+}
+
+const breaker = createCircuitBreaker<WorkflowRun[]>({
+  name: 'CodeStatus',
+  cacheTtlMs: 60_000, // 1 minute cache
+});
+
+/**
+ * Fetches recent GitHub workflow runs.
+ * owner and repo are optional — if omitted the server uses defaults
+ * defined in the API proxy (e.g. from environment variables).
+ */
+export async function fetchWorkflowRuns(
+  owner?: string,
+  repo?: string,
+): Promise<WorkflowRun[]> {
+  return breaker.execute(async () => {
+    const params = new URLSearchParams();
+    if (owner) params.set('owner', owner);
+    if (repo)  params.set('repo',  repo);
+
+    const qs = params.toString();
+    const resp = await fetch(qs ? `/api/github?${qs}` : '/api/github');
+    if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
+    return resp.json();
+  }, []);
+}
+```
+
+### Rules for optional params
+
+| Rule | Rationale |
+|------|-----------|
+| Use `params.set(key, value)` inside `if (value)` guards | Prevents `key=undefined` or `key=` appearing in the URL |
+| Build `URLSearchParams` before the `fetch` call | Keeps URL construction readable and testable in isolation |
+| Use `params.toString()` to decide whether to append `?` | `new URLSearchParams().toString()` is `""`, so `qs ? url?${qs} : url` is safe |
+| Prefer `params.set()` over string concatenation | Automatically percent-encodes special characters |
+| All optional params go after required params | Keeps the URLSearchParams object ordered and predictable |
+
+### Testing optional params in isolation
+
+Because URL construction is pure logic, test it separately before wiring up the fetch:
+
+```typescript
+function buildGithubParams(owner?: string, repo?: string): string {
+  const params = new URLSearchParams();
+  if (owner) params.set('owner', owner);
+  if (repo)  params.set('repo',  repo);
+  return params.toString();
+}
+
+// Examples
+buildGithubParams();                    // ""
+buildGithubParams('acme');              // "owner=acme"
+buildGithubParams('acme', 'dashboard'); // "owner=acme&repo=dashboard"
+buildGithubParams(undefined, 'dash');   // "repo=dash"
+```
+
+Extract this helper when the same param set is used in multiple service functions.
+
 ## Key Patterns
 
 1. **One circuit breaker per API endpoint** — create at module level
@@ -137,20 +243,26 @@
 4. **Default values** are empty arrays `[]` or empty objects `{}`
 5. **Services are stateless modules** — no class instances, just exported functions
 6. **API proxy**: Frontend calls `/api/...` which proxies to external APIs (hides API keys)
+7. **Optional params**: Use `URLSearchParams` + conditional `params.set()` — never build query strings by hand
 
 ## API Proxy Pattern
 
 For APIs requiring keys, create server-side proxy endpoints:
 
 ```typescript
-// api/stocks.ts (Vercel serverless function)
+// api/github.ts (Vercel serverless function)
 export default async function handler(req, res) {
-  const { symbols } = req.query;
-  const apiKey = process.env.FINNHUB_API_KEY;
-  const data = await fetch(`https://finnhub.io/api/v1/quote?symbol=${symbols}&token=${apiKey}`);
+  // Read optional overrides from the query string; fall back to env defaults
+  const owner = (req.query.owner as string) ?? process.env.GITHUB_OWNER;
+  const repo  = (req.query.repo  as string) ?? process.env.GITHUB_REPO;
+  const token = process.env.GITHUB_TOKEN;
+
+  const url = `https://api.github.com/repos/${owner}/${repo}/actions/runs?per_page=10`;
+  const data = await fetch(url, {
+    headers: { Authorization: `Bearer ${token}`, Accept: 'application/vnd.github+json' },
+  });
   res.json(await data.json());
 }
 ```
 
-This keeps API keys server-side and provides a single endpoint for the frontend.
-
+This keeps API keys server-side, forwards only the safe query parameters the frontend supplies, and provides a single endpoint for the frontend.

lineage_content_snapshot:
{"SKILL.md": "---\nname: data-service-optional-params\ndescription: Create data fetching services with circuit breaker pattern for API resilience, including a first-class pattern for building optional query parameters via URLSearchParams. Services handle fetch, cache, retry, optional filters, and expose typed data to panel components.\n---\n\n# Data Service Pattern\n\nEach panel's data comes from a dedicated **service module** in `src/services/`. Services handle API calls, caching, error handling with circuit breaker pattern.\n\n## Circuit Breaker\n\nThe circuit breaker prevents cascading failures when an API is down. After N consecutive failures, it enters \"cooldown\" mode and serves cached data instead of hitting the API.\n\nCreate `src/utils/circuit-breaker.ts`:\n\n```typescript\ninterface CircuitState {\n  failures: number;\n  cooldownUntil: number;\n}\n\ninterface CacheEntry<T> {\n  data: T;\n  timestamp: number;\n}\n\nexport interface CircuitBreakerOptions {\n  name: string;\n  maxFailures?: number;\n  cooldownMs?: number;\n  cacheTtlMs?: number;\n}\n\nexport class CircuitBreaker<T> {\n  private state: CircuitState = { failures: 0, cooldownUntil: 0 };\n  private cache: CacheEntry<T> | null = null;\n  private name: string;\n  private maxFailures: number;\n  private cooldownMs: number;\n  private cacheTtlMs: number;\n\n  constructor(options: CircuitBreakerOptions) {\n    this.name = options.name;\n    this.maxFailures = options.maxFailures ?? 2;\n    this.cooldownMs = options.cooldownMs ?? 5 * 60 * 1000;  // 5 minutes\n    this.cacheTtlMs = options.cacheTtlMs ?? 10 * 60 * 1000; // 10 minutes\n  }\n\n  isOnCooldown(): boolean {\n    if (Date.now() < this.state.cooldownUntil) return true;\n    if (this.state.cooldownUntil > 0) {\n      this.state = { failures: 0, cooldownUntil: 0 };\n    }\n    return false;\n  }\n\n  getCached(): T | null {\n    if (this.cache && Date.now() - this.cache.timestamp < this.cacheTtlMs) {\n      return this.cache.data;\n    }\n    return null;\n  }\n\n  recordSuccess(data: T): void {\n    this.state = { failures: 0, cooldownUntil: 0 };\n    this.cache = { data, timestamp: Date.now() };\n  }\n\n  recordFailure(error?: string): void {\n    this.state.failures++;\n    if (this.state.failures >= this.maxFailures) {\n      this.state.cooldownUntil = Date.now() + this.cooldownMs;\n      console.warn(`[${this.name}] Cooldown for ${this.cooldownMs / 1000}s`);\n    }\n  }\n\n  async execute<R extends T>(fn: () => Promise<R>, defaultValue: R): Promise<R> {\n    if (this.isOnCooldown()) {\n      const cached = this.getCached();\n      return (cached as R) ?? defaultValue;\n    }\n    const cached = this.getCached();\n    if (cached !== null) return cached as R;\n    try {\n      const result = await fn();\n      this.recordSuccess(result);\n      return result;\n    } catch (e) {\n      console.error(`[${this.name}] Failed:`, e);\n      this.recordFailure(String(e));\n      return defaultValue;\n    }\n  }\n}\n\nexport function createCircuitBreaker<T>(options: CircuitBreakerOptions): CircuitBreaker<T> {\n  return new CircuitBreaker<T>(options);\n}\n```\n\n## Service Module Pattern\n\nEach service is a TypeScript module that exports async data-fetching functions:\n\n```typescript\n// src/services/stock-market.ts\nimport { createCircuitBreaker } from '../utils/circuit-breaker';\n\nexport interface StockQuote {\n  symbol: string;\n  name: string;\n  price: number | null;\n  change: number | null;\n  sparkline?: number[];\n}\n\nconst breaker = createCircuitBreaker<StockQuote[]>({\n  name: 'StockMarket',\n  cacheTtlMs: 60_000, // 1 minute cache\n});\n\nexport async function fetchStockQuotes(symbols: string[]): Promise<StockQuote[]> {\n  return breaker.execute(async () => {\n    const params = new URLSearchParams({ symbols: symbols.join(',') });\n    const resp = await fetch(`/api/stocks?${params}`);\n    if (!resp.ok) throw new Error(`HTTP ${resp.status}`);\n    return resp.json();\n  }, []);\n}\n```\n\n## Optional Query Parameter Pattern\n\nWhen a service function accepts **optional filter arguments** (e.g. `owner?`, `repo?`, `branch?`), build the query string conditionally using `URLSearchParams` and only append a key when the value is defined. Never include `undefined` or empty-string values in the query string — omit the key entirely.\n\n### Template\n\n```typescript\nexport async function fetchItems(\n  required: string,\n  owner?: string,\n  repo?: string,\n  branch?: string,\n): Promise<Item[]> {\n  return breaker.execute(async () => {\n    // 1. Start with required params\n    const params = new URLSearchParams({ required });\n\n    // 2. Conditionally append optional params\n    if (owner)  params.set('owner',  owner);\n    if (repo)   params.set('repo',   repo);\n    if (branch) params.set('branch', branch);\n\n    // 3. Append query string only when there are params\n    const qs = params.toString();\n    const url = qs ? `/api/items?${qs}` : '/api/items';\n\n    const resp = await fetch(url);\n    if (!resp.ok) throw new Error(`HTTP ${resp.status}`);\n    return resp.json();\n  }, []);\n}\n```\n\n### Real-world example — GitHub workflow runs\n\n```typescript\n// src/services/code-status.ts\nimport { createCircuitBreaker } from '../utils/circuit-breaker';\n\nexport interface WorkflowRun {\n  id: number;\n  name: string;\n  status: string;\n  conclusion: string | null;\n  created_at: string;\n  html_url: string;\n}\n\nconst breaker = createCircuitBreaker<WorkflowRun[]>({\n  name: 'CodeStatus',\n  cacheTtlMs: 60_000, // 1 minute cache\n});\n\n/**\n * Fetches recent GitHub workflow runs.\n * owner and repo are optional — if omitted the server uses defaults\n * defined in the API proxy (e.g. from environment variables).\n */\nexport async function fetchWorkflowRuns(\n  owner?: string,\n  repo?: string,\n): Promise<WorkflowRun[]> {\n  return breaker.execute(async () => {\n    const params = new URLSearchParams();\n    if (owner) params.set('owner', owner);\n    if (repo)  params.set('repo',  repo);\n\n    const qs = params.toString();\n    const resp = await fetch(qs ? `/api/github?${qs}` : '/api/github');\n    if (!resp.ok) throw new Error(`HTTP ${resp.status}`);\n    return resp.json();\n  }, []);\n}\n```\n\n### Rules for optional params\n\n| Rule | Rationale |\n|------|-----------|\n| Use `params.set(key, value)` inside `if (value)` guards | Prevents `key=undefined` or `key=` appearing in the URL |\n| Build `URLSearchParams` before the `fetch` call | Keeps URL construction readable and testable in isolation |\n| Use `params.toString()` to decide whether to append `?` | `new URLSearchParams().toString()` is `\"\"`, so `qs ? url?${qs} : url` is safe |\n| Prefer `params.set()` over string concatenation | Automatically percent-encodes special characters |\n| All optional params go after required params | Keeps the URLSearchParams object ordered and predictable |\n\n### Testing optional params in isolation\n\nBecause URL construction is pure logic, test it separately before wiring up the fetch:\n\n```typescript\nfunction buildGithubParams(owner?: string, repo?: string): string {\n  const params = new URLSearchParams();\n  if (owner) params.set('owner', owner);\n  if (repo)  params.set('repo',  repo);\n  return params.toString();\n}\n\n// Examples\nbuildGithubParams();                    // \"\"\nbuildGithubParams('acme');              // \"owner=acme\"\nbuildGithubParams('acme', 'dashboard'); // \"owner=acme&repo=dashboard\"\nbuildGithubParams(undefined, 'dash');   // \"repo=dash\"\n```\n\nExtract this helper when the same param set is used in multiple service functions.\n\n## Key Patterns\n\n1. **One circuit breaker per API endpoint** — create at module level\n2. **Export typed interfaces** for data shapes\n3. **Wrap fetch calls** in `breaker.execute(fn, defaultValue)`\n4. **Default values** are empty arrays `[]` or empty objects `{}`\n5. **Services are stateless modules** — no class instances, just exported functions\n6. **API proxy**: Frontend calls `/api/...` which proxies to external APIs (hides API keys)\n7. **Optional params**: Use `URLSearchParams` + conditional `params.set()` — never build query strings by hand\n\n## API Proxy Pattern\n\nFor APIs requiring keys, create server-side proxy endpoints:\n\n```typescript\n// api/github.ts (Vercel serverless function)\nexport default async function handler(req, res) {\n  // Read optional overrides from the query string; fall back to env defaults\n  const owner = (req.query.owner as string) ?? process.env.GITHUB_OWNER;\n  const repo  = (req.query.repo  as string) ?? process.env.GITHUB_REPO;\n  const token = process.env.GITHUB_TOKEN;\n\n  const url = `https://api.github.com/repos/${owner}/${repo}/actions/runs?per_page=10`;\n  const data = await fetch(url, {\n    headers: { Authorization: `Bearer ${token}`, Accept: 'application/vnd.github+json' },\n  });\n  res.json(await data.json());\n}\n```\n\nThis keeps API keys server-side, forwards only the safe query parameters the frontend supplies, and provides a single endpoint for the frontend.\n"}


skill_id: panel-component-enhanced-enhanced__v0_49b114a8
name: panel-component-colocated-helpers
path: example/skills/panel-component-colocated-helpers/SKILL.md
category: reference
visibility: private
lineage_origin: derived
lineage_source_task_id: evolution-v2-task-02
lineage_created_at: 2026-03-10T18:16:49.374823
lineage_created_by: openrouter/anthropic/claude-sonnet-4.6
first_seen: 2026-03-10T18:16:49.374866
last_updated: 2026-03-10T18:16:49.374867
lineage_change_summary:
Added documentation for co-located inline helper functions (timeAgo, statusIcon) within panel files to promote self-contained implementations and reduce import overhead for panel-specific utilities.

lineage_content_diff:
--- a/SKILL.md
+++ b/SKILL.md
@@ -1,6 +1,6 @@
 ---
-name: panel-component-enhanced
-description: Enhanced dashboard panel component with robust error handling, retry logic, and localStorage persistence for state management.
+name: panel-component-colocated-helpers
+description: Enhanced dashboard panel component with robust error handling, retry logic, localStorage persistence, and co-located inline helper functions (e.g. timeAgo, statusIcon) for self-contained, import-light panel implementations.
 ---
 
 # Panel Component Pattern
@@ -13,6 +13,7 @@
 - **Retry Logic**: Automatically retries failed data fetches with exponential backoff.
 - **State Persistence**: Saves panel state (expanded/collapsed, size) to localStorage.
 - **Detailed Error Handling**: Provides more granular error messages and recovery options.
+- **Co-located Helpers**: Panel-specific pure utility functions live in the same file as the panel class, keeping implementations self-contained.
 
 ```
 Panel (base class)
@@ -264,6 +265,180 @@
 5. Use `showLoading()` during initial load (auto-called in constructor)
 6. Use `showError(msg, retryFn)` on failure
 7. Sparkline SVGs use inline `<svg>` with `<polyline>` — see sparkline utility
+8. **Co-located helpers**: Define panel-specific pure utility functions (e.g. `timeAgo`, `statusIcon`) as module-level functions in the same `.ts` file as the panel class. Do **not** place them in `src/utils` unless they are reused by three or more panels.
+
+## Co-located Helper Pattern
+
+Panel-specific formatting and mapping utilities live directly above the class
+definition in the same file. This keeps the panel self-contained, avoids
+polluting `src/utils`, and makes the file easier to read and test in isolation.
+
+### When to co-locate
+
+| Situation | Decision |
+|-----------|----------|
+| Helper used only in this panel | ✅ Co-locate in panel file |
+| Helper used in 2 panels | ✅ Co-locate in the more "owning" panel; import from there |
+| Helper used in 3+ panels | ❌ Move to `src/utils/<helper>.ts` |
+| Helper depends on DOM or Panel API | ✅ Always co-locate (private method or module function) |
+
+### timeAgo — relative timestamp helper
+
+```typescript
+// co-located at the top of, e.g., src/components/CodeStatusPanel.ts
+
+/** Returns a human-readable relative time string, e.g. "3 minutes ago". */
+function timeAgo(dateStr: string): string {
+  const diff = Date.now() - new Date(dateStr).getTime();
+  const minutes = Math.floor(diff / 60_000);
+  if (minutes < 1)  return 'just now';
+  if (minutes < 60) return `${minutes} minute${minutes === 1 ? '' : 's'} ago`;
+  const hours = Math.floor(minutes / 60);
+  if (hours < 24)   return `${hours} hour${hours === 1 ? '' : 's'} ago`;
+  const days = Math.floor(hours / 24);
+  return `${days} day${days === 1 ? '' : 's'} ago`;
+}
+```
+
+Usage inside `render()`:
+
+```typescript
+private render(runs: WorkflowRun[]): void {
+  const rows = runs.map(r => `
+    <div class="run-row">
+      <span class="run-name">${r.name}</span>
+      <span class="run-time">${timeAgo(r.updatedAt)}</span>
+    </div>
+  `).join('');
+  this.setContent(`<div class="run-list">${rows}</div>`);
+}
+```
+
+### statusIcon — enum-to-icon mapping helper
+
+Maps a string enum (e.g. CI status) to an icon character or emoji. Define
+the map as a `const` outside the function so it is allocated once.
+
+```typescript
+// co-located at the top of, e.g., src/components/CodeStatusPanel.ts
+
+const STATUS_ICON: Record<string, string> = {
+  success:     '✅',
+  failure:     '❌',
+  cancelled:   '⛔',
+  skipped:     '⏭️',
+  in_progress: '🔄',
+  queued:      '⏳',
+};
+
+/** Returns an icon string for a workflow run conclusion/status value. */
+function statusIcon(status: string | null): string {
+  if (!status) return '❓';
+  return STATUS_ICON[status] ?? '❓';
+}
+```
+
+Usage inside `render()`:
+
+```typescript
+const rows = runs.map(r => `
+  <div class="run-row ${r.conclusion ?? r.status}">
+    <span class="run-status">${statusIcon(r.conclusion ?? r.status)}</span>
+    <span class="run-name">${r.name}</span>
+    <span class="run-time">${timeAgo(r.updatedAt)}</span>
+  </div>
+`).join('');
+```
+
+### Full example: CodeStatusPanel with co-located helpers
+
+```typescript
+import { Panel } from './Panel';
+import { fetchWorkflowRuns, WorkflowRun } from '../services/code-status';
+
+// ── Co-located helpers ────────────────────────────────────────────────────────
+
+const STATUS_ICON: Record<string, string> = {
+  success:     '✅',
+  failure:     '❌',
+  cancelled:   '⛔',
+  skipped:     '⏭️',
+  in_progress: '🔄',
+  queued:      '⏳',
+};
+
+function statusIcon(status: string | null): string {
+  if (!status) return '❓';
+  return STATUS_ICON[status] ?? '❓';
+}
+
+function timeAgo(dateStr: string): string {
+  const diff = Date.now() - new Date(dateStr).getTime();
+  const minutes = Math.floor(diff / 60_000);
+  if (minutes < 1)  return 'just now';
+  if (minutes < 60) return `${minutes} minute${minutes === 1 ? '' : 's'} ago`;
+  const hours = Math.floor(minutes / 60);
+  if (hours < 24)   return `${hours} hour${hours === 1 ? '' : 's'} ago`;
+  const days = Math.floor(hours / 24);
+  return `${days} day${days === 1 ? '' : 's'} ago`;
+}
+
+// ── Panel class ───────────────────────────────────────────────────────────────
+
+export class CodeStatusPanel extends Panel {
+  private refreshTimer: ReturnType<typeof setInterval> | null = null;
+
+  constructor() {
+    super({ id: 'codeStatus', title: 'CI / CD Status', showCount: true });
+    this.fetchData();
+    this.refreshTimer = setInterval(() => this.fetchData(), 60_000);
+  }
+
+  private async fetchData(): Promise<void> {
+    if (this.isFetching) return;
+    this.setFetching(true);
+    try {
+      const runs = await fetchWorkflowRuns();
+      this.render(runs);
+      this.setCount(runs.length);
+      this.saveState();
+    } catch (err) {
+      this.showError(`Failed to load CI status: ${(err as Error).message}`, () => this.fetchData());
+    } finally {
+      this.setFetching(false);
+    }
+  }
+
+  private render(runs: WorkflowRun[]): void {
+    if (!runs.length) {
+      this.setContent('<p class="panel-empty">No recent runs.</p>');
+      return;
+    }
+    const rows = runs.map(r => `
+      <div class="run-row ${r.conclusion ?? r.status}">
+        <span class="run-icon">${statusIcon(r.conclusion ?? r.status)}</span>
+        <span class="run-name">${r.name}</span>
+        <span class="run-branch">${r.headBranch}</span>
+        <span class="run-time">${timeAgo(r.updatedAt)}</span>
+      </div>
+    `).join('');
+    this.setContent(`<div class="run-list">${rows}</div>`);
+  }
+
+  public override destroy(): void {
+    if (this.refreshTimer) clearInterval(this.refreshTimer);
+    super.destroy();
+  }
+}
+```
+
+### Naming conventions for co-located helpers
+
+- Use **camelCase** function names (`timeAgo`, `statusIcon`, `formatBytes`).
+- Keep them **pure** (no side effects, no DOM access) so they are trivially unit-testable.
+- Place them **above** the class definition, below imports.
+- Prefix module-level constants that back a helper with the helper name in SCREAMING_SNAKE (`STATUS_ICON` for `statusIcon`).
+- Do **not** export them unless another file already needs them — keep the API surface minimal.
 
 ## Sparkline Utility
 

--- a/examples/localStorageUtils.ts
+++ b/examples/localStorageUtils.ts
@@ -0,0 +1,3 @@
+*** End Files
+
+<EVOLUTION_COMPLETE>

lineage_content_snapshot:
{"SKILL.md": "---\nname: panel-component-colocated-helpers\ndescription: Enhanced dashboard panel component with robust error handling, retry logic, localStorage persistence, and co-located inline helper functions (e.g. timeAgo, statusIcon) for self-contained, import-light panel implementations.\n---\n\n# Panel Component Pattern\n\nCreate dashboard panel components using **vanilla TypeScript** (no framework, no JSX). Each panel is a class extending a `Panel` base class.\n\n## Architecture Overview\n\n### Enhanced Features\n- **Retry Logic**: Automatically retries failed data fetches with exponential backoff.\n- **State Persistence**: Saves panel state (expanded/collapsed, size) to localStorage.\n- **Detailed Error Handling**: Provides more granular error messages and recovery options.\n- **Co-located Helpers**: Panel-specific pure utility functions live in the same file as the panel class, keeping implementations self-contained.\n\n```\nPanel (base class)\n├── element: HTMLElement (outer container, .panel)\n│   ├── header: HTMLElement (.panel-header)\n│   │   ├── headerLeft (.panel-header-left)\n│   │   │   ├── title (.panel-title)\n│   │   │   └── newBadge (.panel-new-badge) [optional]\n│   │   ├── statusBadge (.panel-data-badge) [optional]\n│   │   └── countEl (.panel-count) [optional]\n│   ├── content: HTMLElement (.panel-content)\n│   └── resizeHandle (.panel-resize-handle)\n```\n\n## Base Panel Class (Simplified for this project)\n\n```typescript\nexport class Panel {\n  // ... existing code ...\n\n  private retryAttempts = 0;\n  private maxRetries = 3;\n  private retryDelay = 1000; // starts at 1s, doubles each retry\n\n  protected async fetchWithRetry(url: string): Promise<any> {\n    while (this.retryAttempts < this.maxRetries) {\n      try {\n        const response = await fetch(url);\n        if (!response.ok) throw new Error(`HTTP ${response.status}`);\n        return await response.json();\n      } catch (error) {\n        this.retryAttempts++;\n        if (this.retryAttempts >= this.maxRetries) {\n          throw new Error(`Failed after ${this.maxRetries} attempts: ${error.message}`);\n        }\n        await new Promise(resolve => setTimeout(resolve, this.retryDelay));\n        this.retryDelay *= 2;\n      }\n    }\n  }\n\n  public saveState(): void {\n    localStorage.setItem(`panelState_${this.panelId}`, JSON.stringify({\n      isExpanded: !this.element.classList.contains('collapsed'),\n      width: this.element.style.width,\n      height: this.element.style.height\n    }));\n  }\n\n  public loadState(): void {\n    const savedState = localStorage.getItem(`panelState_${this.panelId}`);\n    if (savedState) {\n      const { isExpanded, width, height } = JSON.parse(savedState);\n      if (!isExpanded) this.element.classList.add('collapsed');\n      if (width) this.element.style.width = width;\n      if (height) this.element.style.height = height;\n    }\n  }\n}\n```\n\nCreate `src/components/Panel.ts`:\n\n```typescript\nexport interface PanelOptions {\n  id: string;\n  title: string;\n  showCount?: boolean;\n  className?: string;\n}\n\nexport class Panel {\n  protected element: HTMLElement;\n  protected content: HTMLElement;\n  protected header: HTMLElement;\n  protected countEl: HTMLElement | null = null;\n  protected panelId: string;\n  private _fetching = false;\n\n  constructor(options: PanelOptions) {\n    this.panelId = options.id;\n    this.element = document.createElement('div');\n    this.element.className = `panel ${options.className || ''}`;\n    this.element.dataset.panel = options.id;\n\n    // Header\n    this.header = document.createElement('div');\n    this.header.className = 'panel-header';\n\n    const headerLeft = document.createElement('div');\n    headerLeft.className = 'panel-header-left';\n\n    const title = document.createElement('span');\n    title.className = 'panel-title';\n    title.textContent = options.title;\n    headerLeft.appendChild(title);\n    this.header.appendChild(headerLeft);\n\n    // Count badge (optional)\n    if (options.showCount) {\n      this.countEl = document.createElement('span');\n      this.countEl.className = 'panel-count';\n      this.countEl.textContent = '0';\n      this.header.appendChild(this.countEl);\n    }\n\n    // Content area\n    this.content = document.createElement('div');\n    this.content.className = 'panel-content';\n    this.content.id = `${options.id}Content`;\n\n    this.element.appendChild(this.header);\n    this.element.appendChild(this.content);\n    this.showLoading();\n  }\n\n  public getElement(): HTMLElement { return this.element; }\n\n  public showLoading(message = 'Loading...'): void {\n    this.content.innerHTML = `\n      <div class=\"panel-loading\">\n        <div class=\"panel-loading-spinner\"></div>\n        <div class=\"panel-loading-text\">${message}</div>\n      </div>`;\n  }\n\n  public showError(message = 'Failed to load', onRetry?: () => void): void {\n    this.content.innerHTML = `\n      <div class=\"panel-error-state\">\n        <div class=\"panel-error-msg\">${message}</div>\n        ${onRetry ? '<button class=\"panel-retry-btn\" data-panel-retry>Retry</button>' : ''}\n      </div>`;\n    if (onRetry) {\n      this.content.querySelector('[data-panel-retry]')?.addEventListener('click', onRetry);\n    }\n  }\n\n  public setContent(html: string): void {\n    this.content.innerHTML = html;\n  }\n\n  public setCount(count: number): void {\n    if (this.countEl) this.countEl.textContent = count.toString();\n  }\n\n  public show(): void { this.element.classList.remove('hidden'); }\n  public hide(): void { this.element.classList.add('hidden'); }\n\n  protected setFetching(v: boolean): void { this._fetching = v; }\n  protected get isFetching(): boolean { return this._fetching; }\n\n  public destroy(): void {\n    this.element.remove();\n  }\n}\n```\n\n## Creating a Concrete Panel (Example: StockPanel)\n\n```typescript\nexport class StockPanel extends Panel {\n  // ... existing code ...\n\n  private async fetchData(): Promise<void> {\n    if (this.isFetching) return;\n    this.setFetching(true);\n    try {\n      const quotes = await this.fetchWithRetry('/api/stocks');\n      this.render(quotes);\n      this.setCount(quotes.length);\n      this.saveState();\n    } catch (err) {\n      this.showError(`Failed to load stock data: ${err.message}`, () => {\n        this.retryAttempts = 0;\n        this.retryDelay = 1000;\n        this.fetchData();\n      });\n    } finally {\n      this.setFetching(false);\n    }\n  }\n}\n```\n\nEach panel extends `Panel` and manages its own data fetching + rendering:\n\n```typescript\nimport { Panel } from './Panel';\n\ninterface StockQuote {\n  symbol: string;\n  name: string;\n  price: number | null;\n  change: number | null;\n  sparkline?: number[];\n}\n\nexport class StockPanel extends Panel {\n  private refreshTimer: ReturnType<typeof setInterval> | null = null;\n\n  constructor() {\n    super({ id: 'stocks', title: 'Stock Market', showCount: true });\n    this.fetchData();\n    this.refreshTimer = setInterval(() => this.fetchData(), 60_000);\n  }\n\n  private async fetchData(): Promise<void> {\n    if (this.isFetching) return;\n    this.setFetching(true);\n    try {\n      const quotes = await fetchStockQuotes(); // from data-service\n      this.render(quotes);\n      this.setCount(quotes.length);\n    } catch (err) {\n      this.showError('Failed to load stock data', () => this.fetchData());\n    } finally {\n      this.setFetching(false);\n    }\n  }\n\n  private render(quotes: StockQuote[]): void {\n    const rows = quotes.map(q => `\n      <div class=\"stock-row\">\n        <span class=\"stock-symbol\">${q.symbol}</span>\n        <span class=\"stock-name\">${q.name}</span>\n        <span class=\"stock-price\">${q.price != null ? '$' + q.price.toFixed(2) : '—'}</span>\n        <span class=\"stock-change ${(q.change ?? 0) >= 0 ? 'positive' : 'negative'}\">\n          ${q.change != null ? (q.change >= 0 ? '+' : '') + q.change.toFixed(2) + '%' : '—'}\n        </span>\n      </div>\n    `).join('');\n\n    this.setContent(`<div class=\"stock-list\">${rows}</div>`);\n  }\n\n  public override destroy(): void {\n    if (this.refreshTimer) clearInterval(this.refreshTimer);\n    super.destroy();\n  }\n}\n```\n\n## Key Patterns\n\n1. **Constructor** calls `super()` with panel config, then triggers initial data fetch\n2. **fetchData()** is async, uses `isFetching` guard, shows error on failure with retry\n3. **render()** builds HTML strings and calls `this.setContent(html)`\n4. **destroy()** cleans up timers and event listeners\n5. Use `showLoading()` during initial load (auto-called in constructor)\n6. Use `showError(msg, retryFn)` on failure\n7. Sparkline SVGs use inline `<svg>` with `<polyline>` — see sparkline utility\n8. **Co-located helpers**: Define panel-specific pure utility functions (e.g. `timeAgo`, `statusIcon`) as module-level functions in the same `.ts` file as the panel class. Do **not** place them in `src/utils` unless they are reused by three or more panels.\n\n## Co-located Helper Pattern\n\nPanel-specific formatting and mapping utilities live directly above the class\ndefinition in the same file. This keeps the panel self-contained, avoids\npolluting `src/utils`, and makes the file easier to read and test in isolation.\n\n### When to co-locate\n\n| Situation | Decision |\n|-----------|----------|\n| Helper used only in this panel | ✅ Co-locate in panel file |\n| Helper used in 2 panels | ✅ Co-locate in the more \"owning\" panel; import from there |\n| Helper used in 3+ panels | ❌ Move to `src/utils/<helper>.ts` |\n| Helper depends on DOM or Panel API | ✅ Always co-locate (private method or module function) |\n\n### timeAgo — relative timestamp helper\n\n```typescript\n// co-located at the top of, e.g., src/components/CodeStatusPanel.ts\n\n/** Returns a human-readable relative time string, e.g. \"3 minutes ago\". */\nfunction timeAgo(dateStr: string): string {\n  const diff = Date.now() - new Date(dateStr).getTime();\n  const minutes = Math.floor(diff / 60_000);\n  if (minutes < 1)  return 'just now';\n  if (minutes < 60) return `${minutes} minute${minutes === 1 ? '' : 's'} ago`;\n  const hours = Math.floor(minutes / 60);\n  if (hours < 24)   return `${hours} hour${hours === 1 ? '' : 's'} ago`;\n  const days = Math.floor(hours / 24);\n  return `${days} day${days === 1 ? '' : 's'} ago`;\n}\n```\n\nUsage inside `render()`:\n\n```typescript\nprivate render(runs: WorkflowRun[]): void {\n  const rows = runs.map(r => `\n    <div class=\"run-row\">\n      <span class=\"run-name\">${r.name}</span>\n      <span class=\"run-time\">${timeAgo(r.updatedAt)}</span>\n    </div>\n  `).join('');\n  this.setContent(`<div class=\"run-list\">${rows}</div>`);\n}\n```\n\n### statusIcon — enum-to-icon mapping helper\n\nMaps a string enum (e.g. CI status) to an icon character or emoji. Define\nthe map as a `const` outside the function so it is allocated once.\n\n```typescript\n// co-located at the top of, e.g., src/components/CodeStatusPanel.ts\n\nconst STATUS_ICON: Record<string, string> = {\n  success:     '✅',\n  failure:     '❌',\n  cancelled:   '⛔',\n  skipped:     '⏭️',\n  in_progress: '🔄',\n  queued:      '⏳',\n};\n\n/** Returns an icon string for a workflow run conclusion/status value. */\nfunction statusIcon(status: string | null): string {\n  if (!status) return '❓';\n  return STATUS_ICON[status] ?? '❓';\n}\n```\n\nUsage inside `render()`:\n\n```typescript\nconst rows = runs.map(r => `\n  <div class=\"run-row ${r.conclusion ?? r.status}\">\n    <span class=\"run-status\">${statusIcon(r.conclusion ?? r.status)}</span>\n    <span class=\"run-name\">${r.name}</span>\n    <span class=\"run-time\">${timeAgo(r.updatedAt)}</span>\n  </div>\n`).join('');\n```\n\n### Full example: CodeStatusPanel with co-located helpers\n\n```typescript\nimport { Panel } from './Panel';\nimport { fetchWorkflowRuns, WorkflowRun } from '../services/code-status';\n\n// ── Co-located helpers ────────────────────────────────────────────────────────\n\nconst STATUS_ICON: Record<string, string> = {\n  success:     '✅',\n  failure:     '❌',\n  cancelled:   '⛔',\n  skipped:     '⏭️',\n  in_progress: '🔄',\n  queued:      '⏳',\n};\n\nfunction statusIcon(status: string | null): string {\n  if (!status) return '❓';\n  return STATUS_ICON[status] ?? '❓';\n}\n\nfunction timeAgo(dateStr: string): string {\n  const diff = Date.now() - new Date(dateStr).getTime();\n  const minutes = Math.floor(diff / 60_000);\n  if (minutes < 1)  return 'just now';\n  if (minutes < 60) return `${minutes} minute${minutes === 1 ? '' : 's'} ago`;\n  const hours = Math.floor(minutes / 60);\n  if (hours < 24)   return `${hours} hour${hours === 1 ? '' : 's'} ago`;\n  const days = Math.floor(hours / 24);\n  return `${days} day${days === 1 ? '' : 's'} ago`;\n}\n\n// ── Panel class ───────────────────────────────────────────────────────────────\n\nexport class CodeStatusPanel extends Panel {\n  private refreshTimer: ReturnType<typeof setInterval> | null = null;\n\n  constructor() {\n    super({ id: 'codeStatus', title: 'CI / CD Status', showCount: true });\n    this.fetchData();\n    this.refreshTimer = setInterval(() => this.fetchData(), 60_000);\n  }\n\n  private async fetchData(): Promise<void> {\n    if (this.isFetching) return;\n    this.setFetching(true);\n    try {\n      const runs = await fetchWorkflowRuns();\n      this.render(runs);\n      this.setCount(runs.length);\n      this.saveState();\n    } catch (err) {\n      this.showError(`Failed to load CI status: ${(err as Error).message}`, () => this.fetchData());\n    } finally {\n      this.setFetching(false);\n    }\n  }\n\n  private render(runs: WorkflowRun[]): void {\n    if (!runs.length) {\n      this.setContent('<p class=\"panel-empty\">No recent runs.</p>');\n      return;\n    }\n    const rows = runs.map(r => `\n      <div class=\"run-row ${r.conclusion ?? r.status}\">\n        <span class=\"run-icon\">${statusIcon(r.conclusion ?? r.status)}</span>\n        <span class=\"run-name\">${r.name}</span>\n        <span class=\"run-branch\">${r.headBranch}</span>\n        <span class=\"run-time\">${timeAgo(r.updatedAt)}</span>\n      </div>\n    `).join('');\n    this.setContent(`<div class=\"run-list\">${rows}</div>`);\n  }\n\n  public override destroy(): void {\n    if (this.refreshTimer) clearInterval(this.refreshTimer);\n    super.destroy();\n  }\n}\n```\n\n### Naming conventions for co-located helpers\n\n- Use **camelCase** function names (`timeAgo`, `statusIcon`, `formatBytes`).\n- Keep them **pure** (no side effects, no DOM access) so they are trivially unit-testable.\n- Place them **above** the class definition, below imports.\n- Prefix module-level constants that back a helper with the helper name in SCREAMING_SNAKE (`STATUS_ICON` for `statusIcon`).\n- Do **not** export them unless another file already needs them — keep the API surface minimal.\n\n## Sparkline Utility\n\n```typescript\nexport function miniSparkline(data: number[] | undefined, change: number | null, w = 50, h = 16): string {\n  if (!data || data.length < 2) return '';\n  const min = Math.min(...data);\n  const max = Math.max(...data);\n  const range = max - min || 1;\n  const color = change != null && change >= 0 ? 'var(--green)' : 'var(--red)';\n  const points = data.map((v, i) => {\n    const x = (i / (data.length - 1)) * w;\n    const y = h - ((v - min) / range) * (h - 2) - 1;\n    return `${x.toFixed(1)},${y.toFixed(1)}`;\n  }).join(' ');\n  return `<svg width=\"${w}\" height=\"${h}\" viewBox=\"0 0 ${w} ${h}\"><polyline points=\"${points}\" fill=\"none\" stroke=\"${color}\" stroke-width=\"1.2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>`;\n}\n```\n", "examples/localStorageUtils.ts": "*** End Files\n\n<EVOLUTION_COMPLETE>\n"}


skill_id: panel-component-enhanced__v2_d942ec85
name: panel-retry-localstorage
path: example/skills/panel-retry-localstorage/SKILL.md
category: workflow
visibility: private
lineage_origin: fixed
lineage_source_task_id: evolution-v2-task-03
lineage_created_at: 2026-03-10T18:24:30.547622
lineage_created_by: openrouter/anthropic/claude-sonnet-4.6
first_seen: 2026-03-10T18:24:30.547638
last_updated: 2026-03-10T19:15:54.402406
lineage_change_summary:
Added an explicit "Protected API Reference" section to SKILL.md listing all protected members and methods available to subclasses.

lineage_content_diff:
--- a/SKILL.md
+++ b/SKILL.md
@@ -255,6 +255,36 @@
 }
 ```
 
+## Protected API Reference
+
+All members and methods listed below are accessible from subclasses without importing or inspecting `Panel.ts` directly:
+
+| Member / Method | Type | Description |
+|---|---|---|
+| `element` | `HTMLElement` | Outer container div (`.panel`) |
+| `header` | `HTMLElement` | Header bar div (`.panel-header`) — append extra controls here |
+| `content` | `HTMLElement` | Content area div (`.panel-content`) |
+| `countEl` | `HTMLElement \| null` | Count badge span, or `null` if `showCount` was not set |
+| `panelId` | `string` | The `id` passed to `PanelOptions` |
+| `isFetching` | `boolean` (getter) | `true` while an async fetch is in progress |
+| `setFetching(v: boolean)` | `void` | Set the fetching guard flag |
+| `showLoading(message?)` | `void` | Replace content with a loading spinner |
+| `showError(message?, onRetry?)` | `void` | Replace content with an error state and optional retry button |
+| `setContent(html: string)` | `void` | Set raw HTML into the content area |
+| `setCount(count: number)` | `void` | Update the count badge text (no-op if `countEl` is null) |
+
+> **Example — appending a button to the header in a subclass:**
+> ```typescript
+> constructor() {
+>   super({ id: 'insights', title: 'Insights', className: 'panel-wide' });
+>   const btn = document.createElement('button');
+>   btn.className = 'panel-refresh-btn';
+>   btn.textContent = '↻';
+>   btn.addEventListener('click', () => this.generate());
+>   this.header.appendChild(btn);   // 'header' is protected — safe to use here
+> }
+> ```
+
 ## Key Patterns
 
 1. **Constructor** calls `super()` with panel config, then triggers initial data fetch

lineage_content_snapshot:
{"SKILL.md": "---\nname: panel-component-enhanced\ndescription: Enhanced dashboard panel component with robust error handling, retry logic, and localStorage persistence for state management.\n---\n\n# Panel Component Pattern\n\nCreate dashboard panel components using **vanilla TypeScript** (no framework, no JSX). Each panel is a class extending a `Panel` base class.\n\n## Architecture Overview\n\n### Enhanced Features\n- **Retry Logic**: Automatically retries failed data fetches with exponential backoff.\n- **State Persistence**: Saves panel state (expanded/collapsed, size) to localStorage.\n- **Detailed Error Handling**: Provides more granular error messages and recovery options.\n\n```\nPanel (base class)\n├── element: HTMLElement (outer container, .panel)\n│   ├── header: HTMLElement (.panel-header)\n│   │   ├── headerLeft (.panel-header-left)\n│   │   │   ├── title (.panel-title)\n│   │   │   └── newBadge (.panel-new-badge) [optional]\n│   │   ├── statusBadge (.panel-data-badge) [optional]\n│   │   └── countEl (.panel-count) [optional]\n│   ├── content: HTMLElement (.panel-content)\n│   └── resizeHandle (.panel-resize-handle)\n```\n\n## Base Panel Class (Simplified for this project)\n\n```typescript\nexport class Panel {\n  // ... existing code ...\n\n  private retryAttempts = 0;\n  private maxRetries = 3;\n  private retryDelay = 1000; // starts at 1s, doubles each retry\n\n  protected async fetchWithRetry(url: string): Promise<any> {\n    while (this.retryAttempts < this.maxRetries) {\n      try {\n        const response = await fetch(url);\n        if (!response.ok) throw new Error(`HTTP ${response.status}`);\n        return await response.json();\n      } catch (error) {\n        this.retryAttempts++;\n        if (this.retryAttempts >= this.maxRetries) {\n          throw new Error(`Failed after ${this.maxRetries} attempts: ${error.message}`);\n        }\n        await new Promise(resolve => setTimeout(resolve, this.retryDelay));\n        this.retryDelay *= 2;\n      }\n    }\n  }\n\n  public saveState(): void {\n    localStorage.setItem(`panelState_${this.panelId}`, JSON.stringify({\n      isExpanded: !this.element.classList.contains('collapsed'),\n      width: this.element.style.width,\n      height: this.element.style.height\n    }));\n  }\n\n  public loadState(): void {\n    const savedState = localStorage.getItem(`panelState_${this.panelId}`);\n    if (savedState) {\n      const { isExpanded, width, height } = JSON.parse(savedState);\n      if (!isExpanded) this.element.classList.add('collapsed');\n      if (width) this.element.style.width = width;\n      if (height) this.element.style.height = height;\n    }\n  }\n}\n```\n\nCreate `src/components/Panel.ts`:\n\n```typescript\nexport interface PanelOptions {\n  id: string;\n  title: string;\n  showCount?: boolean;\n  className?: string;\n}\n\nexport class Panel {\n  protected element: HTMLElement;\n  protected content: HTMLElement;\n  protected header: HTMLElement;\n  protected countEl: HTMLElement | null = null;\n  protected panelId: string;\n  private _fetching = false;\n\n  constructor(options: PanelOptions) {\n    this.panelId = options.id;\n    this.element = document.createElement('div');\n    this.element.className = `panel ${options.className || ''}`;\n    this.element.dataset.panel = options.id;\n\n    // Header\n    this.header = document.createElement('div');\n    this.header.className = 'panel-header';\n\n    const headerLeft = document.createElement('div');\n    headerLeft.className = 'panel-header-left';\n\n    const title = document.createElement('span');\n    title.className = 'panel-title';\n    title.textContent = options.title;\n    headerLeft.appendChild(title);\n    this.header.appendChild(headerLeft);\n\n    // Count badge (optional)\n    if (options.showCount) {\n      this.countEl = document.createElement('span');\n      this.countEl.className = 'panel-count';\n      this.countEl.textContent = '0';\n      this.header.appendChild(this.countEl);\n    }\n\n    // Content area\n    this.content = document.createElement('div');\n    this.content.className = 'panel-content';\n    this.content.id = `${options.id}Content`;\n\n    this.element.appendChild(this.header);\n    this.element.appendChild(this.content);\n    this.showLoading();\n  }\n\n  public getElement(): HTMLElement { return this.element; }\n\n  public showLoading(message = 'Loading...'): void {\n    this.content.innerHTML = `\n      <div class=\"panel-loading\">\n        <div class=\"panel-loading-spinner\"></div>\n        <div class=\"panel-loading-text\">${message}</div>\n      </div>`;\n  }\n\n  public showError(message = 'Failed to load', onRetry?: () => void): void {\n    this.content.innerHTML = `\n      <div class=\"panel-error-state\">\n        <div class=\"panel-error-msg\">${message}</div>\n        ${onRetry ? '<button class=\"panel-retry-btn\" data-panel-retry>Retry</button>' : ''}\n      </div>`;\n    if (onRetry) {\n      this.content.querySelector('[data-panel-retry]')?.addEventListener('click', onRetry);\n    }\n  }\n\n  public setContent(html: string): void {\n    this.content.innerHTML = html;\n  }\n\n  public setCount(count: number): void {\n    if (this.countEl) this.countEl.textContent = count.toString();\n  }\n\n  public show(): void { this.element.classList.remove('hidden'); }\n  public hide(): void { this.element.classList.add('hidden'); }\n\n  protected setFetching(v: boolean): void { this._fetching = v; }\n  protected get isFetching(): boolean { return this._fetching; }\n\n  public destroy(): void {\n    this.element.remove();\n  }\n}\n```\n\n## Creating a Concrete Panel (Example: StockPanel)\n\n```typescript\nexport class StockPanel extends Panel {\n  // ... existing code ...\n\n  private async fetchData(): Promise<void> {\n    if (this.isFetching) return;\n    this.setFetching(true);\n    try {\n      const quotes = await this.fetchWithRetry('/api/stocks');\n      this.render(quotes);\n      this.setCount(quotes.length);\n      this.saveState();\n    } catch (err) {\n      this.showError(`Failed to load stock data: ${err.message}`, () => {\n        this.retryAttempts = 0;\n        this.retryDelay = 1000;\n        this.fetchData();\n      });\n    } finally {\n      this.setFetching(false);\n    }\n  }\n}\n```\n\nEach panel extends `Panel` and manages its own data fetching + rendering:\n\n```typescript\nimport { Panel } from './Panel';\n\ninterface StockQuote {\n  symbol: string;\n  name: string;\n  price: number | null;\n  change: number | null;\n  sparkline?: number[];\n}\n\nexport class StockPanel extends Panel {\n  private refreshTimer: ReturnType<typeof setInterval> | null = null;\n\n  constructor() {\n    super({ id: 'stocks', title: 'Stock Market', showCount: true });\n    this.fetchData();\n    this.refreshTimer = setInterval(() => this.fetchData(), 60_000);\n  }\n\n  private async fetchData(): Promise<void> {\n    if (this.isFetching) return;\n    this.setFetching(true);\n    try {\n      const quotes = await fetchStockQuotes(); // from data-service\n      this.render(quotes);\n      this.setCount(quotes.length);\n    } catch (err) {\n      this.showError('Failed to load stock data', () => this.fetchData());\n    } finally {\n      this.setFetching(false);\n    }\n  }\n\n  private render(quotes: StockQuote[]): void {\n    const rows = quotes.map(q => `\n      <div class=\"stock-row\">\n        <span class=\"stock-symbol\">${q.symbol}</span>\n        <span class=\"stock-name\">${q.name}</span>\n        <span class=\"stock-price\">${q.price != null ? '$' + q.price.toFixed(2) : '—'}</span>\n        <span class=\"stock-change ${(q.change ?? 0) >= 0 ? 'positive' : 'negative'}\">\n          ${q.change != null ? (q.change >= 0 ? '+' : '') + q.change.toFixed(2) + '%' : '—'}\n        </span>\n      </div>\n    `).join('');\n\n    this.setContent(`<div class=\"stock-list\">${rows}</div>`);\n  }\n\n  public override destroy(): void {\n    if (this.refreshTimer) clearInterval(this.refreshTimer);\n    super.destroy();\n  }\n}\n```\n\n## Protected API Reference\n\nAll members and methods listed below are accessible from subclasses without importing or inspecting `Panel.ts` directly:\n\n| Member / Method | Type | Description |\n|---|---|---|\n| `element` | `HTMLElement` | Outer container div (`.panel`) |\n| `header` | `HTMLElement` | Header bar div (`.panel-header`) — append extra controls here |\n| `content` | `HTMLElement` | Content area div (`.panel-content`) |\n| `countEl` | `HTMLElement \\| null` | Count badge span, or `null` if `showCount` was not set |\n| `panelId` | `string` | The `id` passed to `PanelOptions` |\n| `isFetching` | `boolean` (getter) | `true` while an async fetch is in progress |\n| `setFetching(v: boolean)` | `void` | Set the fetching guard flag |\n| `showLoading(message?)` | `void` | Replace content with a loading spinner |\n| `showError(message?, onRetry?)` | `void` | Replace content with an error state and optional retry button |\n| `setContent(html: string)` | `void` | Set raw HTML into the content area |\n| `setCount(count: number)` | `void` | Update the count badge text (no-op if `countEl` is null) |\n\n> **Example — appending a button to the header in a subclass:**\n> ```typescript\n> constructor() {\n>   super({ id: 'insights', title: 'Insights', className: 'panel-wide' });\n>   const btn = document.createElement('button');\n>   btn.className = 'panel-refresh-btn';\n>   btn.textContent = '↻';\n>   btn.addEventListener('click', () => this.generate());\n>   this.header.appendChild(btn);   // 'header' is protected — safe to use here\n> }\n> ```\n\n## Key Patterns\n\n1. **Constructor** calls `super()` with panel config, then triggers initial data fetch\n2. **fetchData()** is async, uses `isFetching` guard, shows error on failure with retry\n3. **render()** builds HTML strings and calls `this.setContent(html)`\n4. **destroy()** cleans up timers and event listeners\n5. Use `showLoading()` during initial load (auto-called in constructor)\n6. Use `showError(msg, retryFn)` on failure\n7. Sparkline SVGs use inline `<svg>` with `<polyline>` — see sparkline utility\n\n## Sparkline Utility\n\n```typescript\nexport function miniSparkline(data: number[] | undefined, change: number | null, w = 50, h = 16): string {\n  if (!data || data.length < 2) return '';\n  const min = Math.min(...data);\n  const max = Math.max(...data);\n  const range = max - min || 1;\n  const color = change != null && change >= 0 ? 'var(--green)' : 'var(--red)';\n  const points = data.map((v, i) => {\n    const x = (i / (data.length - 1)) * w;\n    const y = h - ((v - min) / range) * (h - 2) - 1;\n    return `${x.toFixed(1)},${y.toFixed(1)}`;\n  }).join(' ');\n  return `<svg width=\"${w}\" height=\"${h}\" viewBox=\"0 0 ${w} ${h}\"><polyline points=\"${points}\" fill=\"none\" stroke=\"${color}\" stroke-width=\"1.2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>`;\n}\n```\n", "examples/localStorageUtils.ts": ""}


skill_id: data-service-enhanced_a157fb__v0_fec5a36e
name: data-service-circuit-breaker-aware
path: example/skills/data-service-circuit-breaker-aware/SKILL.md
category: workflow
visibility: private
lineage_origin: derived
lineage_source_task_id: evolution-v2-task-03
lineage_created_at: 2026-03-10T18:24:44.179370
lineage_created_by: openrouter/anthropic/claude-sonnet-4.6
first_seen: 2026-03-10T18:24:44.179389
last_updated: 2026-03-10T18:24:44.179390
lineage_change_summary:
Added a "When to Skip the Circuit Breaker" section that distinguishes user-triggered one-shot API calls from background auto-polling services, clarifying that only the latter requires circuit breaker protection.

lineage_content_diff:
--- a/SKILL.md
+++ b/SKILL.md
@@ -1,6 +1,6 @@
 ---
-name: data-service
-description: Create data fetching services with circuit breaker pattern for API resilience. Services handle fetch, cache, retry, and expose typed data to panel components.
+name: data-service-circuit-breaker-aware
+description: Create data fetching services with circuit breaker pattern for API resilience. Services handle fetch, cache, retry, and expose typed data to panel components. Includes clear guidance on when to skip the circuit breaker for user-triggered one-shot calls versus background auto-polling services.
 ---
 
 # Data Service Pattern
@@ -138,6 +138,94 @@
 5. **Services are stateless modules** — no class instances, just exported functions
 6. **API proxy**: Frontend calls `/api/...` which proxies to external APIs (hides API keys)
 
+## When to Skip the Circuit Breaker
+
+The circuit breaker is designed to protect **background auto-polling services** from cascading failures. It is **not always appropriate** — applying it blindly adds unnecessary complexity and can actively cause problems for user-driven flows.
+
+### Use the circuit breaker when:
+- The service is **polled automatically** on an interval (e.g., every 30 seconds, every minute)
+- Repeated failures would hammer a degraded API endpoint
+- Serving stale cached data is an acceptable fallback (the user won't notice a brief outage)
+- The data refreshes in the background without explicit user intent
+
+```typescript
+// ✅ Background polling — circuit breaker appropriate
+const breaker = createCircuitBreaker<StockQuote[]>({ name: 'StockMarket', cacheTtlMs: 60_000 });
+
+export async function fetchStockQuotes(symbols: string[]): Promise<StockQuote[]> {
+  return breaker.execute(async () => {
+    const resp = await fetch(`/api/stocks?symbols=${symbols.join(',')}`);
+    if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
+    return resp.json();
+  }, []);
+}
+
+// Called on a timer — circuit breaker protects against rapid failure storms
+setInterval(() => fetchStockQuotes(['AAPL', 'MSFT']), 60_000);
+```
+
+### Skip the circuit breaker when:
+- The call is **user-triggered** (button click, explicit refresh, form submit)
+- The operation is **one-shot** and non-repeating (e.g., LLM text generation, send message)
+- Failure should surface immediately to the user as an error — not silently return stale data
+- The user expects a **fresh result**, not a cache hit
+- Retry/error handling is managed by the UI layer (e.g., a "Try again" button)
+
+```typescript
+// ✅ User-triggered one-shot LLM call — NO circuit breaker
+export interface DailyBriefingData {
+  briefing: string;
+  generatedAt: string;
+}
+
+export async function generateDailyBriefing(context: string): Promise<DailyBriefingData> {
+  // No circuit breaker: this is explicitly triggered by the user clicking "Generate".
+  // Failures should throw so the UI can show an error and offer a retry button.
+  const resp = await fetch('/api/llm/generate', {
+    method: 'POST',
+    headers: { 'Content-Type': 'application/json' },
+    body: JSON.stringify({ prompt: context }),
+  });
+  if (!resp.ok) throw new Error(`LLM generation failed: HTTP ${resp.status}`);
+  const { text } = await resp.json();
+  return { briefing: text, generatedAt: new Date().toISOString() };
+}
+```
+
+### Decision table
+
+| Trigger              | Repeats automatically? | Stale data OK? | Use circuit breaker? |
+|----------------------|------------------------|----------------|----------------------|
+| Timer / interval     | ✅ Yes                 | ✅ Yes         | ✅ Yes               |
+| App startup fetch    | ❌ No                  | ✅ Yes         | Optional             |
+| Button / user action | ❌ No                  | ❌ No          | ❌ No                |
+| LLM generation       | ❌ No                  | ❌ No          | ❌ No                |
+| Explicit refresh     | ❌ No                  | ❌ No          | ❌ No                |
+
+### UI error handling for one-shot calls
+
+When skipping the circuit breaker, the calling component is responsible for surfacing errors:
+
+```typescript
+// In the panel/component that calls a one-shot service:
+async generate(): Promise<void> {
+  if (this.isFetching) return;
+  this.isFetching = true;
+  this.showLoading();
+  try {
+    const data = await generateDailyBriefing(this.buildContext());
+    this.render(data);
+  } catch (e) {
+    // Surface the error directly — no silent cache fallback
+    this.showError('Generation failed. Please try again.', () => this.generate());
+  } finally {
+    this.isFetching = false;
+  }
+}
+```
+
+> **Rule of thumb**: If a human is waiting for the result of a specific action they just took, skip the circuit breaker and let errors propagate to the UI. If a machine is polling in the background, use the circuit breaker.
+
 ## API Proxy Pattern
 
 For APIs requiring keys, create server-side proxy endpoints:
@@ -153,4 +241,6 @@
 ```
 
 This keeps API keys server-side and provides a single endpoint for the frontend.
-
+*** End Files
+
+<EVOLUTION_COMPLETE>

lineage_content_snapshot:
{"SKILL.md": "---\nname: data-service-circuit-breaker-aware\ndescription: Create data fetching services with circuit breaker pattern for API resilience. Services handle fetch, cache, retry, and expose typed data to panel components. Includes clear guidance on when to skip the circuit breaker for user-triggered one-shot calls versus background auto-polling services.\n---\n\n# Data Service Pattern\n\nEach panel's data comes from a dedicated **service module** in `src/services/`. Services handle API calls, caching, error handling with circuit breaker pattern.\n\n## Circuit Breaker\n\nThe circuit breaker prevents cascading failures when an API is down. After N consecutive failures, it enters \"cooldown\" mode and serves cached data instead of hitting the API.\n\nCreate `src/utils/circuit-breaker.ts`:\n\n```typescript\ninterface CircuitState {\n  failures: number;\n  cooldownUntil: number;\n}\n\ninterface CacheEntry<T> {\n  data: T;\n  timestamp: number;\n}\n\nexport interface CircuitBreakerOptions {\n  name: string;\n  maxFailures?: number;\n  cooldownMs?: number;\n  cacheTtlMs?: number;\n}\n\nexport class CircuitBreaker<T> {\n  private state: CircuitState = { failures: 0, cooldownUntil: 0 };\n  private cache: CacheEntry<T> | null = null;\n  private name: string;\n  private maxFailures: number;\n  private cooldownMs: number;\n  private cacheTtlMs: number;\n\n  constructor(options: CircuitBreakerOptions) {\n    this.name = options.name;\n    this.maxFailures = options.maxFailures ?? 2;\n    this.cooldownMs = options.cooldownMs ?? 5 * 60 * 1000;  // 5 minutes\n    this.cacheTtlMs = options.cacheTtlMs ?? 10 * 60 * 1000; // 10 minutes\n  }\n\n  isOnCooldown(): boolean {\n    if (Date.now() < this.state.cooldownUntil) return true;\n    if (this.state.cooldownUntil > 0) {\n      this.state = { failures: 0, cooldownUntil: 0 };\n    }\n    return false;\n  }\n\n  getCached(): T | null {\n    if (this.cache && Date.now() - this.cache.timestamp < this.cacheTtlMs) {\n      return this.cache.data;\n    }\n    return null;\n  }\n\n  recordSuccess(data: T): void {\n    this.state = { failures: 0, cooldownUntil: 0 };\n    this.cache = { data, timestamp: Date.now() };\n  }\n\n  recordFailure(error?: string): void {\n    this.state.failures++;\n    if (this.state.failures >= this.maxFailures) {\n      this.state.cooldownUntil = Date.now() + this.cooldownMs;\n      console.warn(`[${this.name}] Cooldown for ${this.cooldownMs / 1000}s`);\n    }\n  }\n\n  async execute<R extends T>(fn: () => Promise<R>, defaultValue: R): Promise<R> {\n    if (this.isOnCooldown()) {\n      const cached = this.getCached();\n      return (cached as R) ?? defaultValue;\n    }\n    const cached = this.getCached();\n    if (cached !== null) return cached as R;\n    try {\n      const result = await fn();\n      this.recordSuccess(result);\n      return result;\n    } catch (e) {\n      console.error(`[${this.name}] Failed:`, e);\n      this.recordFailure(String(e));\n      return defaultValue;\n    }\n  }\n}\n\nexport function createCircuitBreaker<T>(options: CircuitBreakerOptions): CircuitBreaker<T> {\n  return new CircuitBreaker<T>(options);\n}\n```\n\n## Service Module Pattern\n\nEach service is a TypeScript module that exports async data-fetching functions:\n\n```typescript\n// src/services/stock-market.ts\nimport { createCircuitBreaker } from '../utils/circuit-breaker';\n\nexport interface StockQuote {\n  symbol: string;\n  name: string;\n  price: number | null;\n  change: number | null;\n  sparkline?: number[];\n}\n\nconst breaker = createCircuitBreaker<StockQuote[]>({\n  name: 'StockMarket',\n  cacheTtlMs: 60_000, // 1 minute cache\n});\n\nexport async function fetchStockQuotes(symbols: string[]): Promise<StockQuote[]> {\n  return breaker.execute(async () => {\n    const params = new URLSearchParams({ symbols: symbols.join(',') });\n    const resp = await fetch(`/api/stocks?${params}`);\n    if (!resp.ok) throw new Error(`HTTP ${resp.status}`);\n    return resp.json();\n  }, []);\n}\n```\n\n## Key Patterns\n\n1. **One circuit breaker per API endpoint** — create at module level\n2. **Export typed interfaces** for data shapes\n3. **Wrap fetch calls** in `breaker.execute(fn, defaultValue)`\n4. **Default values** are empty arrays `[]` or empty objects `{}`\n5. **Services are stateless modules** — no class instances, just exported functions\n6. **API proxy**: Frontend calls `/api/...` which proxies to external APIs (hides API keys)\n\n## When to Skip the Circuit Breaker\n\nThe circuit breaker is designed to protect **background auto-polling services** from cascading failures. It is **not always appropriate** — applying it blindly adds unnecessary complexity and can actively cause problems for user-driven flows.\n\n### Use the circuit breaker when:\n- The service is **polled automatically** on an interval (e.g., every 30 seconds, every minute)\n- Repeated failures would hammer a degraded API endpoint\n- Serving stale cached data is an acceptable fallback (the user won't notice a brief outage)\n- The data refreshes in the background without explicit user intent\n\n```typescript\n// ✅ Background polling — circuit breaker appropriate\nconst breaker = createCircuitBreaker<StockQuote[]>({ name: 'StockMarket', cacheTtlMs: 60_000 });\n\nexport async function fetchStockQuotes(symbols: string[]): Promise<StockQuote[]> {\n  return breaker.execute(async () => {\n    const resp = await fetch(`/api/stocks?symbols=${symbols.join(',')}`);\n    if (!resp.ok) throw new Error(`HTTP ${resp.status}`);\n    return resp.json();\n  }, []);\n}\n\n// Called on a timer — circuit breaker protects against rapid failure storms\nsetInterval(() => fetchStockQuotes(['AAPL', 'MSFT']), 60_000);\n```\n\n### Skip the circuit breaker when:\n- The call is **user-triggered** (button click, explicit refresh, form submit)\n- The operation is **one-shot** and non-repeating (e.g., LLM text generation, send message)\n- Failure should surface immediately to the user as an error — not silently return stale data\n- The user expects a **fresh result**, not a cache hit\n- Retry/error handling is managed by the UI layer (e.g., a \"Try again\" button)\n\n```typescript\n// ✅ User-triggered one-shot LLM call — NO circuit breaker\nexport interface DailyBriefingData {\n  briefing: string;\n  generatedAt: string;\n}\n\nexport async function generateDailyBriefing(context: string): Promise<DailyBriefingData> {\n  // No circuit breaker: this is explicitly triggered by the user clicking \"Generate\".\n  // Failures should throw so the UI can show an error and offer a retry button.\n  const resp = await fetch('/api/llm/generate', {\n    method: 'POST',\n    headers: { 'Content-Type': 'application/json' },\n    body: JSON.stringify({ prompt: context }),\n  });\n  if (!resp.ok) throw new Error(`LLM generation failed: HTTP ${resp.status}`);\n  const { text } = await resp.json();\n  return { briefing: text, generatedAt: new Date().toISOString() };\n}\n```\n\n### Decision table\n\n| Trigger              | Repeats automatically? | Stale data OK? | Use circuit breaker? |\n|----------------------|------------------------|----------------|----------------------|\n| Timer / interval     | ✅ Yes                 | ✅ Yes         | ✅ Yes               |\n| App startup fetch    | ❌ No                  | ✅ Yes         | Optional             |\n| Button / user action | ❌ No                  | ❌ No          | ❌ No                |\n| LLM generation       | ❌ No                  | ❌ No          | ❌ No                |\n| Explicit refresh     | ❌ No                  | ❌ No          | ❌ No                |\n\n### UI error handling for one-shot calls\n\nWhen skipping the circuit breaker, the calling component is responsible for surfacing errors:\n\n```typescript\n// In the panel/component that calls a one-shot service:\nasync generate(): Promise<void> {\n  if (this.isFetching) return;\n  this.isFetching = true;\n  this.showLoading();\n  try {\n    const data = await generateDailyBriefing(this.buildContext());\n    this.render(data);\n  } catch (e) {\n    // Surface the error directly — no silent cache fallback\n    this.showError('Generation failed. Please try again.', () => this.generate());\n  } finally {\n    this.isFetching = false;\n  }\n}\n```\n\n> **Rule of thumb**: If a human is waiting for the result of a specific action they just took, skip the circuit breaker and let errors propagate to the UI. If a machine is polling in the background, use the circuit breaker.\n\n## API Proxy Pattern\n\nFor APIs requiring keys, create server-side proxy endpoints:\n\n```typescript\n// api/stocks.ts (Vercel serverless function)\nexport default async function handler(req, res) {\n  const { symbols } = req.query;\n  const apiKey = process.env.FINNHUB_API_KEY;\n  const data = await fetch(`https://finnhub.io/api/v1/quote?symbol=${symbols}&token=${apiKey}`);\n  res.json(await data.json());\n}\n```\n\nThis keeps API keys server-side and provides a single endpoint for the frontend.\n*** End Files\n\n<EVOLUTION_COMPLETE>\n"}


skill_id: panel-component-enhanced-enhanced_784b3f__v0_ccfb9e68
name: panel-component-aggregator
path: example/skills/panel-component-aggregator/SKILL.md
category: workflow
visibility: private
lineage_origin: derived
lineage_source_task_id: evolution-v2-task-03
lineage_created_at: 2026-03-10T18:26:06.492243
lineage_created_by: openrouter/anthropic/claude-sonnet-4.6
first_seen: 2026-03-10T18:26:06.492261
last_updated: 2026-03-10T18:26:06.492262
lineage_change_summary:
Extended the panel skill to cover aggregator/summary panels that receive pushed partial data updates via updateData() instead of fetching independently, with deferred first-render, incremental re-render, and staleness tracking.

lineage_content_diff:
--- a/SKILL.md
+++ b/SKILL.md
@@ -1,78 +1,58 @@
 ---
-name: panel-component-enhanced
-description: Enhanced dashboard panel component with robust error handling, retry logic, and localStorage persistence for state management.
----
-
-# Panel Component Pattern
-
-Create dashboard panel components using **vanilla TypeScript** (no framework, no JSX). Each panel is a class extending a `Panel` base class.
+name: panel-component-aggregator
+description: Dashboard panel component pattern for aggregator/summary panels that accept pushed partial data updates via updateData(), defer rendering until first data arrives, and incrementally re-render metrics without re-fetching — ideal for panels that synthesize data already fetched by other panels.
+---
+
+# Aggregator Panel Component Pattern
+
+Create **aggregator** dashboard panel components using **vanilla TypeScript** (no framework, no JSX).
+An aggregator panel does **not** fetch data itself. Instead, it receives partial data pushes from sibling panels (or a coordinator) via `updateData(partial)`, defers its first render until enough data has arrived, and re-renders incrementally on each subsequent push.
+
+This skill is self-contained. It covers the base `Panel` class, the `AggregatorPanel` generic base class, and a concrete example (`SummaryPanel`).
+
+---
+
+## When to Use This Pattern
+
+| Use case | Fetching Panel | Aggregator Panel |
+|---|---|---|
+| Panel owns its own API endpoint | yes | no |
+| Panel summarises data from multiple sources | no | yes |
+| Data arrives asynchronously from siblings | no | yes |
+| Re-renders on each push without extra network calls | no | yes |
+
+---
 
 ## Architecture Overview
 
-### Enhanced Features
-- **Retry Logic**: Automatically retries failed data fetches with exponential backoff.
-- **State Persistence**: Saves panel state (expanded/collapsed, size) to localStorage.
-- **Detailed Error Handling**: Provides more granular error messages and recovery options.
-
 ```
 Panel (base class)
-├── element: HTMLElement (outer container, .panel)
-│   ├── header: HTMLElement (.panel-header)
-│   │   ├── headerLeft (.panel-header-left)
-│   │   │   ├── title (.panel-title)
-│   │   │   └── newBadge (.panel-new-badge) [optional]
-│   │   ├── statusBadge (.panel-data-badge) [optional]
-│   │   └── countEl (.panel-count) [optional]
-│   ├── content: HTMLElement (.panel-content)
-│   └── resizeHandle (.panel-resize-handle)
-```
-
-## Base Panel Class (Simplified for this project)
-
-```typescript
-export class Panel {
-  // ... existing code ...
-
-  private retryAttempts = 0;
-  private maxRetries = 3;
-  private retryDelay = 1000; // starts at 1s, doubles each retry
-
-  protected async fetchWithRetry(url: string): Promise<any> {
-    while (this.retryAttempts < this.maxRetries) {
-      try {
-        const response = await fetch(url);
-        if (!response.ok) throw new Error(`HTTP ${response.status}`);
-        return await response.json();
-      } catch (error) {
-        this.retryAttempts++;
-        if (this.retryAttempts >= this.maxRetries) {
-          throw new Error(`Failed after ${this.maxRetries} attempts: ${error.message}`);
-        }
-        await new Promise(resolve => setTimeout(resolve, this.retryDelay));
-        this.retryDelay *= 2;
-      }
-    }
-  }
-
-  public saveState(): void {
-    localStorage.setItem(`panelState_${this.panelId}`, JSON.stringify({
-      isExpanded: !this.element.classList.contains('collapsed'),
-      width: this.element.style.width,
-      height: this.element.style.height
-    }));
-  }
-
-  public loadState(): void {
-    const savedState = localStorage.getItem(`panelState_${this.panelId}`);
-    if (savedState) {
-      const { isExpanded, width, height } = JSON.parse(savedState);
-      if (!isExpanded) this.element.classList.add('collapsed');
-      if (width) this.element.style.width = width;
-      if (height) this.element.style.height = height;
-    }
-  }
-}
-```
+└── AggregatorPanel<TData> (generic aggregator base)
+    ├── partialData: Partial<TData>         — accumulated pushed fields
+    ├── receivedKeys: Set<keyof TData>      — tracks which keys have arrived
+    ├── requiredKeys: (keyof TData)[]       — keys needed before first render
+    ├── lastUpdated: Map<keyof TData, number> — staleness timestamps
+    └── SummaryPanel (concrete example)
+```
+
+Data flow:
+
+```
+StockPanel ──pushes──► coordinator.push('stocks', quotes)
+EmailPanel ──pushes──► coordinator.push('emails', messages)
+                               │
+                    AggregatorPanel.updateData({ stocks, emails })
+                               │
+                    ┌──────────▼──────────┐
+                    │  enough data yet?   │
+                    │  no  → showPending  │
+                    │  yes → render()     │
+                    └─────────────────────┘
+```
+
+---
+
+## Base Panel Class
 
 Create `src/components/Panel.ts`:
 
@@ -95,7 +75,7 @@
   constructor(options: PanelOptions) {
     this.panelId = options.id;
     this.element = document.createElement('div');
-    this.element.className = `panel ${options.className || ''}`;
+    this.element.className = `panel ${options.className || ''}`.trim();
     this.element.dataset.panel = options.id;
 
     // Header
@@ -111,7 +91,6 @@
     headerLeft.appendChild(title);
     this.header.appendChild(headerLeft);
 
-    // Count badge (optional)
     if (options.showCount) {
       this.countEl = document.createElement('span');
       this.countEl.className = 'panel-count';
@@ -119,14 +98,12 @@
       this.header.appendChild(this.countEl);
     }
 
-    // Content area
     this.content = document.createElement('div');
     this.content.className = 'panel-content';
     this.content.id = `${options.id}Content`;
 
     this.element.appendChild(this.header);
     this.element.appendChild(this.content);
-    this.showLoading();
   }
 
   public getElement(): HTMLElement { return this.element; }
@@ -136,6 +113,14 @@
       <div class="panel-loading">
         <div class="panel-loading-spinner"></div>
         <div class="panel-loading-text">${message}</div>
+      </div>`;
+  }
+
+  /** Show a "waiting for data" placeholder — softer than showLoading */
+  public showPending(message = 'Waiting for data\u2026'): void {
+    this.content.innerHTML = `
+      <div class="panel-pending">
+        <div class="panel-pending-text">${message}</div>
       </div>`;
   }
 
@@ -146,13 +131,12 @@
         ${onRetry ? '<button class="panel-retry-btn" data-panel-retry>Retry</button>' : ''}
       </div>`;
     if (onRetry) {
-      this.content.querySelector('[data-panel-retry]')?.addEventListener('click', onRetry);
-    }
-  }
-
-  public setContent(html: string): void {
-    this.content.innerHTML = html;
-  }
+      this.content.querySelector('[data-panel-retry]')
+        ?.addEventListener('click', onRetry);
+    }
+  }
+
+  public setContent(html: string): void { this.content.innerHTML = html; }
 
   public setCount(count: number): void {
     if (this.countEl) this.countEl.textContent = count.toString();
@@ -164,151 +148,336 @@
   protected setFetching(v: boolean): void { this._fetching = v; }
   protected get isFetching(): boolean { return this._fetching; }
 
-  public destroy(): void {
-    this.element.remove();
-  }
-}
-```
-
-## Creating a Concrete Panel (Example: StockPanel)
+  public destroy(): void { this.element.remove(); }
+}
+```
+
+---
+
+## AggregatorPanel Base Class
+
+Create `src/components/AggregatorPanel.ts`:
 
 ```typescript
-export class StockPanel extends Panel {
-  // ... existing code ...
-
-  private async fetchData(): Promise<void> {
-    if (this.isFetching) return;
-    this.setFetching(true);
+import { Panel, PanelOptions } from './Panel';
+
+export interface AggregatorPanelOptions<TData> extends PanelOptions {
+  /**
+   * Keys that MUST be present before the first render is attempted.
+   * Until all required keys have been received at least once, the panel
+   * shows a pending placeholder.
+   */
+  requiredKeys: (keyof TData)[];
+
+  /**
+   * How old (ms) a field can be before it is considered stale and a
+   * staleness indicator is shown in the header. Defaults to 5 minutes.
+   */
+  staleThresholdMs?: number;
+}
+
+/**
+ * Base class for panels that aggregate data pushed from other panels
+ * rather than fetching independently.
+ *
+ * Usage:
+ *   1. Extend AggregatorPanel<YourDataShape>.
+ *   2. Implement render(data: YourDataShape): void.
+ *   3. Call updateData(partial) whenever a sibling pushes new data.
+ */
+export abstract class AggregatorPanel<TData extends Record<string, unknown>>
+  extends Panel {
+
+  private partialData: Partial<TData> = {};
+  private receivedKeys = new Set<keyof TData>();
+  private readonly requiredKeys: (keyof TData)[];
+  private readonly staleThresholdMs: number;
+  private lastUpdated = new Map<keyof TData, number>();
+  private hasRenderedOnce = false;
+
+  constructor(options: AggregatorPanelOptions<TData>) {
+    super(options);
+    this.requiredKeys = options.requiredKeys;
+    this.staleThresholdMs = options.staleThresholdMs ?? 5 * 60 * 1000;
+    // Show pending placeholder immediately — no spinner, no fetch
+    this.showPending(this.buildPendingMessage());
+  }
+
+  // ── Public API ──────────────────────────────────────────────────────────
+
+  /**
+   * Accept a partial data push. Merges incoming fields into accumulated
+   * state, timestamps each updated key, and either triggers the first
+   * render (if required keys are now satisfied) or re-renders incrementally.
+   */
+  public updateData(partial: Partial<TData>): void {
+    for (const key of Object.keys(partial) as (keyof TData)[]) {
+      this.partialData[key] = partial[key];
+      this.receivedKeys.add(key);
+      this.lastUpdated.set(key, Date.now());
+    }
+
+    if (!this.hasRequiredData()) {
+      this.showPending(this.buildPendingMessage());
+      return;
+    }
+
+    // All required keys present — safe to cast
     try {
-      const quotes = await this.fetchWithRetry('/api/stocks');
-      this.render(quotes);
-      this.setCount(quotes.length);
-      this.saveState();
+      this.render(this.partialData as TData);
+      this.hasRenderedOnce = true;
+      this.updateStalenessIndicators();
     } catch (err) {
-      this.showError(`Failed to load stock data: ${err.message}`, () => {
-        this.retryAttempts = 0;
-        this.retryDelay = 1000;
-        this.fetchData();
+      const message = err instanceof Error ? err.message : String(err);
+      this.showError(`Render error: ${message}`, () => {
+        this.updateData({});
       });
-    } finally {
-      this.setFetching(false);
-    }
-  }
-}
-```
-
-Each panel extends `Panel` and manages its own data fetching + rendering:
+    }
+  }
+
+  /** Returns true if every requiredKey has been received at least once. */
+  public hasRequiredData(): boolean {
+    return this.requiredKeys.every(k => this.receivedKeys.has(k));
+  }
+
+  /** Returns the set of required keys that have not yet been received. */
+  public getMissingKeys(): (keyof TData)[] {
+    return this.requiredKeys.filter(k => !this.receivedKeys.has(k));
+  }
+
+  /** Returns true if a specific key's data is older than staleThresholdMs. */
+  public isStale(key: keyof TData): boolean {
+    const ts = this.lastUpdated.get(key);
+    if (ts == null) return false;
+    return Date.now() - ts > this.staleThresholdMs;
+  }
+
+  /**
+   * Force-clears accumulated data and reverts to pending state.
+   * Useful when the data source resets (e.g. user logs out).
+   */
+  public reset(): void {
+    this.partialData = {};
+    this.receivedKeys.clear();
+    this.lastUpdated.clear();
+    this.hasRenderedOnce = false;
+    this.showPending(this.buildPendingMessage());
+  }
+
+  // ── Protected helpers ───────────────────────────────────────────────────
+
+  /**
+   * Subclasses MUST implement this. Called every time updateData() receives
+   * a push after required keys are satisfied. `data` is guaranteed to
+   * contain all requiredKeys.
+   */
+  protected abstract render(data: TData): void;
+
+  /**
+   * Returns the accumulated partial snapshot. Useful inside render() for
+   * optional (non-required) keys.
+   */
+  protected getSnapshot(): Partial<TData> {
+    return { ...this.partialData };
+  }
+
+  /**
+   * True on the first render call, false on subsequent updates.
+   * Use to decide between full DOM replacement vs. in-place patch.
+   */
+  protected get isFirstRender(): boolean {
+    return !this.hasRenderedOnce;
+  }
+
+  // ── Private helpers ─────────────────────────────────────────────────────
+
+  private buildPendingMessage(): string {
+    const missing = this.getMissingKeys() as string[];
+    if (missing.length === 0) return 'Processing\u2026';
+    return `Waiting for: ${missing.join(', ')}`;
+  }
+
+  private updateStalenessIndicators(): void {
+    const staleKeys = this.requiredKeys.filter(k => this.isStale(k));
+    const indicator = this.element.querySelector('.panel-stale-indicator');
+    if (staleKeys.length > 0) {
+      const msg = `Stale data: ${(staleKeys as string[]).join(', ')}`;
+      if (indicator) {
+        indicator.textContent = msg;
+      } else {
+        const el = document.createElement('div');
+        el.className = 'panel-stale-indicator';
+        el.textContent = msg;
+        this.header.appendChild(el);
+      }
+    } else {
+      indicator?.remove();
+    }
+  }
+}
+```
+
+---
+
+## Concrete Example: SummaryPanel
+
+Create `src/components/SummaryPanel.ts`:
 
 ```typescript
-import { Panel } from './Panel';
-
-interface StockQuote {
-  symbol: string;
-  name: string;
-  price: number | null;
-  change: number | null;
-  sparkline?: number[];
-}
-
-export class StockPanel extends Panel {
-  private refreshTimer: ReturnType<typeof setInterval> | null = null;
-
+import { AggregatorPanel } from './AggregatorPanel';
+import { StockQuote } from '../services/stock-service';
+import { EmailMessage } from '../services/email-service';
+import { CalendarEvent } from '../services/calendar-service';
+
+interface SummaryData {
+  stocks: StockQuote[];
+  emails: EmailMessage[];
+  events: CalendarEvent[];
+}
+
+export class SummaryPanel extends AggregatorPanel<SummaryData> {
   constructor() {
-    super({ id: 'stocks', title: 'Stock Market', showCount: true });
-    this.fetchData();
-    this.refreshTimer = setInterval(() => this.fetchData(), 60_000);
-  }
-
-  private async fetchData(): Promise<void> {
-    if (this.isFetching) return;
-    this.setFetching(true);
-    try {
-      const quotes = await fetchStockQuotes(); // from data-service
-      this.render(quotes);
-      this.setCount(quotes.length);
-    } catch (err) {
-      this.showError('Failed to load stock data', () => this.fetchData());
-    } finally {
-      this.setFetching(false);
-    }
-  }
-
-  private render(quotes: StockQuote[]): void {
-    const rows = quotes.map(q => `
-      <div class="stock-row">
-        <span class="stock-symbol">${q.symbol}</span>
-        <span class="stock-name">${q.name}</span>
-        <span class="stock-price">${q.price != null ? '$' + q.price.toFixed(2) : '—'}</span>
-        <span class="stock-change ${(q.change ?? 0) >= 0 ? 'positive' : 'negative'}">
-          ${q.change != null ? (q.change >= 0 ? '+' : '') + q.change.toFixed(2) + '%' : '—'}
-        </span>
-      </div>
-    `).join('');
-
-    this.setContent(`<div class="stock-list">${rows}</div>`);
-  }
-
-  public override destroy(): void {
-    if (this.refreshTimer) clearInterval(this.refreshTimer);
-    super.destroy();
-  }
-}
-```
-
-## Protected API Reference
-
-All members and methods listed below are accessible from subclasses without importing or inspecting `Panel.ts` directly:
-
-| Member / Method | Type | Description |
+    super({
+      id: 'summary',
+      title: 'Daily Summary',
+      className: 'panel-wide',
+      showCount: true,
+      // Render as soon as stocks + emails arrive; events are optional bonus
+      requiredKeys: ['stocks', 'emails'],
+      staleThresholdMs: 3 * 60 * 1000,
+    });
+  }
+
+  protected render(data: SummaryData): void {
+    const snapshot = this.getSnapshot();
+
+    const unread  = data.emails.filter(e => !e.read).length;
+    const gainers = data.stocks.filter(q => (q.change ?? 0) > 0).length;
+    const losers  = data.stocks.filter(q => (q.change ?? 0) < 0).length;
+
+    // Optional key — may not have arrived yet
+    const upcomingCount = snapshot.events
+      ? snapshot.events.filter(ev => new Date(ev.start) > new Date()).length
+      : null;
+
+    const eventsHtml = upcomingCount != null
+      ? `<div class="summary-metric" data-metric="events">
+           <span class="summary-metric-label">Upcoming events</span>
+           <span class="summary-metric-value">${upcomingCount}</span>
+         </div>`
+      : '';
+
+    if (this.isFirstRender) {
+      // Full DOM build on first render
+      this.setContent(`
+        <div class="summary-metrics">
+          <div class="summary-metric" data-metric="unread">
+            <span class="summary-metric-label">Unread emails</span>
+            <span class="summary-metric-value">${unread}</span>
+          </div>
+          <div class="summary-metric" data-metric="gainers">
+            <span class="summary-metric-label">Stocks up</span>
+            <span class="summary-metric-value positive">${gainers}</span>
+          </div>
+          <div class="summary-metric" data-metric="losers">
+            <span class="summary-metric-label">Stocks down</span>
+            <span class="summary-metric-value negative">${losers}</span>
+          </div>
+          ${eventsHtml}
+        </div>
+      `);
+    } else {
+      // Incremental patch — avoid full DOM thrash on subsequent pushes
+      this.patchMetric('unread', unread);
+      this.patchMetric('gainers', gainers);
+      this.patchMetric('losers', losers);
+      if (upcomingCount != null) this.patchMetric('events', upcomingCount);
+    }
+
+    this.setCount(data.emails.length + data.stocks.length);
+  }
+
+  /** Update a single metric value in-place */
+  private patchMetric(name: string, value: number): void {
+    const el = this.content.querySelector(
+      `[data-metric="${name}"] .summary-metric-value`
+    );
+    if (el) el.textContent = String(value);
+  }
+}
+```
+
+---
+
+## Coordinator / Push Pattern
+
+Create `src/services/panel-coordinator.ts`:
+
+```typescript
+import { SummaryPanel } from '../components/SummaryPanel';
+
+/**
+ * Lightweight pub/sub coordinator. Fetching panels call coordinator.push()
+ * after each successful render; aggregator panels receive those updates.
+ */
+export class PanelCoordinator {
+  private summaryPanel: SummaryPanel;
+
+  constructor(summaryPanel: SummaryPanel) {
+    this.summaryPanel = summaryPanel;
+  }
+
+  push<K extends 'stocks' | 'emails' | 'events'>(
+    key: K,
+    value: unknown
+  ): void {
+    this.summaryPanel.updateData({ [key]: value } as any);
+  }
+}
+```
+
+In your fetching panels, call `coordinator.push()` after each successful render:
+
+```typescript
+// Inside StockPanel.fetchData(), after this.render(quotes):
+coordinator.push('stocks', quotes);
+
+// Inside EmailPanel.fetchData(), after this.render(messages):
+coordinator.push('emails', messages);
+```
+
+---
+
+## Key Patterns
+
+1. **Constructor** calls `super()` with `requiredKeys` — the minimum fields needed before the first render.
+2. **updateData(partial)** is the only public ingestion point. It merges, timestamps, and either shows a pending placeholder or triggers `render()`.
+3. **render(data)** receives a fully-typed snapshot; use `this.getSnapshot()` for optional keys.
+4. **isFirstRender** lets you choose between a full DOM replace (first time) vs. incremental in-place patch (subsequent pushes) to avoid unnecessary reflow.
+5. **Staleness indicators** are automatically injected into the header when a key's last-update timestamp exceeds `staleThresholdMs`.
+6. **reset()** clears all state and reverts to pending — call it on session logout or data source disconnect.
+7. Aggregator panels never call `fetch()` or `showLoading()` — they call `showPending()` until required data arrives.
+8. The coordinator is a thin pub/sub shim; replace with EventEmitter, RxJS Subject, or your app's store if available.
+
+---
+
+## Aggregator vs Fetching Panel Checklist
+
+| Concern | Fetching Panel | Aggregator Panel |
 |---|---|---|
-| `element` | `HTMLElement` | Outer container div (`.panel`) |
-| `header` | `HTMLElement` | Header bar div (`.panel-header`) — append extra controls here |
-| `content` | `HTMLElement` | Content area div (`.panel-content`) |
-| `countEl` | `HTMLElement \| null` | Count badge span, or `null` if `showCount` was not set |
-| `panelId` | `string` | The `id` passed to `PanelOptions` |
-| `isFetching` | `boolean` (getter) | `true` while an async fetch is in progress |
-| `setFetching(v: boolean)` | `void` | Set the fetching guard flag |
-| `showLoading(message?)` | `void` | Replace content with a loading spinner |
-| `showError(message?, onRetry?)` | `void` | Replace content with an error state and optional retry button |
-| `setContent(html: string)` | `void` | Set raw HTML into the content area |
-| `setCount(count: number)` | `void` | Update the count badge text (no-op if `countEl` is null) |
-
-> **Example — appending a button to the header in a subclass:**
-> ```typescript
-> constructor() {
->   super({ id: 'insights', title: 'Insights', className: 'panel-wide' });
->   const btn = document.createElement('button');
->   btn.className = 'panel-refresh-btn';
->   btn.textContent = '↻';
->   btn.addEventListener('click', () => this.generate());
->   this.header.appendChild(btn);   // 'header' is protected — safe to use here
-> }
-> ```
-
-## Key Patterns
-
-1. **Constructor** calls `super()` with panel config, then triggers initial data fetch
-2. **fetchData()** is async, uses `isFetching` guard, shows error on failure with retry
-3. **render()** builds HTML strings and calls `this.setContent(html)`
-4. **destroy()** cleans up timers and event listeners
-5. Use `showLoading()` during initial load (auto-called in constructor)
-6. Use `showError(msg, retryFn)` on failure
-7. Sparkline SVGs use inline `<svg>` with `<polyline>` — see sparkline utility
-
-## Sparkline Utility
-
-```typescript
-export function miniSparkline(data: number[] | undefined, change: number | null, w = 50, h = 16): string {
-  if (!data || data.length < 2) return '';
-  const min = Math.min(...data);
-  const max = Math.max(...data);
-  const range = max - min || 1;
-  const color = change != null && change >= 0 ? 'var(--green)' : 'var(--red)';
-  const points = data.map((v, i) => {
-    const x = (i / (data.length - 1)) * w;
-    const y = h - ((v - min) / range) * (h - 2) - 1;
-    return `${x.toFixed(1)},${y.toFixed(1)}`;
-  }).join(' ');
-  return `<svg width="${w}" height="${h}" viewBox="0 0 ${w} ${h}"><polyline points="${points}" fill="none" stroke="${color}" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
-}
-```
+| Network call | `fetchWithRetry(url)` | none |
+| Initial state | `showLoading()` in constructor | `showPending()` in constructor |
+| Data ingestion | internal async fetch | `updateData(partial)` |
+| First render gate | none | `hasRequiredData()` |
+| Re-render strategy | full replace | incremental patch preferred |
+| Staleness | timer-based refetch | automatic header indicator |
+| `destroy()` | clear interval + super | super only (no timers to clear) |
+
+---
+
+## TypeScript Strict-Mode Notes
+
+- `TData extends Record<string, unknown>` ensures key iteration is safe.
+- `Object.keys(partial) as (keyof TData)[]` is safe because `partial` is `Partial<TData>`.
+- The `as TData` cast in `this.render(this.partialData as TData)` is guarded by `hasRequiredData()` — a runtime guarantee backing the compile-time assertion.
+- If your project enables `exactOptionalPropertyTypes`, declare optional aggregated sources as `key?: T | undefined` in `TData`.

--- a/examples/SummaryPanel.usage.ts
+++ b/examples/SummaryPanel.usage.ts
@@ -0,0 +1,56 @@
+```typescript
+/**
+ * Example: wiring SummaryPanel into a dashboard.
+ *
+ * SummaryPanel renders as soon as 'stocks' and 'emails' have been pushed
+ * at least once. 'events' is optional and enriches the display when it
+ * arrives.
+ */
+import { PanelCoordinator } from '../src/services/panel-coordinator';
+import { SummaryPanel } from '../src/components/SummaryPanel';
+import { StockPanel } from '../src/components/StockPanel';
+import { EmailPanel } from '../src/components/EmailPanel';
+import { CalendarPanel } from '../src/components/CalendarPanel';
+import {
+  saveAggregatorSnapshot,
+  loadAggregatorSnapshot,
+} from './localStorageUtils';
+
+const summaryPanel = new SummaryPanel();
+const coordinator  = new PanelCoordinator(summaryPanel);
+
+// Optional: pre-populate from localStorage on page load
+const cached = loadAggregatorSnapshot<{
+  stocks: unknown;
+  emails: unknown;
+  events: unknown;
+}>('summary');
+if (cached) {
+  summaryPanel.updateData(cached);
+}
+
+// Wire fetching panels to push their data after each successful render
+const stockPanel    = new StockPanel(data  => coordinator.push('stocks', data));
+const emailPanel    = new EmailPanel(data  => coordinator.push('emails', data));
+const calendarPanel = new CalendarPanel(data => coordinator.push('events', data));
+
+// Save snapshot after each update so the next page load pre-populates faster
+const origUpdate = summaryPanel.updateData.bind(summaryPanel);
+summaryPanel.updateData = (partial) => {
+  origUpdate(partial);
+  if (summaryPanel.hasRequiredData()) {
+    saveAggregatorSnapshot('summary', partial);
+  }
+};
+
+// Mount to DOM
+document.getElementById('dashboard')!.append(
+  stockPanel.getElement(),
+  emailPanel.getElement(),
+  calendarPanel.getElement(),
+  summaryPanel.getElement(),
+);
+```
+*** End Files
+
+<EVOLUTION_COMPLETE>

--- a/examples/localStorageUtils.ts
+++ b/examples/localStorageUtils.ts
@@ -0,0 +1,39 @@
+```typescript
+// Utility: persist aggregator panel's last-received snapshot to localStorage
+// so the panel can pre-populate on page reload before sibling panels re-push.
+
+export function saveAggregatorSnapshot<T>(
+  panelId: string,
+  data: Partial<T>
+): void {
+  try {
+    localStorage.setItem(
+      `aggregatorSnapshot_${panelId}`,
+      JSON.stringify({ data, savedAt: Date.now() })
+    );
+  } catch {
+    // Quota exceeded or private browsing — silently ignore
+  }
+}
+
+export function loadAggregatorSnapshot<T>(
+  panelId: string,
+  maxAgeMs = 10 * 60 * 1000
+): Partial<T> | null {
+  try {
+    const raw = localStorage.getItem(`aggregatorSnapshot_${panelId}`);
+    if (!raw) return null;
+    const { data, savedAt } = JSON.parse(raw) as {
+      data: Partial<T>;
+      savedAt: number;
+    };
+    if (Date.now() - savedAt > maxAgeMs) {
+      localStorage.removeItem(`aggregatorSnapshot_${panelId}`);
+      return null;
+    }
+    return data;
+  } catch {
+    return null;
+  }
+}
+```

lineage_content_snapshot:
{"SKILL.md": "---\nname: panel-component-aggregator\ndescription: Dashboard panel component pattern for aggregator/summary panels that accept pushed partial data updates via updateData(), defer rendering until first data arrives, and incrementally re-render metrics without re-fetching — ideal for panels that synthesize data already fetched by other panels.\n---\n\n# Aggregator Panel Component Pattern\n\nCreate **aggregator** dashboard panel components using **vanilla TypeScript** (no framework, no JSX).\nAn aggregator panel does **not** fetch data itself. Instead, it receives partial data pushes from sibling panels (or a coordinator) via `updateData(partial)`, defers its first render until enough data has arrived, and re-renders incrementally on each subsequent push.\n\nThis skill is self-contained. It covers the base `Panel` class, the `AggregatorPanel` generic base class, and a concrete example (`SummaryPanel`).\n\n---\n\n## When to Use This Pattern\n\n| Use case | Fetching Panel | Aggregator Panel |\n|---|---|---|\n| Panel owns its own API endpoint | yes | no |\n| Panel summarises data from multiple sources | no | yes |\n| Data arrives asynchronously from siblings | no | yes |\n| Re-renders on each push without extra network calls | no | yes |\n\n---\n\n## Architecture Overview\n\n```\nPanel (base class)\n└── AggregatorPanel<TData> (generic aggregator base)\n    ├── partialData: Partial<TData>         — accumulated pushed fields\n    ├── receivedKeys: Set<keyof TData>      — tracks which keys have arrived\n    ├── requiredKeys: (keyof TData)[]       — keys needed before first render\n    ├── lastUpdated: Map<keyof TData, number> — staleness timestamps\n    └── SummaryPanel (concrete example)\n```\n\nData flow:\n\n```\nStockPanel ──pushes──► coordinator.push('stocks', quotes)\nEmailPanel ──pushes──► coordinator.push('emails', messages)\n                               │\n                    AggregatorPanel.updateData({ stocks, emails })\n                               │\n                    ┌──────────▼──────────┐\n                    │  enough data yet?   │\n                    │  no  → showPending  │\n                    │  yes → render()     │\n                    └─────────────────────┘\n```\n\n---\n\n## Base Panel Class\n\nCreate `src/components/Panel.ts`:\n\n```typescript\nexport interface PanelOptions {\n  id: string;\n  title: string;\n  showCount?: boolean;\n  className?: string;\n}\n\nexport class Panel {\n  protected element: HTMLElement;\n  protected content: HTMLElement;\n  protected header: HTMLElement;\n  protected countEl: HTMLElement | null = null;\n  protected panelId: string;\n  private _fetching = false;\n\n  constructor(options: PanelOptions) {\n    this.panelId = options.id;\n    this.element = document.createElement('div');\n    this.element.className = `panel ${options.className || ''}`.trim();\n    this.element.dataset.panel = options.id;\n\n    // Header\n    this.header = document.createElement('div');\n    this.header.className = 'panel-header';\n\n    const headerLeft = document.createElement('div');\n    headerLeft.className = 'panel-header-left';\n\n    const title = document.createElement('span');\n    title.className = 'panel-title';\n    title.textContent = options.title;\n    headerLeft.appendChild(title);\n    this.header.appendChild(headerLeft);\n\n    if (options.showCount) {\n      this.countEl = document.createElement('span');\n      this.countEl.className = 'panel-count';\n      this.countEl.textContent = '0';\n      this.header.appendChild(this.countEl);\n    }\n\n    this.content = document.createElement('div');\n    this.content.className = 'panel-content';\n    this.content.id = `${options.id}Content`;\n\n    this.element.appendChild(this.header);\n    this.element.appendChild(this.content);\n  }\n\n  public getElement(): HTMLElement { return this.element; }\n\n  public showLoading(message = 'Loading...'): void {\n    this.content.innerHTML = `\n      <div class=\"panel-loading\">\n        <div class=\"panel-loading-spinner\"></div>\n        <div class=\"panel-loading-text\">${message}</div>\n      </div>`;\n  }\n\n  /** Show a \"waiting for data\" placeholder — softer than showLoading */\n  public showPending(message = 'Waiting for data\\u2026'): void {\n    this.content.innerHTML = `\n      <div class=\"panel-pending\">\n        <div class=\"panel-pending-text\">${message}</div>\n      </div>`;\n  }\n\n  public showError(message = 'Failed to load', onRetry?: () => void): void {\n    this.content.innerHTML = `\n      <div class=\"panel-error-state\">\n        <div class=\"panel-error-msg\">${message}</div>\n        ${onRetry ? '<button class=\"panel-retry-btn\" data-panel-retry>Retry</button>' : ''}\n      </div>`;\n    if (onRetry) {\n      this.content.querySelector('[data-panel-retry]')\n        ?.addEventListener('click', onRetry);\n    }\n  }\n\n  public setContent(html: string): void { this.content.innerHTML = html; }\n\n  public setCount(count: number): void {\n    if (this.countEl) this.countEl.textContent = count.toString();\n  }\n\n  public show(): void { this.element.classList.remove('hidden'); }\n  public hide(): void { this.element.classList.add('hidden'); }\n\n  protected setFetching(v: boolean): void { this._fetching = v; }\n  protected get isFetching(): boolean { return this._fetching; }\n\n  public destroy(): void { this.element.remove(); }\n}\n```\n\n---\n\n## AggregatorPanel Base Class\n\nCreate `src/components/AggregatorPanel.ts`:\n\n```typescript\nimport { Panel, PanelOptions } from './Panel';\n\nexport interface AggregatorPanelOptions<TData> extends PanelOptions {\n  /**\n   * Keys that MUST be present before the first render is attempted.\n   * Until all required keys have been received at least once, the panel\n   * shows a pending placeholder.\n   */\n  requiredKeys: (keyof TData)[];\n\n  /**\n   * How old (ms) a field can be before it is considered stale and a\n   * staleness indicator is shown in the header. Defaults to 5 minutes.\n   */\n  staleThresholdMs?: number;\n}\n\n/**\n * Base class for panels that aggregate data pushed from other panels\n * rather than fetching independently.\n *\n * Usage:\n *   1. Extend AggregatorPanel<YourDataShape>.\n *   2. Implement render(data: YourDataShape): void.\n *   3. Call updateData(partial) whenever a sibling pushes new data.\n */\nexport abstract class AggregatorPanel<TData extends Record<string, unknown>>\n  extends Panel {\n\n  private partialData: Partial<TData> = {};\n  private receivedKeys = new Set<keyof TData>();\n  private readonly requiredKeys: (keyof TData)[];\n  private readonly staleThresholdMs: number;\n  private lastUpdated = new Map<keyof TData, number>();\n  private hasRenderedOnce = false;\n\n  constructor(options: AggregatorPanelOptions<TData>) {\n    super(options);\n    this.requiredKeys = options.requiredKeys;\n    this.staleThresholdMs = options.staleThresholdMs ?? 5 * 60 * 1000;\n    // Show pending placeholder immediately — no spinner, no fetch\n    this.showPending(this.buildPendingMessage());\n  }\n\n  // ── Public API ──────────────────────────────────────────────────────────\n\n  /**\n   * Accept a partial data push. Merges incoming fields into accumulated\n   * state, timestamps each updated key, and either triggers the first\n   * render (if required keys are now satisfied) or re-renders incrementally.\n   */\n  public updateData(partial: Partial<TData>): void {\n    for (const key of Object.keys(partial) as (keyof TData)[]) {\n      this.partialData[key] = partial[key];\n      this.receivedKeys.add(key);\n      this.lastUpdated.set(key, Date.now());\n    }\n\n    if (!this.hasRequiredData()) {\n      this.showPending(this.buildPendingMessage());\n      return;\n    }\n\n    // All required keys present — safe to cast\n    try {\n      this.render(this.partialData as TData);\n      this.hasRenderedOnce = true;\n      this.updateStalenessIndicators();\n    } catch (err) {\n      const message = err instanceof Error ? err.message : String(err);\n      this.showError(`Render error: ${message}`, () => {\n        this.updateData({});\n      });\n    }\n  }\n\n  /** Returns true if every requiredKey has been received at least once. */\n  public hasRequiredData(): boolean {\n    return this.requiredKeys.every(k => this.receivedKeys.has(k));\n  }\n\n  /** Returns the set of required keys that have not yet been received. */\n  public getMissingKeys(): (keyof TData)[] {\n    return this.requiredKeys.filter(k => !this.receivedKeys.has(k));\n  }\n\n  /** Returns true if a specific key's data is older than staleThresholdMs. */\n  public isStale(key: keyof TData): boolean {\n    const ts = this.lastUpdated.get(key);\n    if (ts == null) return false;\n    return Date.now() - ts > this.staleThresholdMs;\n  }\n\n  /**\n   * Force-clears accumulated data and reverts to pending state.\n   * Useful when the data source resets (e.g. user logs out).\n   */\n  public reset(): void {\n    this.partialData = {};\n    this.receivedKeys.clear();\n    this.lastUpdated.clear();\n    this.hasRenderedOnce = false;\n    this.showPending(this.buildPendingMessage());\n  }\n\n  // ── Protected helpers ───────────────────────────────────────────────────\n\n  /**\n   * Subclasses MUST implement this. Called every time updateData() receives\n   * a push after required keys are satisfied. `data` is guaranteed to\n   * contain all requiredKeys.\n   */\n  protected abstract render(data: TData): void;\n\n  /**\n   * Returns the accumulated partial snapshot. Useful inside render() for\n   * optional (non-required) keys.\n   */\n  protected getSnapshot(): Partial<TData> {\n    return { ...this.partialData };\n  }\n\n  /**\n   * True on the first render call, false on subsequent updates.\n   * Use to decide between full DOM replacement vs. in-place patch.\n   */\n  protected get isFirstRender(): boolean {\n    return !this.hasRenderedOnce;\n  }\n\n  // ── Private helpers ─────────────────────────────────────────────────────\n\n  private buildPendingMessage(): string {\n    const missing = this.getMissingKeys() as string[];\n    if (missing.length === 0) return 'Processing\\u2026';\n    return `Waiting for: ${missing.join(', ')}`;\n  }\n\n  private updateStalenessIndicators(): void {\n    const staleKeys = this.requiredKeys.filter(k => this.isStale(k));\n    const indicator = this.element.querySelector('.panel-stale-indicator');\n    if (staleKeys.length > 0) {\n      const msg = `Stale data: ${(staleKeys as string[]).join(', ')}`;\n      if (indicator) {\n        indicator.textContent = msg;\n      } else {\n        const el = document.createElement('div');\n        el.className = 'panel-stale-indicator';\n        el.textContent = msg;\n        this.header.appendChild(el);\n      }\n    } else {\n      indicator?.remove();\n    }\n  }\n}\n```\n\n---\n\n## Concrete Example: SummaryPanel\n\nCreate `src/components/SummaryPanel.ts`:\n\n```typescript\nimport { AggregatorPanel } from './AggregatorPanel';\nimport { StockQuote } from '../services/stock-service';\nimport { EmailMessage } from '../services/email-service';\nimport { CalendarEvent } from '../services/calendar-service';\n\ninterface SummaryData {\n  stocks: StockQuote[];\n  emails: EmailMessage[];\n  events: CalendarEvent[];\n}\n\nexport class SummaryPanel extends AggregatorPanel<SummaryData> {\n  constructor() {\n    super({\n      id: 'summary',\n      title: 'Daily Summary',\n      className: 'panel-wide',\n      showCount: true,\n      // Render as soon as stocks + emails arrive; events are optional bonus\n      requiredKeys: ['stocks', 'emails'],\n      staleThresholdMs: 3 * 60 * 1000,\n    });\n  }\n\n  protected render(data: SummaryData): void {\n    const snapshot = this.getSnapshot();\n\n    const unread  = data.emails.filter(e => !e.read).length;\n    const gainers = data.stocks.filter(q => (q.change ?? 0) > 0).length;\n    const losers  = data.stocks.filter(q => (q.change ?? 0) < 0).length;\n\n    // Optional key — may not have arrived yet\n    const upcomingCount = snapshot.events\n      ? snapshot.events.filter(ev => new Date(ev.start) > new Date()).length\n      : null;\n\n    const eventsHtml = upcomingCount != null\n      ? `<div class=\"summary-metric\" data-metric=\"events\">\n           <span class=\"summary-metric-label\">Upcoming events</span>\n           <span class=\"summary-metric-value\">${upcomingCount}</span>\n         </div>`\n      : '';\n\n    if (this.isFirstRender) {\n      // Full DOM build on first render\n      this.setContent(`\n        <div class=\"summary-metrics\">\n          <div class=\"summary-metric\" data-metric=\"unread\">\n            <span class=\"summary-metric-label\">Unread emails</span>\n            <span class=\"summary-metric-value\">${unread}</span>\n          </div>\n          <div class=\"summary-metric\" data-metric=\"gainers\">\n            <span class=\"summary-metric-label\">Stocks up</span>\n            <span class=\"summary-metric-value positive\">${gainers}</span>\n          </div>\n          <div class=\"summary-metric\" data-metric=\"losers\">\n            <span class=\"summary-metric-label\">Stocks down</span>\n            <span class=\"summary-metric-value negative\">${losers}</span>\n          </div>\n          ${eventsHtml}\n        </div>\n      `);\n    } else {\n      // Incremental patch — avoid full DOM thrash on subsequent pushes\n      this.patchMetric('unread', unread);\n      this.patchMetric('gainers', gainers);\n      this.patchMetric('losers', losers);\n      if (upcomingCount != null) this.patchMetric('events', upcomingCount);\n    }\n\n    this.setCount(data.emails.length + data.stocks.length);\n  }\n\n  /** Update a single metric value in-place */\n  private patchMetric(name: string, value: number): void {\n    const el = this.content.querySelector(\n      `[data-metric=\"${name}\"] .summary-metric-value`\n    );\n    if (el) el.textContent = String(value);\n  }\n}\n```\n\n---\n\n## Coordinator / Push Pattern\n\nCreate `src/services/panel-coordinator.ts`:\n\n```typescript\nimport { SummaryPanel } from '../components/SummaryPanel';\n\n/**\n * Lightweight pub/sub coordinator. Fetching panels call coordinator.push()\n * after each successful render; aggregator panels receive those updates.\n */\nexport class PanelCoordinator {\n  private summaryPanel: SummaryPanel;\n\n  constructor(summaryPanel: SummaryPanel) {\n    this.summaryPanel = summaryPanel;\n  }\n\n  push<K extends 'stocks' | 'emails' | 'events'>(\n    key: K,\n    value: unknown\n  ): void {\n    this.summaryPanel.updateData({ [key]: value } as any);\n  }\n}\n```\n\nIn your fetching panels, call `coordinator.push()` after each successful render:\n\n```typescript\n// Inside StockPanel.fetchData(), after this.render(quotes):\ncoordinator.push('stocks', quotes);\n\n// Inside EmailPanel.fetchData(), after this.render(messages):\ncoordinator.push('emails', messages);\n```\n\n---\n\n## Key Patterns\n\n1. **Constructor** calls `super()` with `requiredKeys` — the minimum fields needed before the first render.\n2. **updateData(partial)** is the only public ingestion point. It merges, timestamps, and either shows a pending placeholder or triggers `render()`.\n3. **render(data)** receives a fully-typed snapshot; use `this.getSnapshot()` for optional keys.\n4. **isFirstRender** lets you choose between a full DOM replace (first time) vs. incremental in-place patch (subsequent pushes) to avoid unnecessary reflow.\n5. **Staleness indicators** are automatically injected into the header when a key's last-update timestamp exceeds `staleThresholdMs`.\n6. **reset()** clears all state and reverts to pending — call it on session logout or data source disconnect.\n7. Aggregator panels never call `fetch()` or `showLoading()` — they call `showPending()` until required data arrives.\n8. The coordinator is a thin pub/sub shim; replace with EventEmitter, RxJS Subject, or your app's store if available.\n\n---\n\n## Aggregator vs Fetching Panel Checklist\n\n| Concern | Fetching Panel | Aggregator Panel |\n|---|---|---|\n| Network call | `fetchWithRetry(url)` | none |\n| Initial state | `showLoading()` in constructor | `showPending()` in constructor |\n| Data ingestion | internal async fetch | `updateData(partial)` |\n| First render gate | none | `hasRequiredData()` |\n| Re-render strategy | full replace | incremental patch preferred |\n| Staleness | timer-based refetch | automatic header indicator |\n| `destroy()` | clear interval + super | super only (no timers to clear) |\n\n---\n\n## TypeScript Strict-Mode Notes\n\n- `TData extends Record<string, unknown>` ensures key iteration is safe.\n- `Object.keys(partial) as (keyof TData)[]` is safe because `partial` is `Partial<TData>`.\n- The `as TData` cast in `this.render(this.partialData as TData)` is guarded by `hasRequiredData()` — a runtime guarantee backing the compile-time assertion.\n- If your project enables `exactOptionalPropertyTypes`, declare optional aggregated sources as `key?: T | undefined` in `TData`.\n", "examples/SummaryPanel.usage.ts": "```typescript\n/**\n * Example: wiring SummaryPanel into a dashboard.\n *\n * SummaryPanel renders as soon as 'stocks' and 'emails' have been pushed\n * at least once. 'events' is optional and enriches the display when it\n * arrives.\n */\nimport { PanelCoordinator } from '../src/services/panel-coordinator';\nimport { SummaryPanel } from '../src/components/SummaryPanel';\nimport { StockPanel } from '../src/components/StockPanel';\nimport { EmailPanel } from '../src/components/EmailPanel';\nimport { CalendarPanel } from '../src/components/CalendarPanel';\nimport {\n  saveAggregatorSnapshot,\n  loadAggregatorSnapshot,\n} from './localStorageUtils';\n\nconst summaryPanel = new SummaryPanel();\nconst coordinator  = new PanelCoordinator(summaryPanel);\n\n// Optional: pre-populate from localStorage on page load\nconst cached = loadAggregatorSnapshot<{\n  stocks: unknown;\n  emails: unknown;\n  events: unknown;\n}>('summary');\nif (cached) {\n  summaryPanel.updateData(cached);\n}\n\n// Wire fetching panels to push their data after each successful render\nconst stockPanel    = new StockPanel(data  => coordinator.push('stocks', data));\nconst emailPanel    = new EmailPanel(data  => coordinator.push('emails', data));\nconst calendarPanel = new CalendarPanel(data => coordinator.push('events', data));\n\n// Save snapshot after each update so the next page load pre-populates faster\nconst origUpdate = summaryPanel.updateData.bind(summaryPanel);\nsummaryPanel.updateData = (partial) => {\n  origUpdate(partial);\n  if (summaryPanel.hasRequiredData()) {\n    saveAggregatorSnapshot('summary', partial);\n  }\n};\n\n// Mount to DOM\ndocument.getElementById('dashboard')!.append(\n  stockPanel.getElement(),\n  emailPanel.getElement(),\n  calendarPanel.getElement(),\n  summaryPanel.getElement(),\n);\n```\n*** End Files\n\n<EVOLUTION_COMPLETE>\n", "examples/localStorageUtils.ts": "```typescript\n// Utility: persist aggregator panel's last-received snapshot to localStorage\n// so the panel can pre-populate on page reload before sibling panels re-push.\n\nexport function saveAggregatorSnapshot<T>(\n  panelId: string,\n  data: Partial<T>\n): void {\n  try {\n    localStorage.setItem(\n      `aggregatorSnapshot_${panelId}`,\n      JSON.stringify({ data, savedAt: Date.now() })\n    );\n  } catch {\n    // Quota exceeded or private browsing — silently ignore\n  }\n}\n\nexport function loadAggregatorSnapshot<T>(\n  panelId: string,\n  maxAgeMs = 10 * 60 * 1000\n): Partial<T> | null {\n  try {\n    const raw = localStorage.getItem(`aggregatorSnapshot_${panelId}`);\n    if (!raw) return null;\n    const { data, savedAt } = JSON.parse(raw) as {\n      data: Partial<T>;\n      savedAt: number;\n    };\n    if (Date.now() - savedAt > maxAgeMs) {\n      localStorage.removeItem(`aggregatorSnapshot_${panelId}`);\n      return null;\n    }\n    return data;\n  } catch {\n    return null;\n  }\n}\n```\n"}


skill_id: data-service-enhanced__v2_c7ebfd74
name: data-service-proxy-direct
path: example/skills/data-service-proxy-direct/SKILL.md
category: workflow
visibility: private
lineage_origin: fixed
lineage_source_task_id: evolution-v2-task-04
lineage_created_at: 2026-03-10T18:35:23.540385
lineage_created_by: openrouter/anthropic/claude-sonnet-4.6
first_seen: 2026-03-10T18:35:23.540407
last_updated: 2026-03-10T18:35:23.540408
lineage_change_summary:
Renamed the incorrectly named `Cache<T>` interface to `CacheEntry<T>` in the circuit-breaker code template to match the actual implementation used throughout the class.

lineage_content_diff:
--- a/SKILL.md
+++ b/SKILL.md
@@ -19,7 +19,7 @@
   cooldownUntil: number;
 }
 
-interface Cache<T> {
+interface CacheEntry<T> {
   data: T;
   timestamp: number;
 }

lineage_content_snapshot:
{"SKILL.md": "---\nname: data-service-enhanced\ndescription: Create resilient data fetching services with circuit breaker pattern, supporting both proxied and direct API calls. Services handle fetch, cache, retry, and expose typed data to panel components.\n---\n\n# Data Service Pattern\n\nEach panel's data comes from a dedicated **service module** in `src/services/`. Services handle API calls, caching, error handling with circuit breaker pattern, and can work with both proxied and direct API calls.\n\n## Circuit Breaker\n\nThe circuit breaker prevents cascading failures when an API is down. After N consecutive failures, it enters \"cooldown\" mode and serves cached data instead of hitting the API.\n\nCreate `src/utils/circuit-breaker.ts`:\n\n```typescript\ninterface CircuitState {\n  failures: number;\n  cooldownUntil: number;\n}\n\ninterface CacheEntry<T> {\n  data: T;\n  timestamp: number;\n}\n\nexport interface CircuitBreakerOptions {\n  name: string;\n  maxFailures?: number;\n  cooldownMs?: number;\n  cacheTtlMs?: number;\n}\n\nexport class CircuitBreaker<T> {\n  private state: CircuitState = { failures: 0, cooldownUntil: 0 };\n  private cache: CacheEntry<T> | null = null;\n  private name: string;\n  private maxFailures: number;\n  private cooldownMs: number;\n  private cacheTtlMs: number;\n\n  constructor(options: CircuitBreakerOptions) {\n    this.name = options.name;\n    this.maxFailures = options.maxFailures ?? 2;\n    this.cooldownMs = options.cooldownMs ?? 5 * 60 * 1000;  // 5 minutes\n    this.cacheTtlMs = options.cacheTtlMs ?? 10 * 60 * 1000; // 10 minutes\n  }\n\n  isOnCooldown(): boolean {\n    if (Date.now() < this.state.cooldownUntil) return true;\n    if (this.state.cooldownUntil > 0) {\n      this.state = { failures: 0, cooldownUntil: 0 };\n    }\n    return false;\n  }\n\n  getCached(): T | null {\n    if (this.cache && Date.now() - this.cache.timestamp < this.cacheTtlMs) {\n      return this.cache.data;\n    }\n    return null;\n  }\n\n  recordSuccess(data: T): void {\n    this.state = { failures: 0, cooldownUntil: 0 };\n    this.cache = { data, timestamp: Date.now() };\n  }\n\n  recordFailure(error?: string): void {\n    this.state.failures++;\n    if (this.state.failures >= this.maxFailures) {\n      this.state.cooldownUntil = Date.now() + this.cooldownMs;\n      console.warn(`[${this.name}] Cooldown for ${this.cooldownMs / 1000}s`);\n    }\n  }\n\n  async execute<R extends T>(fn: () => Promise<R>, defaultValue: R): Promise<R> {\n    if (this.isOnCooldown()) {\n      const cached = this.getCached();\n      return (cached as R) ?? defaultValue;\n    }\n    const cached = this.getCached();\n    if (cached !== null) return cached as R;\n    try {\n      const result = await fn();\n      this.recordSuccess(result);\n      return result;\n    } catch (e) {\n      console.error(`[${this.name}] Failed:`, e);\n      this.recordFailure(String(e));\n      return defaultValue;\n    }\n  }\n}\n\nexport function createCircuitBreaker<T>(options: CircuitBreakerOptions): CircuitBreaker<T> {\n  return new CircuitBreaker<T>(options);\n}\n```\n\n## Service Module Pattern\n\nEach service is a TypeScript module that exports async data-fetching functions. Services can work with both proxied and direct API calls.\n\n### Proxied API Example\n\n```typescript\n// src/services/stock-market.ts\nimport { createCircuitBreaker } from '../utils/circuit-breaker';\n\nexport interface StockQuote {\n  symbol: string;\n  name: string;\n  price: number | null;\n  change: number | null;\n  sparkline?: number[];\n}\n\nconst breaker = createCircuitBreaker<StockQuote[]>({\n  name: 'StockMarket',\n  cacheTtlMs: 60_000, // 1 minute cache\n});\n\nexport async function fetchStockQuotes(symbols: string[]): Promise<StockQuote[]> {\n  return breaker.execute(async () => {\n    const params = new URLSearchParams({ symbols: symbols.join(',') });\n    const resp = await fetch(`/api/stocks?${params}`);\n    if (!resp.ok) throw new Error(`HTTP ${resp.status}`);\n    return resp.json();\n  }, []);\n}\n```\n\n### Direct API Example\n\n```typescript\n// src/services/weather.ts\nimport { createCircuitBreaker } from '../utils/circuit-breaker';\n\nexport interface WeatherData {\n  city: string;\n  temperature: number;\n  condition: string;\n  forecast: Array<{\n    day: string;\n    high: number;\n    low: number;\n    condition: string;\n  }>;\n}\n\nconst breaker = createCircuitBreaker<WeatherData>({\n  name: 'WeatherAPI',\n  cacheTtlMs: 30_000, // 30 seconds cache\n});\n\nexport async function fetchWeather(city: string): Promise<WeatherData> {\n  return breaker.execute(async () => {\n    const resp = await fetch(`https://wttr.in/${city}?format=j1`);\n    if (!resp.ok) throw new Error(`HTTP ${resp.status}`);\n    return resp.json();\n  }, {\n    city,\n    temperature: 0,\n    condition: 'Unknown',\n    forecast: []\n  });\n}\n```\n\n## Key Patterns\n\n1. **One circuit breaker per API endpoint** — create at module level\n2. **Export typed interfaces** for data shapes\n3. **Wrap fetch calls** in `breaker.execute(fn, defaultValue)`\n4. **Default values** are empty arrays `[]` or empty objects `{}`\n5. **Services are stateless modules** — no class instances, just exported functions\n6. **Support both proxied and direct API calls** — choose based on API requirements\n7. **API proxy**: Frontend calls `/api/...` which proxies to external APIs (hides API keys)\n8. **Direct API**: Frontend calls external APIs directly when no sensitive data is involved\n\n## API Proxy Pattern\n\nFor APIs requiring keys, create server-side proxy endpoints:\n\n```typescript\n// api/stocks.ts (Vercel serverless function)\nexport default async function handler(req, res) {\n  const { symbols } = req.query;\n  const apiKey = process.env.FINNHUB_API_KEY;\n  const data = await fetch(`https://finnhub.io/api/v1/quote?symbol=${symbols}&token=${apiKey}`);\n  res.json(await data.json());\n}\n```\n\nThis keeps API keys server-side and provides a single endpoint for the frontend.\n"}


skill_id: vanilla-ts-http-route-handler__v0_376aea31
name: vanilla-ts-http-route-handler
path: example/skills/vanilla-ts-http-route-handler/SKILL.md
category: workflow
visibility: private
lineage_origin: captured
lineage_source_task_id: evolution-v2-task-04
lineage_created_at: 2026-03-10T18:35:38.029776
lineage_created_by: openrouter/anthropic/claude-sonnet-4.6
first_seen: 2026-03-10T18:35:38.029785
last_updated: 2026-03-10T18:35:38.029786
description:
How to correctly add route handlers in a vanilla TypeScript project that uses http.createServer with a manual switch/case router — without introducing Express Router or Next.js patterns.

lineage_change_summary:
Capture the pattern for adding route handlers in vanilla TypeScript HTTP servers that use manual switch/case routing instead of Express or Next.js patterns.

lineage_content_diff:
--- a/SKILL.md
+++ b/SKILL.md
@@ -0,0 +1,147 @@
+---
+name: vanilla-ts-http-route-handler
+description: How to correctly add route handlers in a vanilla TypeScript project that uses http.createServer with a manual switch/case router — without introducing Express Router or Next.js patterns.
+---
+
+# Adding Route Handlers to a Vanilla TypeScript HTTP Server
+
+Use this skill whenever you need to add a new route to a TypeScript project
+that uses Node's built-in `http` module and a `switch/case` pathname router
+**without Express or Next.js**.
+
+---
+
+## Step 0 — Confirm the routing style
+
+Before writing any code, inspect `server/index.ts` (or whichever file starts
+the HTTP server).  Look for:
+
+```ts
+http.createServer((req, res) => {
+  const { pathname } = new URL(req.url!, `http://${req.headers.host}`);
+  switch (pathname) {
+    case '/api/foo': ...
+  }
+});
+```
+
+If you see this pattern, follow the steps below.  
+**Do NOT use `express.Router`, `next/server`, or any framework-specific
+handler signature** — even if other files in `server/routes/` happen to
+use those patterns.
+
+---
+
+## Step 1 — Create the route file
+
+Create `server/routes/<feature>.ts`.  
+Use **only** the native Node types: `http.IncomingMessage` and
+`http.ServerResponse`.
+
+```ts
+// server/routes/health.ts
+import http from 'http';
+
+export async function handleHealth(
+  req: http.IncomingMessage,
+  res: http.ServerResponse
+): Promise<void> {
+  // Parse body for POST/PUT if needed
+  const body = await readBody(req);   // helper shown in Step 3
+
+  res.writeHead(200, { 'Content-Type': 'application/json' });
+  res.end(JSON.stringify({ status: 'ok' }));
+}
+```
+
+Rules for the route file:
+- Export the handler as a **named async function** (`handle<Feature>`).
+- Accept exactly `(req: http.IncomingMessage, res: http.ServerResponse)`.
+- Return `Promise<void>`.
+- Do **not** call `next()` or return a `Response` object.
+- Handle errors internally and write an appropriate HTTP status + JSON body.
+
+---
+
+## Step 2 — Import the handler in `server/index.ts`
+
+Add a named import at the top of the file alongside any existing imports:
+
+```ts
+import { handleHealth } from './routes/health';
+```
+
+---
+
+## Step 3 — Add a `case` entry to the switch block
+
+Locate the existing `switch (pathname)` block and add a new `case`:
+
+```ts
+switch (pathname) {
+  case '/api/health':
+    await handleHealth(req, res);
+    break;
+
+  // … existing cases …
+
+  default:
+    res.writeHead(404, { 'Content-Type': 'application/json' });
+    res.end(JSON.stringify({ error: 'Not found' }));
+}
+```
+
+- Match the exact pathname string used by the client.
+- Always `break` (or `return`) after calling the handler.
+- Keep the `default` case last.
+
+---
+
+## Step 4 — Optional: shared body-reading helper
+
+If multiple routes need to parse a JSON request body, add a small utility
+rather than duplicating the logic:
+
+```ts
+// server/utils/readBody.ts
+import http from 'http';
+
+export function readBody(req: http.IncomingMessage): Promise<unknown> {
+  return new Promise((resolve, reject) => {
+    let data = '';
+    req.on('data', (chunk) => (data += chunk));
+    req.on('end', () => {
+      try {
+        resolve(data ? JSON.parse(data) : {});
+      } catch {
+        reject(new Error('Invalid JSON'));
+      }
+    });
+    req.on('error', reject);
+  });
+}
+```
+
+Import and use it inside any route handler that needs it.
+
+---
+
+## Checklist
+
+- [ ] `server/routes/<feature>.ts` exists and exports a named async handler.
+- [ ] Handler signature is `(req: http.IncomingMessage, res: http.ServerResponse): Promise<void>`.
+- [ ] Handler always calls `res.writeHead(...)` and `res.end(...)` on every code path.
+- [ ] Handler is imported in `server/index.ts`.
+- [ ] A matching `case` entry is present in the `switch (pathname)` block.
+- [ ] No Express / Next.js / Koa types or patterns were introduced.
+
+---
+
+## Anti-patterns to avoid
+
+| ❌ Wrong | ✅ Correct |
+|---|---|
+| `import { Router } from 'express'` | `import http from 'http'` |
+| `export default function handler(req: NextApiRequest, ...)` | `export async function handleFoo(req: http.IncomingMessage, ...)` |
+| `router.get('/foo', ...)` | `case '/api/foo': await handleFoo(req, res); break;` |
+| Returning a value from the handler | Calling `res.end(...)` and returning `void` |

lineage_content_snapshot:
{"SKILL.md": "---\nname: vanilla-ts-http-route-handler\ndescription: How to correctly add route handlers in a vanilla TypeScript project that uses http.createServer with a manual switch/case router — without introducing Express Router or Next.js patterns.\n---\n\n# Adding Route Handlers to a Vanilla TypeScript HTTP Server\n\nUse this skill whenever you need to add a new route to a TypeScript project\nthat uses Node's built-in `http` module and a `switch/case` pathname router\n**without Express or Next.js**.\n\n---\n\n## Step 0 — Confirm the routing style\n\nBefore writing any code, inspect `server/index.ts` (or whichever file starts\nthe HTTP server).  Look for:\n\n```ts\nhttp.createServer((req, res) => {\n  const { pathname } = new URL(req.url!, `http://${req.headers.host}`);\n  switch (pathname) {\n    case '/api/foo': ...\n  }\n});\n```\n\nIf you see this pattern, follow the steps below.  \n**Do NOT use `express.Router`, `next/server`, or any framework-specific\nhandler signature** — even if other files in `server/routes/` happen to\nuse those patterns.\n\n---\n\n## Step 1 — Create the route file\n\nCreate `server/routes/<feature>.ts`.  \nUse **only** the native Node types: `http.IncomingMessage` and\n`http.ServerResponse`.\n\n```ts\n// server/routes/health.ts\nimport http from 'http';\n\nexport async function handleHealth(\n  req: http.IncomingMessage,\n  res: http.ServerResponse\n): Promise<void> {\n  // Parse body for POST/PUT if needed\n  const body = await readBody(req);   // helper shown in Step 3\n\n  res.writeHead(200, { 'Content-Type': 'application/json' });\n  res.end(JSON.stringify({ status: 'ok' }));\n}\n```\n\nRules for the route file:\n- Export the handler as a **named async function** (`handle<Feature>`).\n- Accept exactly `(req: http.IncomingMessage, res: http.ServerResponse)`.\n- Return `Promise<void>`.\n- Do **not** call `next()` or return a `Response` object.\n- Handle errors internally and write an appropriate HTTP status + JSON body.\n\n---\n\n## Step 2 — Import the handler in `server/index.ts`\n\nAdd a named import at the top of the file alongside any existing imports:\n\n```ts\nimport { handleHealth } from './routes/health';\n```\n\n---\n\n## Step 3 — Add a `case` entry to the switch block\n\nLocate the existing `switch (pathname)` block and add a new `case`:\n\n```ts\nswitch (pathname) {\n  case '/api/health':\n    await handleHealth(req, res);\n    break;\n\n  // … existing cases …\n\n  default:\n    res.writeHead(404, { 'Content-Type': 'application/json' });\n    res.end(JSON.stringify({ error: 'Not found' }));\n}\n```\n\n- Match the exact pathname string used by the client.\n- Always `break` (or `return`) after calling the handler.\n- Keep the `default` case last.\n\n---\n\n## Step 4 — Optional: shared body-reading helper\n\nIf multiple routes need to parse a JSON request body, add a small utility\nrather than duplicating the logic:\n\n```ts\n// server/utils/readBody.ts\nimport http from 'http';\n\nexport function readBody(req: http.IncomingMessage): Promise<unknown> {\n  return new Promise((resolve, reject) => {\n    let data = '';\n    req.on('data', (chunk) => (data += chunk));\n    req.on('end', () => {\n      try {\n        resolve(data ? JSON.parse(data) : {});\n      } catch {\n        reject(new Error('Invalid JSON'));\n      }\n    });\n    req.on('error', reject);\n  });\n}\n```\n\nImport and use it inside any route handler that needs it.\n\n---\n\n## Checklist\n\n- [ ] `server/routes/<feature>.ts` exists and exports a named async handler.\n- [ ] Handler signature is `(req: http.IncomingMessage, res: http.ServerResponse): Promise<void>`.\n- [ ] Handler always calls `res.writeHead(...)` and `res.end(...)` on every code path.\n- [ ] Handler is imported in `server/index.ts`.\n- [ ] A matching `case` entry is present in the `switch (pathname)` block.\n- [ ] No Express / Next.js / Koa types or patterns were introduced.\n\n---\n\n## Anti-patterns to avoid\n\n| ❌ Wrong | ✅ Correct |\n|---|---|\n| `import { Router } from 'express'` | `import http from 'http'` |\n| `export default function handler(req: NextApiRequest, ...)` | `export async function handleFoo(req: http.IncomingMessage, ...)` |\n| `router.get('/foo', ...)` | `case '/api/foo': await handleFoo(req, res); break;` |\n| Returning a value from the handler | Calling `res.end(...)` and returning `void` |"}


skill_id: create-full-stack-panel-feature-enhanced__v0_3a60c062
name: create-full-stack-panel-feature-with-server-wiring
path: example/skills/create-full-stack-panel-feature-with-server-wiring/SKILL.md
category: workflow
visibility: private
lineage_origin: derived
lineage_source_task_id: evolution-v2-task-04
lineage_created_at: 2026-03-10T18:36:43.649980
lineage_created_by: openrouter/anthropic/claude-sonnet-4.6
first_seen: 2026-03-10T18:36:43.650010
last_updated: 2026-03-10T20:53:34.177018
description:
Multi-layer workflow for creating complete panel features by analyzing existing patterns and generating coordinated server routes, services, UI components, registration, and styling files.

lineage_change_summary:
Added Step 5 for wiring new route handlers into server-side dispatch tables (http.createServer switch/case pattern), making the skill self-contained for projects that don't use Express router mounting.

lineage_content_diff:
--- a/SKILL.md
+++ b/SKILL.md
@@ -1,17 +1,18 @@
 ---
-name: create-full-stack-panel-feature
-description: Multi-layer workflow for creating complete panel features by analyzing existing patterns and generating coordinated server routes, services, UI components, registration, and styling files.
----
-
-# Create Full-Stack Panel Feature
-
-This skill guides you through creating a complete, pattern-consistent panel feature across all application layers: backend API routes, service logic, frontend components, registration, and styling.
+name: create-full-stack-panel-feature-with-server-wiring
+description: Multi-layer workflow for creating complete panel features by analyzing existing patterns and generating coordinated server routes, services, UI components, registration, styling files, AND explicitly wiring new handlers into the server's dispatch table (supports both Express middleware and manual http.createServer switch/case patterns).
+---
+
+# Create Full-Stack Panel Feature (with Server Wiring)
+
+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.
 
 ## When to Use
 
 - Adding a new panel type to a dashboard or admin interface
 - Creating a feature that spans multiple architectural layers
 - Ensuring consistency with existing codebase patterns across the stack
+- Projects that use a manual `http.createServer` dispatch loop instead of Express router mounting
 
 ## Workflow Steps
 
@@ -21,16 +22,19 @@
 
 ```bash
 # Find existing server routes
-find . -name "*route*.js" -o -name "*routes*.js" | grep -E "(panel|dashboard)"
+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" | grep -E "(panel|dashboard)"
+find . -name "*service*.js" -o -name "*service*.ts" | grep -E "(panel|dashboard|server)"
 
 # Find UI components
-find . -name "*.jsx" -o -name "*.tsx" | grep -i panel
+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"
+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
@@ -40,7 +44,7 @@
 
 Read 1-2 representative examples from **each layer** to understand:
 
-- Naming conventions (e.g., `MetricsPanel`, `metrics-service.js`)
+- Naming conventions (e.g., `MetricsPanel`, `metrics-service.ts`)
 - Code structure and organization
 - Import/export patterns
 - Registration mechanisms
@@ -48,6 +52,13 @@
 - Error handling approaches
 - Styling conventions
 
+**Also read the server entry point** (`server/index.ts`, `src/server.ts`, etc.) to determine which dispatch pattern is used:
+
+```bash
+# Read the server entry point to identify routing mechanism
+cat server/index.ts   # or server/index.js, src/server.ts, etc.
+```
+
 **Example analysis checklist:**
 
 ```
@@ -71,21 +82,26 @@
 - ✓ 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
 ```
 
 ### 3. Plan Your Feature Files
 
-Based on patterns, list the files you need to create:
+Based on patterns, list the files you need to create **and** the existing files you need to update:
 
 **Typical full-stack panel structure:**
-1. **Server route** (e.g., `server/routes/system-health-route.js`)
-2. **Service layer** (e.g., `server/services/system-health-service.js`)
-3. **UI component** (e.g., `client/components/SystemHealthPanel.jsx`)
-4. **Panel registration** (e.g., update `client/config/panel-registry.js`)
+1. **Server route handler** (e.g., `server/routes/system-health-route.ts`)
+2. **Service layer** (e.g., `server/services/system-health-service.ts`)
+3. **UI component** (e.g., `client/panels/SystemHealthPanel.ts`)
+4. **Panel registration** (e.g., update `client/config/panel-registry.ts`)
 5. **Styling** (e.g., `client/styles/system-health-panel.css`)
 6. **Type definitions** (if TypeScript, e.g., `types/system-health.ts`)
-
-### 4. Create Files in Order
+7. **⚠️ Server wiring** (update `server/index.ts` to import and dispatch to the new handler)
 
 ### 3b. Detect Project Framework
 
@@ -109,12 +125,34 @@
 - If components are `.ts` files extending a `Panel` base class → use the **Vanilla TS template** (Step 4C-Vanilla).
 - When in doubt, read one or two existing component files to confirm before proceeding.
 
+### 3c. Detect Server Dispatch Pattern
+
+**Read the server entry point and classify it:**
+
+```bash
+# 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)
+
 ### 4. Create Files in Order
 
 Generate files in dependency order (backend → frontend → registration):
 
-#### A. Server Route
-
+#### A. Server Route Handler
+
+**Express style:**
 ```javascript
 // server/routes/{feature}-route.js
 const express = require('express');
@@ -133,21 +171,43 @@
 module.exports = router;
 ```
 
+**Manual dispatch style (TypeScript):**
+```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) }));
+  }
+}
+```
+
 #### B. Service Layer
 
-```javascript
-// server/services/{feature}-service.js
-class FeatureService {
-  async getData() {
-    // Implementation following existing service patterns
-    // - Data fetching
-    // - Business logic
-    // - Data transformation
-    return processedData;
-  }
-}
-
-module.exports = new FeatureService();
+```typescript
+// 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;
+}
 ```
 
 #### C-React. UI Component (React/JSX projects)
@@ -306,9 +366,144 @@
 }
 ```
 
-### 5. Verify Pattern Consistency
-
-After creating all files, check:
+### 5. Wire the Route Handler into the Server
+
+**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:
+
+```bash
+cat server/index.ts    # adjust path as needed
+```
+
+---
+
+#### 5A. Express Pattern
+
+Add a `require`/`import` and an `app.use()` call alongside the existing routes:
+
+```typescript
+// 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:**
+- [ ] Import is at the top of the file with other route imports
+- [ ] `app.use()` call is placed before any catch-all `404` handler
+- [ ] Path prefix matches what the client fetches
+
+---
+
+#### 5B. Manual switch/case Pattern
+
+This 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`:
+
+```typescript
+// 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:**
+- [ ] Import is at the top of the file with other handler imports
+- [ ] New `case` is placed **before** the `default` case
+- [ ] `break` (or `return`) statement is present after the handler call
+- [ ] The path string exactly matches the client's `fetch('/api/panels/{feature}')` call
+- [ ] `action` / query-param forwarding matches the handler's signature
+
+---
+
+#### 5C. Manual if/else Chain Pattern
+
+If the server uses an `if/else if` chain instead of `switch`:
+
+```typescript
+// 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:**
+- [ ] New `else if` branch is before the final `else` (404) block
+- [ ] Import is at the top of the file
+
+---
+
+#### 5D. Handler Map Pattern
+
+If the server uses a map object to look up handlers:
+
+```typescript
+// 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:**
+- [ ] New entry key matches client fetch URL exactly
+- [ ] Import is at the top of the file
+
+---
+
+#### 5E. Verify Wiring
+
+After updating the server entry point, confirm the wiring is complete:
+
+```bash
+# 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}
+```
+
+### 6. Verify Pattern Consistency
+
+After creating all files and wiring the server, check:
 
 - [ ] Naming follows project conventions across all layers
 - [ ] Import/export statements are consistent
@@ -317,14 +512,18 @@
 - [ ] Component structure mirrors other panels
 - [ ] Registration metadata is complete
 - [ ] CSS class naming follows BEM or project convention
-
-### 6. Test Integration Points
-
-Verify that:
-
-```bash
-# Server route is registered
-grep -r "require.*{feature}-route" server/
+- [ ] Server entry point updated (new route is reachable)
+
+### 7. Test Integration Points
+
+Verify that all pieces connect:
+
+```bash
+# 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/
@@ -335,19 +534,22 @@
 
 ## Key Principles
 
-1. **Pattern before implementation**: Always read existing code first
+1. **Pattern before implementation**: Always read existing code first — especially the server entry point
 2. **Maintain consistency**: Match naming, structure, and style exactly
-3. **Complete the stack**: Don't leave layers incomplete
-4. **Follow the chain**: Backend → Service → Frontend → Registration → Styling
-5. **Verify integration**: Ensure all pieces connect properly
+3. **Complete the stack**: Don't leave layers incomplete — the server wiring step is as important as creating the handler file
+4. **Follow the chain**: Backend service → Route handler → **Server wiring** → Frontend component → Registration → Styling
+5. **Verify integration**: Ensure all pieces connect properly; a missing `case` in a switch block causes silent 404s
 
 ## Common Pitfalls
 
-- Creating component before verifying API endpoint works
-- Inconsistent naming across layers (camelCase vs kebab-case)
-- Forgetting to register the panel in the appropriate config
-- Missing error handling in any layer
-- Skipping styling, leading to broken UI
+- ❌ Creating a route handler file but forgetting to add it to the server's dispatch table → **silent 404 on every request**
+- ❌ Creating component before verifying API endpoint works
+- ❌ Inconsistent naming across layers (camelCase vs kebab-case)
+- ❌ Forgetting to register the panel in the frontend config
+- ❌ Missing error handling in any layer
+- ❌ Skipping styling, leading to broken UI
+- ❌ Placing a new `case` after the `default` case in a switch (unreachable code)
+- ❌ Path string mismatch between server case and client fetch URL
 
 ## Variations
 
@@ -360,3 +562,7 @@
 - Documentation 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.

lineage_content_snapshot:
{"SKILL.md": "---\nname: create-full-stack-panel-feature-with-server-wiring\ndescription: Multi-layer workflow for creating complete panel features by analyzing existing patterns and generating coordinated server routes, services, UI components, registration, styling files, AND explicitly wiring new handlers into the server's dispatch table (supports both Express middleware and manual http.createServer switch/case patterns).\n---\n\n# Create Full-Stack Panel Feature (with Server Wiring)\n\nThis 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.\n\n## When to Use\n\n- Adding a new panel type to a dashboard or admin interface\n- Creating a feature that spans multiple architectural layers\n- Ensuring consistency with existing codebase patterns across the stack\n- Projects that use a manual `http.createServer` dispatch loop instead of Express router mounting\n\n## Workflow Steps\n\n### 1. Identify Pattern Files\n\nLocate existing examples for each layer you need to implement:\n\n```bash\n# Find existing server routes\nfind . -name \"*route*.js\" -o -name \"*routes*.js\" -o -name \"*route*.ts\" -o -name \"*routes*.ts\" | grep -E \"(panel|dashboard|server)\"\n\n# Find service files\nfind . -name \"*service*.js\" -o -name \"*service*.ts\" | grep -E \"(panel|dashboard|server)\"\n\n# Find UI components\nfind . -name \"*.jsx\" -o -name \"*.tsx\" -o -name \"*.ts\" | grep -i panel\n\n# Find the server entry point (critical for wiring step)\nfind . -name \"index.ts\" -o -name \"index.js\" -o -name \"server.ts\" -o -name \"server.js\" | grep -v node_modules | head -10\n\n# Find registration/config files\nfind . -name \"*config*.js\" -o -name \"*registry*.js\" -o -name \"*config*.ts\" -o -name \"*registry*.ts\"\n\n# Find styling files\nfind . -name \"*.css\" -o -name \"*.scss\" | grep -i panel\n```\n\n### 2. Read and Analyze Patterns\n\nRead 1-2 representative examples from **each layer** to understand:\n\n- Naming conventions (e.g., `MetricsPanel`, `metrics-service.ts`)\n- Code structure and organization\n- Import/export patterns\n- Registration mechanisms\n- API endpoint patterns\n- Error handling approaches\n- Styling conventions\n\n**Also read the server entry point** (`server/index.ts`, `src/server.ts`, etc.) to determine which dispatch pattern is used:\n\n```bash\n# Read the server entry point to identify routing mechanism\ncat server/index.ts   # or server/index.js, src/server.ts, etc.\n```\n\n**Example analysis checklist:**\n\n```\nServer Route Pattern:\n- ✓ Endpoint naming: /api/panels/{type}\n- ✓ Authentication middleware\n- ✓ Response format: { success, data, error }\n\nService Pattern:\n- ✓ Export structure: class vs functions\n- ✓ Data transformation logic\n- ✓ Error propagation\n\nComponent Pattern:\n- ✓ Props interface\n- ✓ State management (hooks/class)\n- ✓ Data fetching approach\n- ✓ Loading/error states\n\nRegistration Pattern:\n- ✓ Registry file location\n- ✓ Registration format (array/object)\n- ✓ Required metadata fields\n\nServer Dispatch Pattern:\n- ✓ Express app.use() / router.use()  → Express routing\n- ✓ switch/case on url/pathname       → Manual dispatch table\n- ✓ if/else if chain on url           → Manual dispatch chain\n- ✓ handler map object { '/path': fn }→ Handler map pattern\n```\n\n### 3. Plan Your Feature Files\n\nBased on patterns, list the files you need to create **and** the existing files you need to update:\n\n**Typical full-stack panel structure:**\n1. **Server route handler** (e.g., `server/routes/system-health-route.ts`)\n2. **Service layer** (e.g., `server/services/system-health-service.ts`)\n3. **UI component** (e.g., `client/panels/SystemHealthPanel.ts`)\n4. **Panel registration** (e.g., update `client/config/panel-registry.ts`)\n5. **Styling** (e.g., `client/styles/system-health-panel.css`)\n6. **Type definitions** (if TypeScript, e.g., `types/system-health.ts`)\n7. **⚠️ Server wiring** (update `server/index.ts` to import and dispatch to the new handler)\n\n### 3b. Detect Project Framework\n\n**Before selecting templates in Step 4, determine the frontend framework in use:**\n\n```bash\n# Check for React\ngrep -s \"react\" package.json | head -5\n\n# Check for TypeScript without React (vanilla TS)\nls client/components/*.ts 2>/dev/null | head -3\nls client/panels/*.ts 2>/dev/null | head -3\n\n# Check for a Panel base class (common in vanilla TS dashboards)\ngrep -r \"class.*Panel\" client/ --include=\"*.ts\" -l | head -3\ngrep -r \"extends Panel\" client/ --include=\"*.ts\" -l | head -3\n```\n\n**Decision rule:**\n- If `react` or `react-dom` is present in `package.json` → use the **React/JSX template** (Step 4C-React).\n- If components are `.ts` files extending a `Panel` base class → use the **Vanilla TS template** (Step 4C-Vanilla).\n- When in doubt, read one or two existing component files to confirm before proceeding.\n\n### 3c. Detect Server Dispatch Pattern\n\n**Read the server entry point and classify it:**\n\n```bash\n# Look for Express usage\ngrep -n \"express\\|app\\.use\\|router\\.\" server/index.ts 2>/dev/null || \\\ngrep -n \"express\\|app\\.use\\|router\\.\" server/index.js 2>/dev/null || \\\ngrep -n \"express\\|app\\.use\\|router\\.\" src/server.ts 2>/dev/null\n\n# Look for manual http.createServer dispatch\ngrep -n \"createServer\\|switch.*url\\|switch.*pathname\\|req\\.url\\|req\\.pathname\" server/index.ts 2>/dev/null || \\\ngrep -n \"createServer\\|switch.*url\\|switch.*pathname\\|req\\.url\\|req\\.pathname\" server/index.js 2>/dev/null\n```\n\n**Decision rule:**\n- `app.use('/api/feature', featureRouter)` style → **Express pattern** (Step 5A)\n- `switch(pathname) { case '/api/feature': ... }` style → **Manual switch/case pattern** (Step 5B)\n- `if (url.startsWith('/api/feature'))` style → **Manual if/else chain** (Step 5C)\n- `{ '/api/feature': handlerFn }` map style → **Handler map pattern** (Step 5D)\n\n### 4. Create Files in Order\n\nGenerate files in dependency order (backend → frontend → registration):\n\n#### A. Server Route Handler\n\n**Express style:**\n```javascript\n// server/routes/{feature}-route.js\nconst express = require('express');\nconst router = express.Router();\nconst featureService = require('../services/{feature}-service');\n\nrouter.get('/api/panels/{feature}', async (req, res) => {\n  try {\n    const data = await featureService.getData();\n    res.json({ success: true, data });\n  } catch (error) {\n    res.status(500).json({ success: false, error: error.message });\n  }\n});\n\nmodule.exports = router;\n```\n\n**Manual dispatch style (TypeScript):**\n```typescript\n// server/routes/{feature}-route.ts\nimport { IncomingMessage, ServerResponse } from 'http';\nimport { featureService } from '../services/{feature}-service';\n\nexport async function handle{Feature}Request(\n  req: IncomingMessage,\n  res: ServerResponse,\n  action?: string\n): Promise<void> {\n  try {\n    const data = await featureService.getData(action);\n    res.writeHead(200, { 'Content-Type': 'application/json' });\n    res.end(JSON.stringify({ success: true, data }));\n  } catch (err) {\n    res.writeHead(500, { 'Content-Type': 'application/json' });\n    res.end(JSON.stringify({ success: false, error: err instanceof Error ? err.message : String(err) }));\n  }\n}\n```\n\n#### B. Service Layer\n\n```typescript\n// server/services/{feature}-service.ts\nexport interface {Feature}Data {\n  // Define your data shape\n}\n\nexport async function getData(action?: string): Promise<{Feature}Data> {\n  // Implementation following existing service patterns\n  // - Data fetching\n  // - Business logic\n  // - Data transformation\n  return {} as {Feature}Data;\n}\n```\n\n#### C-React. UI Component (React/JSX projects)\n\n*Use this template only when React is confirmed in Step 3b.*\n\n```javascript\n// client/components/{Feature}Panel.jsx\nimport React, { useState, useEffect } from 'react';\nimport './styles/{feature}-panel.css';\n\nconst FeaturePanel = () => {\n  const [data, setData] = useState(null);\n  const [loading, setLoading] = useState(true);\n  const [error, setError] = useState(null);\n\n  useEffect(() => {\n    fetch('/api/panels/{feature}')\n      .then(res => res.json())\n      .then(result => {\n        setData(result.data);\n        setLoading(false);\n      })\n      .catch(err => {\n        setError(err.message);\n        setLoading(false);\n      });\n  }, []);\n\n  if (loading) return <div>Loading...</div>;\n  if (error) return <div>Error: {error}</div>;\n\n  return (\n    <div className=\"{feature}-panel\">\n      {/* Render data following UI patterns */}\n    </div>\n  );\n};\n\nexport default FeaturePanel;\n```\n\n#### C-Vanilla. UI Component (Vanilla TypeScript / Panel base-class projects)\n\n*Use this template when components extend a Panel base class (no React).*\n\n```typescript\n// client/panels/{Feature}Panel.ts\nimport { Panel } from '../core/Panel';\n\nexport class FeaturePanel extends Panel {\n  private intervalId: number | null = null;\n\n  constructor(id: string) {\n    super(id);\n    this.setTitle('Feature Display Name');\n  }\n\n  async onLoad(): Promise<void> {\n    if (this.isFetching) return;\n    this.isFetching = true;\n    try {\n      const res = await fetch('/api/panels/{feature}', {\n        headers: { Authorization: `Bearer ${localStorage.getItem('token') ?? ''}` }\n      });\n      if (!res.ok) throw new Error(`HTTP ${res.status}`);\n      const { data } = await res.json();\n      this.render(data);\n    } catch (err) {\n      this.showError(err instanceof Error ? err.message : String(err), () => this.onLoad());\n    } finally {\n      this.isFetching = false;\n    }\n  }\n\n  private render(data: unknown): void {\n    const container = document.createElement('div');\n    container.className = '{feature}-panel';\n    // Build DOM nodes from data instead of JSX:\n    // const item = document.createElement('p');\n    // item.textContent = String(data);\n    // container.appendChild(item);\n    this.setContent(container);\n  }\n\n  // Call this to start polling (optional)\n  startPolling(intervalMs = 60_000): void {\n    this.onLoad();\n    this.intervalId = window.setInterval(() => this.onLoad(), intervalMs);\n  }\n\n  destroy(): void {\n    if (this.intervalId !== null) clearInterval(this.intervalId);\n    super.destroy();\n  }\n}\n```\n\n**Key differences from React template:**\n- Extends `Panel` base class; uses `setContent()` / `showError()` / `setTitle()` / `setCount()` instead of React state.\n- DOM construction via `document.createElement` / `innerHTML` instead of JSX.\n- No `useState` / `useEffect`; lifecycle is `onLoad()` + `destroy()`.\n- File extension is `.ts`, not `.jsx`/`.tsx`.\n- Registration passes a factory function or class constructor, not a JSX component.\n\n#### D. Panel Registration\n\n**React projects** — import the component directly:\n\n```javascript\n// client/config/panel-registry.js (update existing)\nimport FeaturePanel from '../components/FeaturePanel';\n\nexport const panels = [\n  // ... existing panels\n  {\n    id: '{feature}',\n    name: 'Feature Display Name',\n    component: FeaturePanel,\n    icon: 'icon-name',\n    category: 'appropriate-category'\n  }\n];\n```\n\n**Vanilla TS projects** — register via factory:\n\n```typescript\n// client/config/panel-registry.ts (update existing)\nimport { FeaturePanel } from '../panels/FeaturePanel';\n\nexport const panels = [\n  // ... existing panels\n  {\n    id: '{feature}',\n    name: 'Feature Display Name',\n    factory: (id: string) => new FeaturePanel(id),\n    icon: 'icon-name',\n    category: 'appropriate-category'\n  }\n];\n```\n\n#### E. Styling\n\n```css\n/* client/styles/{feature}-panel.css */\n.{feature}-panel {\n  /* Follow existing panel styling conventions */\n  padding: 1rem;\n  border-radius: 4px;\n}\n\n.{feature}-panel__header {\n  /* Consistent header styling */\n}\n```\n\n### 5. Wire the Route Handler into the Server\n\n**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.\n\nFirst, re-read the server entry point to confirm the exact dispatch pattern:\n\n```bash\ncat server/index.ts    # adjust path as needed\n```\n\n---\n\n#### 5A. Express Pattern\n\nAdd a `require`/`import` and an `app.use()` call alongside the existing routes:\n\n```typescript\n// server/index.ts  — Express style\nimport featureRouter from './routes/{feature}-route';\n\n// Place with other app.use() route registrations\napp.use('/api/panels/{feature}', featureRouter);\n```\n\n**Checklist:**\n- [ ] Import is at the top of the file with other route imports\n- [ ] `app.use()` call is placed before any catch-all `404` handler\n- [ ] Path prefix matches what the client fetches\n\n---\n\n#### 5B. Manual switch/case Pattern\n\nThis 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`:\n\n```typescript\n// server/index.ts — manual switch/case dispatch\nimport { handle{Feature}Request } from './routes/{feature}-route';\n\n// Inside the createServer callback, find the switch block:\nconst url = new URL(req.url ?? '/', `http://${req.headers.host}`);\nconst pathname = url.pathname;\nconst action = url.searchParams.get('action') ?? undefined;\n\nswitch (pathname) {\n  // ... existing cases ...\n\n  case '/api/panels/{feature}':\n    await handle{Feature}Request(req, res, action);\n    break;\n\n  // ... keep the default/404 case last ...\n  default:\n    res.writeHead(404, { 'Content-Type': 'application/json' });\n    res.end(JSON.stringify({ error: 'Not found' }));\n}\n```\n\n**Checklist:**\n- [ ] Import is at the top of the file with other handler imports\n- [ ] New `case` is placed **before** the `default` case\n- [ ] `break` (or `return`) statement is present after the handler call\n- [ ] The path string exactly matches the client's `fetch('/api/panels/{feature}')` call\n- [ ] `action` / query-param forwarding matches the handler's signature\n\n---\n\n#### 5C. Manual if/else Chain Pattern\n\nIf the server uses an `if/else if` chain instead of `switch`:\n\n```typescript\n// server/index.ts — if/else chain\nimport { handle{Feature}Request } from './routes/{feature}-route';\n\n// Inside the createServer callback:\nif (pathname === '/api/panels/existing-a') {\n  await handleExistingA(req, res);\n} else if (pathname === '/api/panels/existing-b') {\n  await handleExistingB(req, res);\n// ↓ Add your new branch before the final else/404 block\n} else if (pathname === '/api/panels/{feature}') {\n  await handle{Feature}Request(req, res, action);\n} else {\n  res.writeHead(404, { 'Content-Type': 'application/json' });\n  res.end(JSON.stringify({ error: 'Not found' }));\n}\n```\n\n**Checklist:**\n- [ ] New `else if` branch is before the final `else` (404) block\n- [ ] Import is at the top of the file\n\n---\n\n#### 5D. Handler Map Pattern\n\nIf the server uses a map object to look up handlers:\n\n```typescript\n// server/index.ts — handler map\nimport { handle{Feature}Request } from './routes/{feature}-route';\n\nconst handlers: Record<string, RequestHandler> = {\n  '/api/panels/existing-a': handleExistingA,\n  '/api/panels/existing-b': handleExistingB,\n  // ↓ Add your new entry\n  '/api/panels/{feature}': handle{Feature}Request,\n};\n```\n\n**Checklist:**\n- [ ] New entry key matches client fetch URL exactly\n- [ ] Import is at the top of the file\n\n---\n\n#### 5E. Verify Wiring\n\nAfter updating the server entry point, confirm the wiring is complete:\n\n```bash\n# Confirm the import exists\ngrep -n \"handle{Feature}Request\\|{feature}-route\" server/index.ts\n\n# Confirm the dispatch entry exists\ngrep -n \"{feature}\" server/index.ts\n\n# Confirm no syntax errors (TypeScript projects)\nnpx tsc --noEmit 2>&1 | head -20\n\n# Quick smoke test: start the server and curl the endpoint\n# curl http://localhost:PORT/api/panels/{feature}\n```\n\n### 6. Verify Pattern Consistency\n\nAfter creating all files and wiring the server, check:\n\n- [ ] Naming follows project conventions across all layers\n- [ ] Import/export statements are consistent\n- [ ] Error handling matches existing patterns\n- [ ] API response format is uniform\n- [ ] Component structure mirrors other panels\n- [ ] Registration metadata is complete\n- [ ] CSS class naming follows BEM or project convention\n- [ ] Server entry point updated (new route is reachable)\n\n### 7. Test Integration Points\n\nVerify that all pieces connect:\n\n```bash\n# Server route handler file exists\nls server/routes/{feature}-route.*\n\n# Server entry point imports and dispatches the new handler\ngrep -n \"{feature}\" server/index.ts   # or server/index.js\n\n# Component is imported in registry\ngrep -r \"import.*{Feature}Panel\" client/config/\n\n# Styles are imported\ngrep -r \"import.*{feature}-panel.css\" client/\n```\n\n## Key Principles\n\n1. **Pattern before implementation**: Always read existing code first — especially the server entry point\n2. **Maintain consistency**: Match naming, structure, and style exactly\n3. **Complete the stack**: Don't leave layers incomplete — the server wiring step is as important as creating the handler file\n4. **Follow the chain**: Backend service → Route handler → **Server wiring** → Frontend component → Registration → Styling\n5. **Verify integration**: Ensure all pieces connect properly; a missing `case` in a switch block causes silent 404s\n\n## Common Pitfalls\n\n- ❌ Creating a route handler file but forgetting to add it to the server's dispatch table → **silent 404 on every request**\n- ❌ Creating component before verifying API endpoint works\n- ❌ Inconsistent naming across layers (camelCase vs kebab-case)\n- ❌ Forgetting to register the panel in the frontend config\n- ❌ Missing error handling in any layer\n- ❌ Skipping styling, leading to broken UI\n- ❌ Placing a new `case` after the `default` case in a switch (unreachable code)\n- ❌ Path string mismatch between server case and client fetch URL\n\n## Variations\n\n**For simple features**: May omit service layer if route logic is trivial\n\n**For complex features**: May need additional files:\n- Database migrations/models\n- Redux actions/reducers (if using Redux)\n- Test files for each layer\n- Documentation files\n\n**For TypeScript projects**: Add `.d.ts` or `.ts` type definition files\n\n**For Express projects**: Step 5A covers the wiring; it is usually straightforward (`app.use()`).\n\n**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.\n"}


