{"record":{"id":"28233c410d574133","repo":"odysseus-dev/odysseus","slug":"failed-to-load-skill-md","errorCode":null,"errorMessage":"Failed to load SKILL.md","messagePattern":"Failed to load SKILL\\.md","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"warning","filePath":"static/js/skills.js","lineNumber":40,"sourceCode":"let _cascadeNext = false;   // set true to play the domino-in entrance on the next render\n\nfunction _playSkillsCascade(container = document.getElementById('skills-list')) {\n  if (!container || !container.querySelector('.skill-card')) return false;\n  container.classList.remove('doclib-just-opened');\n  void container.offsetWidth;\n  container.classList.add('doclib-just-opened');\n  setTimeout(() => container.classList.remove('doclib-just-opened'), 900);\n  return true;\n}\n\n// Cache of SKILL.md text by skill name, so expanding is instant (no async\n// fetch + content-settle jump). Populated lazily on expand AND eagerly in\n// the background for all visible cards right after render.\nconst _mdCache = new Map();\nasync function _fetchSkillMarkdown(name) {\n  if (_mdCache.has(name)) return _mdCache.get(name);\n  const res = await fetch(`${API}/api/skills/${encodeURIComponent(name)}/markdown`);\n  if (!res.ok) throw new Error(`HTTP ${res.status}`);\n  const data = await res.json();\n  const md = data.markdown || '';\n  _mdCache.set(name, md);\n  return md;\n}\n// Background-load the markdown for every currently-rendered skill card so it\n// is ready (in the card's <pre> + _mdLoaded) before the user expands it.\nfunction _preloadVisibleMarkdown() {\n  document.querySelectorAll('#skills-list .skill-card[data-skill-name]').forEach(card => {\n    const name = card.dataset.skillName;\n    if (!name || card._mdLoaded) return;\n    const pre = card.querySelector('.skill-md-pre');\n    const apply = (md) => { if (pre) pre.textContent = md || '(empty)'; card._mdLoaded = true; card._md = md || ''; };\n    if (_mdCache.has(name)) { apply(_mdCache.get(name)); return; }\n    _fetchSkillMarkdown(name).then(apply).catch(() => {});\n  });\n}\n","sourceCodeStart":22,"sourceCodeEnd":58,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/static/js/skills.js#L22-L58","documentation":"Raised inside _fetchSkillMarkdown (static/js/skills.js:40) when GET /api/skills/{name}/markdown returns non-2xx; the caller catches it and shows 'Failed to load SKILL.md'. The function caches markdown by skill name in _mdCache, so once one request fails the failure is not cached — every expand retries the fetch.","triggerScenarios":"Expanding a skill card (or the background _preloadVisibleMarkdown sweep) requesting the skill's markdown. 404 when the skill directory/SKILL.md does not exist (deleted on disk, draft never materialized, name mismatch); 401 when the session expired; 500 when the file read fails server-side.","commonSituations":"Skill folder renamed or deleted on disk while the list still shows it; URL-encoded skill names with slashes or special characters mismatching the route; skills directory mounted read-only or missing in a container; stale page after server-side skill changes.","solutions":["Check GET /api/skills/{name}/markdown status: 404 → the SKILL.md is gone, refresh the skill list (loadSkills) to reconcile; 401 → re-login; 500 → server log.","Confirm the skill name is exactly the directory name (encodeURIComponent handles spaces, but not route-parameter slashes).","Retry the expand once after refreshing — the failed attempt is not cached, so a refresh-and-expand recovers.","If it persists, verify the skills directory path in server config exists and is readable by the server process."],"exampleFix":"// before\nconst res = await fetch(`${API}/api/skills/${encodeURIComponent(name)}/markdown`);\nif (!res.ok) throw new Error(`HTTP ${res.status}`);\n// after — cache 404s negatively so preloads stop hammering a dead skill\nconst res = await fetch(`${API}/api/skills/${encodeURIComponent(name)}/markdown`);\nif (!res.ok) {\n  if (res.status === 404) _mdCache.set(name, null); // negative cache\n  throw new Error(`HTTP ${res.status}`);\n}","handlingStrategy":"fallback","validationCode":"if (_mdCache.has(name)) { const cached = _mdCache.get(name); if (cached === null) return; /* known-missing */ }","typeGuard":null,"tryCatchPattern":"try { const md = await _fetchSkillMarkdown(name); } catch (e) { /* show inline error with e.message (HTTP N); do NOT cache the failure so a retry after refresh works */ }","preventionTips":["Negative-cache 404s so background preloading stops re-fetching dead skills.","Refresh the skills list (loadSkills) before retrying — most 404s are stale list entries.","Include the HTTP status in the user-facing message.","Verify skill names match on-disk directories exactly."],"tags":["fetch","api","http","skills","caching"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}