[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6KKDWCBXHX02BHBGSC617YN":3,"skill-same:01M6KKDWCBXHX02BHBGSC617YN":70},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":9,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":15,"categories":16,"keywords":17,"publishedAt":38,"linkOnly":39,"markdownContent":40,"markdownBody":41,"frontMatter":42,"refName":43,"commitSha":44,"skillDirectory":45,"forksCount":13,"repositoryHtmlUrl":46,"repositoryHomepageUrl":6,"ownerHtmlUrl":47,"ownerBlogUrl":6,"ownerDescription":48,"updatedAt":49,"install":50,"resources":63},"01M6KKDWCBXHX02BHBGSC617YN","panel-breaker-backoff",null,"Unified guide for authoring dashboard panel components in vanilla TypeScript — covers the Panel base class architecture, localStorage state persistence, exponential-backoff retry, and CircuitBreaker integration. Shows how to wrap any existing fetchX() service call with CircuitBreaker.execute() as a one-liner and clarifies when each resilience layer is appropriate.","AG064\u002FOpenSpace","AG064","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F184930299?v=4","01M6KHVFG7TB73NHX3RDYSTMH2","01M6KHVFMM7XA2PZA9Z83PHS3V",0,"MIT","https:\u002F\u002Fgithub.com\u002FAG064\u002FOpenSpace\u002Fblob\u002Fmain\u002Fshowcase\u002Fskills\u002Fpanel-breaker-backoff\u002FSKILL.md",[],[18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37],"panel breaker backoff","backoff","panel","authoring","Existing","for","retry","with","how to","as","typescript","call","wrap","how","integration","state","resilience","architecture","components","dashboard","2026-11-10 10:12:30",false,"---\nname: panel-breaker-backoff\ndescription: Unified guide for authoring dashboard panel components in vanilla TypeScript — covers the Panel base class architecture, localStorage state persistence, exponential-backoff retry, and CircuitBreaker integration. Shows how to wrap any existing fetchX() service call with CircuitBreaker.execute() as a one-liner and clarifies when each resilience layer is appropriate.\n---\n\n# Resilient Panel — Unified Pattern\n\nCreate dashboard panel components using **vanilla TypeScript** (no framework, no JSX).\nEach panel is a class extending a `Panel` base class and optionally wraps its data source with a `CircuitBreaker`.\n\n---\n\n## Architecture Overview\n\n```\nPanel (base class)\n├── element: HTMLElement (.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### Resilience layers (choose what you need)\n\n| Layer | File | When to use |\n|---|---|---|\n| **isFetching guard** | `Panel` base class | Always — prevents concurrent duplicate requests |\n| **Exponential-backoff retry** | `Panel.fetchWithRetry()` | When you own the fetch call and want per-request retry before surfacing an error |\n| **CircuitBreaker** | `src\u002Futils\u002Fcircuit-breaker.ts` | When a service may fail repeatedly; stops hammering the API and serves stale cache instead |\n| **localStorage persistence** | `Panel.saveState()` \u002F `loadState()` | When you want collapsed\u002Fsize state to survive page reloads |\n\n> **Wrapping an existing service call with CircuitBreaker is a one-liner:**\n> ```typescript\n> \u002F\u002F Before\n> const data = await fetchStockQuotes();\n> \u002F\u002F After — adds caching + cooldown with zero refactor\n> const data = await breaker.execute(() => fetchStockQuotes(), []);\n> ```\n\n---\n\n## File 1 — `src\u002Fcomponents\u002FPanel.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  \u002F\u002F Retry state (used by fetchWithRetry)\n  protected retryAttempts = 0;\n  protected retryDelay = 1000; \u002F\u002F ms; doubles each attempt\n  private maxRetries = 3;\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    \u002F\u002F 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    \u002F\u002F 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    \u002F\u002F 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  \u002F\u002F ── Public API ───────────────────────────────────────────────────────\n\n  public getElement(): HTMLElement { return this.element; }\n\n  public showLoading(message = 'Loading...'): void {\n    this.content.innerHTML = `\n      \u003Cdiv class=\"panel-loading\">\n        \u003Cdiv class=\"panel-loading-spinner\">\u003C\u002Fdiv>\n        \u003Cdiv class=\"panel-loading-text\">${message}\u003C\u002Fdiv>\n      \u003C\u002Fdiv>`;\n  }\n\n  public showError(message = 'Failed to load', onRetry?: () => void): void {\n    this.content.innerHTML = `\n      \u003Cdiv class=\"panel-error-state\">\n        \u003Cdiv class=\"panel-error-msg\">${message}\u003C\u002Fdiv>\n        ${onRetry ? '\u003Cbutton class=\"panel-retry-btn\" data-panel-retry>Retry\u003C\u002Fbutton>' : ''}\n      \u003C\u002Fdiv>`;\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  public destroy(): void { this.element.remove(); }\n\n  \u002F\u002F ── Protected helpers ────────────────────────────────────────────────\n\n  protected setFetching(v: boolean): void { this._fetching = v; }\n  protected get isFetching(): boolean { return this._fetching; }\n\n  \u002F**\n   * Fetch `url` with exponential-backoff retry (up to maxRetries attempts).\n   * Reset `this.retryAttempts` and `this.retryDelay` before calling if you\n   * want a fresh retry sequence (e.g. from a user-initiated Retry button).\n   *\u002F\n  protected async fetchWithRetry(url: string): Promise\u003Cunknown> {\n    while (this.retryAttempts \u003C 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(\n            `Failed after ${this.maxRetries} attempts: ${(error as Error).message}`\n          );\n        }\n        await new Promise(resolve => setTimeout(resolve, this.retryDelay));\n        this.retryDelay *= 2;\n      }\n    }\n  }\n\n  \u002F** Persist expanded\u002Fcollapsed state and dimensions to localStorage. *\u002F\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  \u002F** Restore state saved by saveState(). *\u002F\n  public loadState(): void {\n    const raw = localStorage.getItem(`panelState_${this.panelId}`);\n    if (!raw) return;\n    const { isExpanded, width, height } = JSON.parse(raw) as {\n      isExpanded: boolean; width: string; height: string;\n    };\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---\n\n## File 2 — `src\u002Futils\u002Fcircuit-breaker.ts`\n\n```typescript\ninterface CircuitState {\n  failures: number;\n  cooldownUntil: number;\n}\n\ninterface CacheEntry\u003CT> {\n  data: T;\n  timestamp: number;\n}\n\nexport interface CircuitBreakerOptions {\n  name: string;\n  maxFailures?: number;   \u002F\u002F default 2\n  cooldownMs?: number;    \u002F\u002F default 5 min\n  cacheTtlMs?: number;    \u002F\u002F default 10 min\n}\n\nexport class CircuitBreaker\u003CT> {\n  private state: CircuitState = { failures: 0, cooldownUntil: 0 };\n  private cache: CacheEntry\u003CT> | null = null;\n  private readonly name: string;\n  private readonly maxFailures: number;\n  private readonly cooldownMs: number;\n  private readonly 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;\n    this.cacheTtlMs  = options.cacheTtlMs  ?? 10 * 60 * 1000;\n  }\n\n  isOnCooldown(): boolean {\n    if (Date.now() \u003C 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 \u003C 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(): 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}] Circuit open — cooldown ${this.cooldownMs \u002F 1000}s`);\n    }\n  }\n\n  \u002F**\n   * Execute fn. On success, caches result. On failure, records failure and\n   * returns defaultValue. While on cooldown or cache is fresh, skips fn\n   * entirely and returns cached\u002Fdefault value.\n   *\u002F\n  async execute\u003CR extends T>(fn: () => Promise\u003CR>, defaultValue: R): Promise\u003CR> {\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}] Request failed:`, e);\n      this.recordFailure();\n      return defaultValue;\n    }\n  }\n}\n\nexport function createCircuitBreaker\u003CT>(\n  options: CircuitBreakerOptions\n): CircuitBreaker\u003CT> {\n  return new CircuitBreaker\u003CT>(options);\n}\n```\n\n---\n\n## File 3 — `src\u002Futils\u002Fsparkline.ts`\n\n```typescript\n\u002F**\n * Returns an inline SVG sparkline string, or '' if data is too short.\n * Embed the return value directly in HTML template strings.\n *\u002F\nexport function miniSparkline(\n  data: number[] | undefined,\n  change: number | null,\n  w = 50,\n  h = 16\n): string {\n  if (!data || data.length \u003C 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 \u002F (data.length - 1)) * w;\n    const y = h - ((v - min) \u002F range) * (h - 2) - 1;\n    return `${x.toFixed(1)},${y.toFixed(1)}`;\n  }).join(' ');\n  return `\u003Csvg width=\"${w}\" height=\"${h}\" viewBox=\"0 0 ${w} ${h}\">` +\n    `\u003Cpolyline points=\"${points}\" fill=\"none\" stroke=\"${color}\" ` +\n    `stroke-width=\"1.2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\u002F>\u003C\u002Fsvg>`;\n}\n```\n\n---\n\n## Creating a Concrete Panel\n\n### Option A — Wrapping an existing project service with CircuitBreaker (preferred)\n\nUse this when the project already has a `fetchStockQuotes()` (or similar) function.\nThe circuit breaker is a **one-liner wrapper** — no refactoring of the service needed.\n\n```typescript\nimport { Panel } from '.\u002FPanel';\nimport { createCircuitBreaker } from '..\u002Futils\u002Fcircuit-breaker';\nimport { fetchStockQuotes } from '..\u002Fservices\u002Fstocks'; \u002F\u002F existing project service\n\ninterface StockQuote {\n  symbol: string;\n  name: string;\n  price: number | null;\n  change: number | null;\n  sparkline?: number[];\n}\n\n\u002F\u002F Module-level breaker — shared across re-renders, persists cache between fetches\nconst breaker = createCircuitBreaker\u003CStockQuote[]>({\n  name: 'StockMarket',\n  cacheTtlMs: 60_000,\n  cooldownMs: 5 * 60_000,\n});\n\nexport class StockPanel extends Panel {\n  private refreshTimer: ReturnType\u003Ctypeof setInterval> | null = null;\n\n  constructor() {\n    super({ id: 'stocks', title: 'Stock Market', showCount: true });\n    this.loadState();       \u002F\u002F restore collapsed\u002Fsize from localStorage\n    this.fetchData();\n    this.refreshTimer = setInterval(() => this.fetchData(), 60_000);\n\n    \u002F\u002F Optional: manual refresh button in header\n    const btn = document.createElement('button');\n    btn.className = 'panel-refresh-btn';\n    btn.textContent = 'Refresh';\n    btn.addEventListener('click', () => this.fetchData());\n    this.header.appendChild(btn);\n  }\n\n  private async fetchData(): Promise\u003Cvoid> {\n    if (this.isFetching) return;\n    this.setFetching(true);\n    try {\n      \u002F\u002F One-liner: wrap existing service call with circuit breaker\n      const quotes = await breaker.execute(() => fetchStockQuotes(), []);\n      this.render(quotes);\n      this.setCount(quotes.length);\n      this.saveState();\n    } catch (err) {\n      this.showError(\n        `Failed to load stock data: ${(err as Error).message}`,\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      \u003Cdiv class=\"stock-row\">\n        \u003Cspan class=\"stock-symbol\">${q.symbol}\u003C\u002Fspan>\n        \u003Cspan class=\"stock-name\">${q.name}\u003C\u002Fspan>\n        \u003Cspan class=\"stock-price\">${q.price != null ? '$' + q.price.toFixed(2) : '--'}\u003C\u002Fspan>\n        \u003Cspan class=\"stock-change ${(q.change ?? 0) >= 0 ? 'positive' : 'negative'}\">\n          ${q.change != null ? (q.change >= 0 ? '+' : '') + q.change.toFixed(2) + '%' : '--'}\n        \u003C\u002Fspan>\n      \u003C\u002Fdiv>`).join('');\n    this.setContent(`\u003Cdiv class=\"stock-list\">${rows}\u003C\u002Fdiv>`);\n  }\n\n  public override destroy(): void {\n    if (this.refreshTimer) clearInterval(this.refreshTimer);\n    super.destroy();\n  }\n}\n```\n\n### Option B — Direct fetch with `fetchWithRetry` (no existing service layer)\n\nUse this when there is no project service and you want per-request exponential backoff:\n\n```typescript\nimport { Panel } from '.\u002FPanel';\n\ninterface NewsArticle {\n  id: string;\n  title: string;\n  url: string;\n}\n\nexport class NewsPanel extends Panel {\n  private refreshTimer: ReturnType\u003Ctypeof setInterval> | null = null;\n\n  constructor() {\n    super({ id: 'news', title: 'Live News', showCount: true });\n    this.fetchData();\n    this.refreshTimer = setInterval(() => this.fetchData(), 120_000);\n  }\n\n  private async fetchData(): Promise\u003Cvoid> {\n    if (this.isFetching) return;\n    this.setFetching(true);\n    try {\n      \u002F\u002F fetchWithRetry handles up to 3 attempts with exponential backoff\n      const articles = await this.fetchWithRetry('\u002Fapi\u002Fnews') as NewsArticle[];\n      this.render(articles);\n      this.setCount(articles.length);\n    } catch (err) {\n      this.showError(\n        `News unavailable: ${(err as Error).message}`,\n        () => {\n          \u002F\u002F Reset retry counters before user-initiated retry\n          this.retryAttempts = 0;\n          this.retryDelay = 1000;\n          this.fetchData();\n        }\n      );\n    } finally {\n      this.setFetching(false);\n    }\n  }\n\n  private render(articles: NewsArticle[]): void {\n    const items = articles.map(a =>\n      `\u003Cdiv class=\"news-item\">\u003Ca href=\"${a.url}\">${a.title}\u003C\u002Fa>\u003C\u002Fdiv>`\n    ).join('');\n    this.setContent(`\u003Cdiv class=\"news-list\">${items}\u003C\u002Fdiv>`);\n  }\n\n  public override destroy(): void {\n    if (this.refreshTimer) clearInterval(this.refreshTimer);\n    super.destroy();\n  }\n}\n```\n\n### Option C — Plain fetch, no resilience layer\n\nFor non-critical panels or mock\u002Fdev data where retries add no value:\n\n```typescript\nprivate async fetchData(): Promise\u003Cvoid> {\n  if (this.isFetching) return;\n  this.setFetching(true);\n  try {\n    const resp = await fetch('\u002Fapi\u002Fconfig');\n    if (!resp.ok) throw new Error(`HTTP ${resp.status}`);\n    const data = await resp.json();\n    this.render(data);\n  } catch (err) {\n    this.showError('Could not load config', () => this.fetchData());\n  } finally {\n    this.setFetching(false);\n  }\n}\n```\n\n---\n\n## Decision Guide — Which resilience layer?\n\n```\nDoes the project have an existing fetchX() service function?\n  YES -> wrap it: breaker.execute(() => fetchX(), defaultValue)     \u003C- Option A\n  NO  -> does the endpoint fail intermittently under normal load?\n           YES -> use fetchWithRetry() for per-request retry         \u003C- Option B\n           NO  -> plain fetch() with showError\u002Fretry is enough       \u003C- Option C\n\nIs the endpoint unreliable or rate-limited for extended periods?\n  YES -> add CircuitBreaker (prevents hammering during outages)\n  NO  -> fetchWithRetry or plain fetch is sufficient\n\nDo you need panel state (size, collapsed) to survive page reloads?\n  YES -> call this.loadState() in constructor (after super())\n         call this.saveState() after a successful render\n```\n\n---\n\n## Protected API Reference\n\nAll members below are accessible from subclasses without importing or inspecting `Panel.ts` directly:\n\n| Member \u002F Method | Type | Description |\n|---|---|---|\n| `element` | `HTMLElement` | Outer container div (`.panel`) |\n| `header` | `HTMLElement` | Header bar div — append extra controls here |\n| `content` | `HTMLElement` | Content area div (`.panel-content`) |\n| `countEl` | `HTMLElement \\| null` | Count badge, or `null` if `showCount` not set |\n| `panelId` | `string` | The `id` from `PanelOptions` |\n| `retryAttempts` | `number` | Current retry count for `fetchWithRetry` |\n| `retryDelay` | `number` | Current delay (ms) for `fetchWithRetry`; reset to 1000 before retry |\n| `isFetching` | `boolean` (getter) | `true` while an async fetch is in progress |\n| `setFetching(v)` | `void` | Set the fetching guard flag |\n| `showLoading(msg?)` | `void` | Replace content with a loading spinner |\n| `showError(msg?, onRetry?)` | `void` | Replace content with an error state and optional retry button |\n| `setContent(html)` | `void` | Set raw HTML into the content area |\n| `setCount(n)` | `void` | Update the count badge (no-op if `countEl` is null) |\n| `fetchWithRetry(url)` | `Promise\u003Cunknown>` | Fetch with exponential-backoff retry (3 attempts) |\n| `saveState()` | `void` | Persist expanded\u002Fsize state to localStorage |\n| `loadState()` | `void` | Restore state from localStorage |\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 = 'Refresh';\n>   btn.addEventListener('click', () => this.generate());\n>   this.header.appendChild(btn); \u002F\u002F 'header' is protected — safe to use here\n> }\n> ```\n\n---\n\n## Key Patterns (checklist)\n\n1. **Constructor**: call `super()` with `PanelOptions`, optionally call `loadState()`, then trigger initial data fetch.\n2. **fetchData()**: async, use `isFetching` guard, wrap service calls with `breaker.execute()` when available.\n3. **render()**: build HTML strings, call `this.setContent(html)`, then `this.saveState()`.\n4. **Error recovery**: `showError(message, () => this.fetchData())` — retry button is wired automatically.\n5. **destroy()**: clear all `setInterval` \u002F `setTimeout` handles, then call `super.destroy()`.\n6. **Sparklines**: import `miniSparkline` from `src\u002Futils\u002Fsparkline.ts`, embed return value directly in HTML template strings.\n7. **Header controls**: `this.header.appendChild(el)` — `header` is `protected`, safe to use in subclasses.\n\n---\n\n## localStorage Utilities (optional helpers)\n\nSee `examples\u002FlocalStorageUtils.ts` for typed wrappers when you need to persist additional per-panel data beyond the built-in size\u002Fcollapsed state (e.g. user filter selections, last-viewed item).\n","# Resilient Panel — Unified Pattern\n\nCreate dashboard panel components using **vanilla TypeScript** (no framework, no JSX).\nEach panel is a class extending a `Panel` base class and optionally wraps its data source with a `CircuitBreaker`.\n\n---\n\n## Architecture Overview\n\n```\nPanel (base class)\n├── element: HTMLElement (.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### Resilience layers (choose what you need)\n\n| Layer | File | When to use |\n|---|---|---|\n| **isFetching guard** | `Panel` base class | Always — prevents concurrent duplicate requests |\n| **Exponential-backoff retry** | `Panel.fetchWithRetry()` | When you own the fetch call and want per-request retry before surfacing an error |\n| **CircuitBreaker** | `src\u002Futils\u002Fcircuit-breaker.ts` | When a service may fail repeatedly; stops hammering the API and serves stale cache instead |\n| **localStorage persistence** | `Panel.saveState()` \u002F `loadState()` | When you want collapsed\u002Fsize state to survive page reloads |\n\n> **Wrapping an existing service call with CircuitBreaker is a one-liner:**\n> ```typescript\n> \u002F\u002F Before\n> const data = await fetchStockQuotes();\n> \u002F\u002F After — adds caching + cooldown with zero refactor\n> const data = await breaker.execute(() => fetchStockQuotes(), []);\n> ```\n\n---\n\n## File 1 — `src\u002Fcomponents\u002FPanel.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  \u002F\u002F Retry state (used by fetchWithRetry)\n  protected retryAttempts = 0;\n  protected retryDelay = 1000; \u002F\u002F ms; doubles each attempt\n  private maxRetries = 3;\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    \u002F\u002F 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    \u002F\u002F 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    \u002F\u002F 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  \u002F\u002F ── Public API ───────────────────────────────────────────────────────\n\n  public getElement(): HTMLElement { return this.element; }\n\n  public showLoading(message = 'Loading...'): void {\n    this.content.innerHTML = `\n      \u003Cdiv class=\"panel-loading\">\n        \u003Cdiv class=\"panel-loading-spinner\">\u003C\u002Fdiv>\n        \u003Cdiv class=\"panel-loading-text\">${message}\u003C\u002Fdiv>\n      \u003C\u002Fdiv>`;\n  }\n\n  public showError(message = 'Failed to load', onRetry?: () => void): void {\n    this.content.innerHTML = `\n      \u003Cdiv class=\"panel-error-state\">\n        \u003Cdiv class=\"panel-error-msg\">${message}\u003C\u002Fdiv>\n        ${onRetry ? '\u003Cbutton class=\"panel-retry-btn\" data-panel-retry>Retry\u003C\u002Fbutton>' : ''}\n      \u003C\u002Fdiv>`;\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  public destroy(): void { this.element.remove(); }\n\n  \u002F\u002F ── Protected helpers ────────────────────────────────────────────────\n\n  protected setFetching(v: boolean): void { this._fetching = v; }\n  protected get isFetching(): boolean { return this._fetching; }\n\n  \u002F**\n   * Fetch `url` with exponential-backoff retry (up to maxRetries attempts).\n   * Reset `this.retryAttempts` and `this.retryDelay` before calling if you\n   * want a fresh retry sequence (e.g. from a user-initiated Retry button).\n   *\u002F\n  protected async fetchWithRetry(url: string): Promise\u003Cunknown> {\n    while (this.retryAttempts \u003C 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(\n            `Failed after ${this.maxRetries} attempts: ${(error as Error).message}`\n          );\n        }\n        await new Promise(resolve => setTimeout(resolve, this.retryDelay));\n        this.retryDelay *= 2;\n      }\n    }\n  }\n\n  \u002F** Persist expanded\u002Fcollapsed state and dimensions to localStorage. *\u002F\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  \u002F** Restore state saved by saveState(). *\u002F\n  public loadState(): void {\n    const raw = localStorage.getItem(`panelState_${this.panelId}`);\n    if (!raw) return;\n    const { isExpanded, width, height } = JSON.parse(raw) as {\n      isExpanded: boolean; width: string; height: string;\n    };\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---\n\n## File 2 — `src\u002Futils\u002Fcircuit-breaker.ts`\n\n```typescript\ninterface CircuitState {\n  failures: number;\n  cooldownUntil: number;\n}\n\ninterface CacheEntry\u003CT> {\n  data: T;\n  timestamp: number;\n}\n\nexport interface CircuitBreakerOptions {\n  name: string;\n  maxFailures?: number;   \u002F\u002F default 2\n  cooldownMs?: number;    \u002F\u002F default 5 min\n  cacheTtlMs?: number;    \u002F\u002F default 10 min\n}\n\nexport class CircuitBreaker\u003CT> {\n  private state: CircuitState = { failures: 0, cooldownUntil: 0 };\n  private cache: CacheEntry\u003CT> | null = null;\n  private readonly name: string;\n  private readonly maxFailures: number;\n  private readonly cooldownMs: number;\n  private readonly 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;\n    this.cacheTtlMs  = options.cacheTtlMs  ?? 10 * 60 * 1000;\n  }\n\n  isOnCooldown(): boolean {\n    if (Date.now() \u003C 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 \u003C 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(): 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}] Circuit open — cooldown ${this.cooldownMs \u002F 1000}s`);\n    }\n  }\n\n  \u002F**\n   * Execute fn. On success, caches result. On failure, records failure and\n   * returns defaultValue. While on cooldown or cache is fresh, skips fn\n   * entirely and returns cached\u002Fdefault value.\n   *\u002F\n  async execute\u003CR extends T>(fn: () => Promise\u003CR>, defaultValue: R): Promise\u003CR> {\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}] Request failed:`, e);\n      this.recordFailure();\n      return defaultValue;\n    }\n  }\n}\n\nexport function createCircuitBreaker\u003CT>(\n  options: CircuitBreakerOptions\n): CircuitBreaker\u003CT> {\n  return new CircuitBreaker\u003CT>(options);\n}\n```\n\n---\n\n## File 3 — `src\u002Futils\u002Fsparkline.ts`\n\n```typescript\n\u002F**\n * Returns an inline SVG sparkline string, or '' if data is too short.\n * Embed the return value directly in HTML template strings.\n *\u002F\nexport function miniSparkline(\n  data: number[] | undefined,\n  change: number | null,\n  w = 50,\n  h = 16\n): string {\n  if (!data || data.length \u003C 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 \u002F (data.length - 1)) * w;\n    const y = h - ((v - min) \u002F range) * (h - 2) - 1;\n    return `${x.toFixed(1)},${y.toFixed(1)}`;\n  }).join(' ');\n  return `\u003Csvg width=\"${w}\" height=\"${h}\" viewBox=\"0 0 ${w} ${h}\">` +\n    `\u003Cpolyline points=\"${points}\" fill=\"none\" stroke=\"${color}\" ` +\n    `stroke-width=\"1.2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\u002F>\u003C\u002Fsvg>`;\n}\n```\n\n---\n\n## Creating a Concrete Panel\n\n### Option A — Wrapping an existing project service with CircuitBreaker (preferred)\n\nUse this when the project already has a `fetchStockQuotes()` (or similar) function.\nThe circuit breaker is a **one-liner wrapper** — no refactoring of the service needed.\n\n```typescript\nimport { Panel } from '.\u002FPanel';\nimport { createCircuitBreaker } from '..\u002Futils\u002Fcircuit-breaker';\nimport { fetchStockQuotes } from '..\u002Fservices\u002Fstocks'; \u002F\u002F existing project service\n\ninterface StockQuote {\n  symbol: string;\n  name: string;\n  price: number | null;\n  change: number | null;\n  sparkline?: number[];\n}\n\n\u002F\u002F Module-level breaker — shared across re-renders, persists cache between fetches\nconst breaker = createCircuitBreaker\u003CStockQuote[]>({\n  name: 'StockMarket',\n  cacheTtlMs: 60_000,\n  cooldownMs: 5 * 60_000,\n});\n\nexport class StockPanel extends Panel {\n  private refreshTimer: ReturnType\u003Ctypeof setInterval> | null = null;\n\n  constructor() {\n    super({ id: 'stocks', title: 'Stock Market', showCount: true });\n    this.loadState();       \u002F\u002F restore collapsed\u002Fsize from localStorage\n    this.fetchData();\n    this.refreshTimer = setInterval(() => this.fetchData(), 60_000);\n\n    \u002F\u002F Optional: manual refresh button in header\n    const btn = document.createElement('button');\n    btn.className = 'panel-refresh-btn';\n    btn.textContent = 'Refresh';\n    btn.addEventListener('click', () => this.fetchData());\n    this.header.appendChild(btn);\n  }\n\n  private async fetchData(): Promise\u003Cvoid> {\n    if (this.isFetching) return;\n    this.setFetching(true);\n    try {\n      \u002F\u002F One-liner: wrap existing service call with circuit breaker\n      const quotes = await breaker.execute(() => fetchStockQuotes(), []);\n      this.render(quotes);\n      this.setCount(quotes.length);\n      this.saveState();\n    } catch (err) {\n      this.showError(\n        `Failed to load stock data: ${(err as Error).message}`,\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      \u003Cdiv class=\"stock-row\">\n        \u003Cspan class=\"stock-symbol\">${q.symbol}\u003C\u002Fspan>\n        \u003Cspan class=\"stock-name\">${q.name}\u003C\u002Fspan>\n        \u003Cspan class=\"stock-price\">${q.price != null ? '$' + q.price.toFixed(2) : '--'}\u003C\u002Fspan>\n        \u003Cspan class=\"stock-change ${(q.change ?? 0) >= 0 ? 'positive' : 'negative'}\">\n          ${q.change != null ? (q.change >= 0 ? '+' : '') + q.change.toFixed(2) + '%' : '--'}\n        \u003C\u002Fspan>\n      \u003C\u002Fdiv>`).join('');\n    this.setContent(`\u003Cdiv class=\"stock-list\">${rows}\u003C\u002Fdiv>`);\n  }\n\n  public override destroy(): void {\n    if (this.refreshTimer) clearInterval(this.refreshTimer);\n    super.destroy();\n  }\n}\n```\n\n### Option B — Direct fetch with `fetchWithRetry` (no existing service layer)\n\nUse this when there is no project service and you want per-request exponential backoff:\n\n```typescript\nimport { Panel } from '.\u002FPanel';\n\ninterface NewsArticle {\n  id: string;\n  title: string;\n  url: string;\n}\n\nexport class NewsPanel extends Panel {\n  private refreshTimer: ReturnType\u003Ctypeof setInterval> | null = null;\n\n  constructor() {\n    super({ id: 'news', title: 'Live News', showCount: true });\n    this.fetchData();\n    this.refreshTimer = setInterval(() => this.fetchData(), 120_000);\n  }\n\n  private async fetchData(): Promise\u003Cvoid> {\n    if (this.isFetching) return;\n    this.setFetching(true);\n    try {\n      \u002F\u002F fetchWithRetry handles up to 3 attempts with exponential backoff\n      const articles = await this.fetchWithRetry('\u002Fapi\u002Fnews') as NewsArticle[];\n      this.render(articles);\n      this.setCount(articles.length);\n    } catch (err) {\n      this.showError(\n        `News unavailable: ${(err as Error).message}`,\n        () => {\n          \u002F\u002F Reset retry counters before user-initiated retry\n          this.retryAttempts = 0;\n          this.retryDelay = 1000;\n          this.fetchData();\n        }\n      );\n    } finally {\n      this.setFetching(false);\n    }\n  }\n\n  private render(articles: NewsArticle[]): void {\n    const items = articles.map(a =>\n      `\u003Cdiv class=\"news-item\">\u003Ca href=\"${a.url}\">${a.title}\u003C\u002Fa>\u003C\u002Fdiv>`\n    ).join('');\n    this.setContent(`\u003Cdiv class=\"news-list\">${items}\u003C\u002Fdiv>`);\n  }\n\n  public override destroy(): void {\n    if (this.refreshTimer) clearInterval(this.refreshTimer);\n    super.destroy();\n  }\n}\n```\n\n### Option C — Plain fetch, no resilience layer\n\nFor non-critical panels or mock\u002Fdev data where retries add no value:\n\n```typescript\nprivate async fetchData(): Promise\u003Cvoid> {\n  if (this.isFetching) return;\n  this.setFetching(true);\n  try {\n    const resp = await fetch('\u002Fapi\u002Fconfig');\n    if (!resp.ok) throw new Error(`HTTP ${resp.status}`);\n    const data = await resp.json();\n    this.render(data);\n  } catch (err) {\n    this.showError('Could not load config', () => this.fetchData());\n  } finally {\n    this.setFetching(false);\n  }\n}\n```\n\n---\n\n## Decision Guide — Which resilience layer?\n\n```\nDoes the project have an existing fetchX() service function?\n  YES -> wrap it: breaker.execute(() => fetchX(), defaultValue)     \u003C- Option A\n  NO  -> does the endpoint fail intermittently under normal load?\n           YES -> use fetchWithRetry() for per-request retry         \u003C- Option B\n           NO  -> plain fetch() with showError\u002Fretry is enough       \u003C- Option C\n\nIs the endpoint unreliable or rate-limited for extended periods?\n  YES -> add CircuitBreaker (prevents hammering during outages)\n  NO  -> fetchWithRetry or plain fetch is sufficient\n\nDo you need panel state (size, collapsed) to survive page reloads?\n  YES -> call this.loadState() in constructor (after super())\n         call this.saveState() after a successful render\n```\n\n---\n\n## Protected API Reference\n\nAll members below are accessible from subclasses without importing or inspecting `Panel.ts` directly:\n\n| Member \u002F Method | Type | Description |\n|---|---|---|\n| `element` | `HTMLElement` | Outer container div (`.panel`) |\n| `header` | `HTMLElement` | Header bar div — append extra controls here |\n| `content` | `HTMLElement` | Content area div (`.panel-content`) |\n| `countEl` | `HTMLElement \\| null` | Count badge, or `null` if `showCount` not set |\n| `panelId` | `string` | The `id` from `PanelOptions` |\n| `retryAttempts` | `number` | Current retry count for `fetchWithRetry` |\n| `retryDelay` | `number` | Current delay (ms) for `fetchWithRetry`; reset to 1000 before retry |\n| `isFetching` | `boolean` (getter) | `true` while an async fetch is in progress |\n| `setFetching(v)` | `void` | Set the fetching guard flag |\n| `showLoading(msg?)` | `void` | Replace content with a loading spinner |\n| `showError(msg?, onRetry?)` | `void` | Replace content with an error state and optional retry button |\n| `setContent(html)` | `void` | Set raw HTML into the content area |\n| `setCount(n)` | `void` | Update the count badge (no-op if `countEl` is null) |\n| `fetchWithRetry(url)` | `Promise\u003Cunknown>` | Fetch with exponential-backoff retry (3 attempts) |\n| `saveState()` | `void` | Persist expanded\u002Fsize state to localStorage |\n| `loadState()` | `void` | Restore state from localStorage |\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 = 'Refresh';\n>   btn.addEventListener('click', () => this.generate());\n>   this.header.appendChild(btn); \u002F\u002F 'header' is protected — safe to use here\n> }\n> ```\n\n---\n\n## Key Patterns (checklist)\n\n1. **Constructor**: call `super()` with `PanelOptions`, optionally call `loadState()`, then trigger initial data fetch.\n2. **fetchData()**: async, use `isFetching` guard, wrap service calls with `breaker.execute()` when available.\n3. **render()**: build HTML strings, call `this.setContent(html)`, then `this.saveState()`.\n4. **Error recovery**: `showError(message, () => this.fetchData())` — retry button is wired automatically.\n5. **destroy()**: clear all `setInterval` \u002F `setTimeout` handles, then call `super.destroy()`.\n6. **Sparklines**: import `miniSparkline` from `src\u002Futils\u002Fsparkline.ts`, embed return value directly in HTML template strings.\n7. **Header controls**: `this.header.appendChild(el)` — `header` is `protected`, safe to use in subclasses.\n\n---\n\n## localStorage Utilities (optional helpers)\n\nSee `examples\u002FlocalStorageUtils.ts` for typed wrappers when you need to persist additional per-panel data beyond the built-in size\u002Fcollapsed state (e.g. user filter selections, last-viewed item).\n",{"name":5,"description":7},"main","ff671ef61ed6587c49bd54c81cdc751c0b99e363","showcase\u002Fskills\u002Fpanel-breaker-backoff","https:\u002F\u002Fgithub.com\u002FAG064\u002FOpenSpace","https:\u002F\u002Fgithub.com\u002FAG064","AG064, E.Mi.\r\n\r\nA person who thinks all the time, yet all they have to think is nothing. I want to improve the world and work on open source projects","2026-10-08 06:53:48",{"source":8,"skillName":5,"commands":51,"prompt":61,"zipAvailable":62},[52,55,58],{"tool":53,"command":54},"npx","npx skills add AG064\u002FOpenSpace --skill panel-breaker-backoff",{"tool":56,"command":57},"bunx","bunx skills add AG064\u002FOpenSpace --skill panel-breaker-backoff",{"tool":59,"command":60},"pnpm","pnpm dlx skills add AG064\u002FOpenSpace --skill panel-breaker-backoff","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002FAG064\u002FOpenSpace\u002Fblob\u002Fmain\u002Fshowcase\u002Fskills\u002Fpanel-breaker-backoff\u002FSKILL.md",true,[64],{"path":65,"resourceType":66,"resourceTypeDesc":67,"sizeBytes":68,"sourceHtmlUrl":69},".skill_id","OTHER","其他",52,"https:\u002F\u002Fgithub.com\u002FAG064\u002FOpenSpace\u002Fblob\u002Fmain\u002Fshowcase\u002Fskills\u002Fpanel-breaker-backoff\u002F.skill_id",[71,92,108,117,144,160],{"skillNo":72,"name":73,"nameZh":6,"description":74,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":9,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":75,"categories":76,"keywords":77,"publishedAt":38},"01M6KKMPWA0B5SCCH766F8M5EA","write-special-chars-ts-check","Workaround for write_file failures caused by special characters (apostrophes, backticks, template literals) using a Python heredoc script, plus a reliable TypeScript type-checking step that filters output to only the newly-written files to avoid being misled by pre-existing errors.","https:\u002F\u002Fgithub.com\u002FAG064\u002FOpenSpace\u002Fblob\u002Fmain\u002Fshowcase\u002Fskills\u002Fwrite-special-chars-ts-check\u002FSKILL.md",[],[78,79,80,81,22,82,23,83,84,85,86,87,88,89,90,91,28],"write special chars ts check","check","write","filters","type-checking","errors","script","output","type","files","only","template","file","python",{"skillNo":93,"name":94,"nameZh":6,"description":95,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":9,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":96,"categories":97,"keywords":98,"publishedAt":38},"01M6KKMJXVBNVEY3TRK055F4A7","write-special-chars-readback","Workaround for write_file failures caused by special characters (apostrophes, backticks, template literals) using a Python heredoc script, with mandatory post-write verification via read_file and cleanup of \u002Ftmp helper scripts.","https:\u002F\u002Fgithub.com\u002FAG064\u002FOpenSpace\u002Fblob\u002Fmain\u002Fshowcase\u002Fskills\u002Fwrite-special-chars-readback\u002FSKILL.md",[],[99,100,80,89,101,102,90,91,103,104,105,106,107,23,84,25],"readback","write special chars readback","cleanup","read","scripts","verification","via","post","helper",{"skillNo":109,"name":110,"nameZh":6,"description":111,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":9,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":112,"categories":113,"keywords":114,"publishedAt":38},"01M6KKMEGX9M9NC105J0W3Q5B9","write-special-chars-heredoc","Workaround for write_file failures caused by special characters (apostrophes, backticks, template literals) by using a Python heredoc script executed via shell.","https:\u002F\u002Fgithub.com\u002FAG064\u002FOpenSpace\u002Fblob\u002Fmain\u002Fshowcase\u002Fskills\u002Fwrite-special-chars-heredoc\u002FSKILL.md",[],[80,115,91,23,105,89,116,90,84],"write special chars heredoc","shell",{"skillNo":118,"name":119,"nameZh":6,"description":120,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":9,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":121,"categories":122,"keywords":128,"publishedAt":38},"01M6KKM8EJ2AATVHZBNB916WT0","worldmonitor-reference","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.","https:\u002F\u002Fgithub.com\u002FAG064\u002FOpenSpace\u002Fblob\u002Fmain\u002Fshowcase\u002Fskills\u002Fworldmonitor-reference\u002FSKILL.md",[123],{"categoryNo":124,"categoryName":125,"taxonomyType":126,"taxonomyTypeDesc":127},"01JBSEEDCAT000000000000033","数据分析师","PROFESSION","适用职业",[129,130,131,37,28,132,35,133,134,135,23,136,20,137,138,139,140,141,142,143],"reference","worldmonitor","worldmonitor reference","css","extracting","hierarchy","source","layer","patterns","edge functions","integrations","open","api","data","scheduler",{"skillNo":145,"name":146,"nameZh":6,"description":147,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":9,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":148,"categories":149,"keywords":150,"publishedAt":38},"01M6KKM39PHYK627F19GE2B9XN","virtual-list","Implement efficient virtual scrolling for rendering large lists with DOM recycling, chunk-based rendering, and performance optimizations","https:\u002F\u002Fgithub.com\u002FAG064\u002FOpenSpace\u002Fblob\u002Fmain\u002Fshowcase\u002Fskills\u002Fvirtual-list\u002FSKILL.md",[],[151,152,153,154,23,155,25,156,157,158,159],"virtual list","list","virtual","chunk","dom","rendering","recycling","implement","performance",{"skillNo":161,"name":162,"nameZh":6,"description":163,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":9,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":164,"categories":165,"keywords":166,"publishedAt":38},"01M6KKKY7T2P61Y0JH54CDEJKQ","verify-directory-structure","Diagnose and resolve file access issues by first verifying the directory structure.","https:\u002F\u002Fgithub.com\u002FAG064\u002FOpenSpace\u002Fblob\u002Fmain\u002Fshowcase\u002Fskills\u002Fverify-directory-structure\u002FSKILL.md",[],[167,168,169,170,171,172,90,173,174,175],"directory","verify","structure","verify directory structure","access","first","issues","diagnose","file access"]