coursecode 0.1.6 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,7 +4,6 @@
4
4
 
5
5
  **Related Docs:**
6
6
  - `COURSE_AUTHORING_GUIDE.md` - For course authors (not framework devs)
7
-
8
7
  - `DATA_MODEL.md` - Complete learner data schemas and storage architecture
9
8
 
10
9
  ---
@@ -143,7 +142,7 @@ const stampedHtml = stampFormat(indexHtml, 'scorm2004');
143
142
  const { filename, content } = generateManifest('scorm2004', config, files, options);
144
143
  ```
145
144
 
146
- Both are pure Node utilities
145
+ Both are pure Node utilities.
147
146
 
148
147
  #### Key Files
149
148
 
@@ -574,6 +573,7 @@ This codebase serves two roles:
574
573
  |------|---------|---------|
575
574
  | `vite.framework-dev.config.js` | Framework developers (this repo) | Builds from `template/course/`, references `lib/` directly |
576
575
  | `template/vite.config.js` | Course authors (their project) | Builds from `course/`, imports from `coursecode` package |
576
+
577
577
  ### Preview Architecture
578
578
 
579
579
  Preview is **not** part of the build output. It is platform infrastructure, served separately from `dist/`:
@@ -599,7 +599,7 @@ User: coursecode build → uploads dist/
599
599
  Cloud Preview Cloud ZIP (SCORM 2004) Cloud ZIP (SCORM 1.2)
600
600
  ┌────────────┐ ┌──────────────────┐ ┌──────────────────┐
601
601
  │ Cloud's own │ │ Copy dist/ │ │ Copy dist/ │
602
- │ stub player │ │ stampFormatInHtml│ │ stampFormatInHtml
602
+ │ stub player │ │ stampFormat │ │ stampFormat
603
603
  │ iframes the │ │ generateManifest │ │ generateManifest │
604
604
  │ uploaded │ │ → ZIP │ │ → ZIP │
605
605
  │ dist/ │ └──────────────────┘ └──────────────────┘
@@ -609,6 +609,7 @@ User: coursecode build → uploads dist/
609
609
  **Cloud dependencies:** The cloud app imports `stampFormat` and `generateManifest` directly from the `coursecode` npm package. These are pure functions — no filesystem, no Vite, no dynamic imports of user code, no `eval`. All inputs (title, version, file list) come from scanning the uploaded `dist/` or the cloud's own database.
610
610
 
611
611
  **Security boundary:** The cloud never executes `course-config.js` or any user-authored JavaScript. The meta tag and manifest are the only format-specific artifacts, and both are generated from trusted framework source code.
612
+
612
613
  ---
613
614
 
614
615
  ## Course Validation
@@ -819,9 +820,6 @@ environment: {
819
820
  **Interactions**: `{id}-check-answer`, `{id}-reset`, `{id}-controls`, `{id}-feedback`, `{id}-choice-{index}`, `{id}-blank-{index}`, `{id}-input`, `{id}-drag-item-{itemId}`, `{id}-drop-zone-{zoneId}`
820
821
 
821
822
  **Assessments**: `assessment-start`, `assessment-nav-{prev|next}`, `assessment-submit`, `assessment-retake`, `assessment-review-question-{index}`
822
-
823
-
824
-
825
823
  ---
826
824
 
827
825
  ## MCP Integration (AI Agent Control)
@@ -833,7 +831,7 @@ The MCP server runs a **persistent headless Chrome** internally via `puppeteer-c
833
831
  The MCP does **not** start or manage the preview server. The preview must be running before using runtime tools:
834
832
 
835
833
  - **Human**: run `coursecode preview` (or `npm run preview`) in a terminal
836
- - **AI agent**: use `run_command` to execute `npm run preview`
834
+ - **AI agent**: use your terminal/command execution tool to run `npm run preview`
837
835
 
838
836
  If the preview is not running, runtime tools fail fast with a clear error message.
839
837
 
@@ -856,12 +854,12 @@ If the preview is not running, runtime tools fail fast with a clear error messag
856
854
 
857
855
  | Tool | Purpose | Returns |
858
856
  |------|---------|--------|
859
- | `coursecode_state` | Full course snapshot | `{slide, structure, interactions, engagement, lmsState, errors}` |
860
- | `coursecode_navigate` | Go to slide by ID | `{slideId}` new state |
857
+ | `coursecode_state` | Full course snapshot | `{slide, toc, interactions, engagement, lmsState, apiLog, errors, frameworkLogs, consoleLogs}` |
858
+ | `coursecode_navigate` | Go to slide by ID | `{slide, interactions, engagement, accessibility}` |
861
859
  | `coursecode_interact` | Set response + evaluate | `{interactionId, response}` → `{correct, score, feedback}` |
862
860
  | `coursecode_screenshot` | Visual capture (JPEG) | Optional `slideId` to navigate first, `fullPage` for scroll capture |
863
861
  | `coursecode_viewport` | Set viewport size | Breakpoint name or `{width, height}` → persists until changed |
864
- | `coursecode_reset` | Clear learner state | `{hard?: boolean}` reload |
862
+ | `coursecode_reset` | Clear learner state | No input; clears local state and reloads |
865
863
 
866
864
  ### Screenshot Quality Modes
867
865
 
@@ -881,6 +879,8 @@ Use `coursecode_viewport` for responsive design testing. Two input modes:
881
879
 
882
880
  The viewport **persists** until explicitly changed again. Default is 1280×720.
883
881
 
882
+ > **AI tip:** For realistic mobile QA, use explicit phone dimensions (for example `{width: 375, height: 812}`) in addition to named breakpoints.
883
+
884
884
  ### Navigation API
885
885
 
886
886
  Use MCP tools for all course interaction — never use external browser tools:
@@ -902,6 +902,18 @@ MCP Server (IDE) ──puppeteer──▶ Headless Chrome ──HTTP──▶ Pr
902
902
  - **Preview not running?** → Tools return clear error: "Start preview server first"
903
903
  - **Chrome not found?** → Install Google Chrome or set `CHROME_PATH` env var
904
904
 
905
+ ### Pre-Release Responsive Checks (Framework)
906
+
907
+ Before merging responsive/layout changes:
908
+
909
+ ```bash
910
+ npm run prerelease:check
911
+ npm run smoke:responsive -- --profile=expanded
912
+ ```
913
+
914
+ - `lint:responsive` guards `responsive.css` ownership (no shell/chrome selectors)
915
+ - `lint:responsive:structure` enforces layout exclusions/scoping for high-risk shell selectors
916
+
905
917
  ---
906
918
 
907
919
  ## Audio Manager (Internals)
@@ -983,6 +995,8 @@ eventBus.on('assessment:retake', ({ id, attemptNumber }) => { });
983
995
  | `components/*.css` | Individual UI component styles (cards, hero, tabs, steps, timeline, etc.) |
984
996
  | `interactions/*.css` | Interaction-specific styles |
985
997
  | `utilities/*.css` | Utility classes (spacing, display, flex) |
998
+ | `responsive.css` | Shared content/component responsive rules (non-shell) |
999
+ | `responsive-structure.css` | App shell/header/footer/nav/audio responsive rules |
986
1000
  | `framework.css` | Main import file, orchestrates all modules |
987
1001
 
988
1002
  ### Layout System
@@ -1002,11 +1016,18 @@ CSS-only layouts controlled via `data-layout` attribute on `<html>`. Files in `f
1002
1016
 
1003
1017
  Set via `layout` in `course-config.js`. The `main.js` automatically applies the attribute from config.
1004
1018
 
1019
+ ### Responsive CSS Ownership
1020
+
1021
+ - Put shared content/component responsive rules in `framework/css/responsive.css`.
1022
+ - Put shell/chrome responsive rules (`#app`, header/brand, footer/nav/audio) in `framework/css/responsive-structure.css`.
1023
+ - For generic shell selectors, exclude layout-owned behavior (especially `article` and `focused`) unless explicitly layout-scoped.
1024
+
1005
1025
  ### Auto-Wrapping
1006
1026
 
1007
1027
  Slides are automatically wrapped with content width class (default: `.content-medium`).
1008
1028
 
1009
1029
  Override per-slide with `data-content-width` attribute or globally via `slideDefaults.contentWidth` in `course-config.js`.
1030
+
1010
1031
  ### Document Gallery
1011
1032
 
1012
1033
  `framework/js/navigation/document-gallery.js` — Collapsible sidebar gallery for reference documents.
@@ -286,8 +286,9 @@ Once connected, your AI assistant gains these capabilities:
286
286
 
287
287
  | Tool | What It Does |
288
288
  |------|--------------|
289
- | `coursecode_state` | Get the full course state — current slide, structure, interactions, engagement, errors |
289
+ | `coursecode_state` | Get the full course state — current slide, TOC, interactions, engagement, LMS state, logs, and errors |
290
290
  | `coursecode_navigate` | Jump to any slide by ID |
291
+ | `coursecode_viewport` | Set the preview viewport (named breakpoint or explicit width/height) for responsive testing |
291
292
  | `coursecode_screenshot` | Take a screenshot of any slide |
292
293
  | `coursecode_interact` | Answer an interaction and check if it's correct |
293
294
  | `coursecode_reset` | Clear progress and restart the course |
@@ -296,6 +297,7 @@ Once connected, your AI assistant gains these capabilities:
296
297
  | `coursecode_interaction_catalog` | Browse available interaction types (multiple choice, drag-drop, etc.) |
297
298
  | `coursecode_css_catalog` | Browse available CSS classes by category |
298
299
  | `coursecode_icon_catalog` | Browse available icons by name/category |
300
+ | `coursecode_export_content` | Export course content/interactions as Markdown or JSON for review |
299
301
  | `coursecode_workflow_status` | Get guidance on what to do next based on your project's current state |
300
302
  | `coursecode_build` | Build the course for LMS deployment |
301
303
 
@@ -6,6 +6,8 @@
6
6
  */
7
7
 
8
8
  // Map pixel values to size class names
9
+ import { logger } from './logger.js';
10
+
9
11
  const SIZE_MAP = {
10
12
  12: 'xs',
11
13
  16: 'sm',
@@ -99,14 +101,14 @@ class IconManager {
99
101
 
100
102
  // Check if it's an emoji - warn but handle gracefully
101
103
  if (this._isEmoji(name)) {
102
- console.warn(`[IconManager] Emoji "${name}" passed to getIcon(). Use getEmoji() instead.`);
104
+ logger.warn(`[IconManager] Emoji "${name}" passed to getIcon(). Use getEmoji() instead.`);
103
105
  const size = typeof options === 'object' ? options.size : undefined;
104
106
  const extraClass = typeof options === 'object' ? options.class : (typeof options === 'string' ? options : '');
105
107
  return this.getEmoji(name, size, extraClass);
106
108
  }
107
109
 
108
110
  // Warn about unknown icons but don't block course loading - return a placeholder
109
- console.warn(`[IconManager] Unknown icon: "${name}". Add it to DEFAULT_ICONS or course/icons.js.`);
111
+ logger.warn(`[IconManager] Unknown icon: "${name}". Add it to DEFAULT_ICONS or course/icons.js.`);
110
112
  const classes = ['icon', `icon-${name}`, 'icon-missing', sizeClass, className].filter(Boolean).join(' ');
111
113
  // Return a simple question mark circle as placeholder
112
114
  return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="${color}" stroke-width="${strokeWidth}" stroke-linecap="round" stroke-linejoin="round" class="${classes}" aria-hidden="true"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><path d="M12 17h.01"/></svg>`;
package/lib/cloud.js CHANGED
@@ -583,6 +583,10 @@ export async function deploy(options = {}) {
583
583
  formData.append('file', new Blob([zipBuffer], { type: 'application/zip' }), 'deploy.zip');
584
584
  formData.append('orgId', orgId);
585
585
 
586
+ if (options.message) {
587
+ formData.append('message', options.message);
588
+ }
589
+
586
590
  if (options.preview && options.password) {
587
591
  const pw = await prompt(' Preview password: ');
588
592
  formData.append('password', pw);
@@ -169,7 +169,7 @@ class HeadlessBrowser {
169
169
  // CourseCodeAutomation exists early but .ready is set after full boot
170
170
  await this.courseFrame.waitForFunction(
171
171
  () => window.CourseCodeAutomation?.ready === true,
172
- { timeout: 10000 }
172
+ { timeout: 30000 }
173
173
  );
174
174
  }
175
175
 
@@ -497,7 +497,7 @@ This creates a course/ directory with the starter template.
497
497
  After creating the project, the author should add reference files (PDF, DOCX, PPTX) to course/references/ for conversion.
498
498
 
499
499
  ## Next Stage
500
- Once the project exists and reference files are added, Stage 1 (Source Ingestion) begins. Run workflow_status to refresh.`;
500
+ Once the project exists and reference files are added, Stage 1 (Source Ingestion) begins. Run coursecode_workflow_status to refresh.`;
501
501
 
502
502
  case 1: { // Source Ingestion
503
503
  const rawFiles = listSafe(refsDir, ['.pdf', '.docx', '.doc', '.pptx', '.ppt']);
@@ -525,7 +525,7 @@ Convert to markdown: coursecode convert
525
525
  Converted files appear in course/references/converted/
526
526
 
527
527
  ## Next Stage
528
- Once references are converted to markdown, Stage 2 (Outline Creation) begins. Call workflow_status after conversion to get updated guidance.`;
528
+ Once references are converted to markdown, Stage 2 (Outline Creation) begins. Call coursecode_workflow_status after conversion to get updated guidance.`;
529
529
  }
530
530
 
531
531
  case 2: { // Outline Creation
@@ -552,7 +552,7 @@ The outline is a DESIGN document. Define content, interactions, structure, engag
552
552
  Pause for author review after creating the outline.
553
553
 
554
554
  ## Next Stage
555
- Once the outline is approved, Stage 3 (Course Building) begins. Call workflow_status to get updated guidance.`;
555
+ Once the outline is approved, Stage 3 (Course Building) begins. Call coursecode_workflow_status to get updated guidance.`;
556
556
  }
557
557
 
558
558
  case 3: { // Course Building
@@ -584,8 +584,8 @@ ${refList}
584
584
  Only import local assets (images, SVGs): import myImage from '../assets/images/photo.png';
585
585
  Interactions: const { createMultipleChoiceQuestion } = CourseCode; (destructure from global, NOT import)
586
586
  Components: use data-component="tabs" in HTML (declarative, no JS needed)
587
- - Use css_catalog to discover available CSS classes by category. Lint catches invalid classes with fix suggestions.
588
- - Use component_catalog and interaction_catalog to discover available components and interactions.
587
+ - Use coursecode_css_catalog to discover available CSS classes by category. Lint catches invalid classes with fix suggestions.
588
+ - Use coursecode_component_catalog and coursecode_interaction_catalog to discover available components and interactions.
589
589
  - Run lint after each batch of file changes. Fix all errors before proceeding.
590
590
  - Never modify files in framework/ — all work goes in course/ only.
591
591
  - No em-dashes in sentence structure. Use alternative phrasing.
@@ -593,7 +593,7 @@ ${refList}
593
593
  Pause for author review after the initial slide build.
594
594
 
595
595
  ## Next Stage
596
- Once slides and config are built, Stage 4 (Preview & Polish) begins. Start the preview and call workflow_status for updated guidance.`;
596
+ Once slides and config are built, Stage 4 (Preview & Polish) begins. Start the preview and call coursecode_workflow_status for updated guidance.`;
597
597
  }
598
598
 
599
599
  case 4: { // Preview & Polish
@@ -608,7 +608,7 @@ Once slides and config are built, Stage 4 (Preview & Polish) begins. Start the p
608
608
 
609
609
  This course was imported from a PowerPoint presentation. Each slide is currently a static image. Your job is to enhance it into an interactive course.
610
610
 
611
- 1. Ensure the preview server is running (\`coursecode preview\` in a terminal, or AI uses run_command)
611
+ 1. Ensure the preview server is running (\`coursecode preview\` in a terminal, or AI uses a terminal/command execution tool)
612
612
  2. Review slides: screenshot each to understand the content
613
613
  3. Enhancement priorities:
614
614
  - **Replace image slides** with interactive HTML — use the extracted text from references/converted/ as source content
@@ -618,7 +618,7 @@ This course was imported from a PowerPoint presentation. Each slide is currently
618
618
  - **Customize theme** — update colors in course/theme.css
619
619
  ${refList ? `\n4. REFERENCE MATERIALS (extracted text from presentation):\n${refList}\n` : ''}
620
620
  5. RULES:
621
- - Use css_catalog, component_catalog, interaction_catalog to discover available tools
621
+ - Use coursecode_css_catalog, coursecode_component_catalog, and coursecode_interaction_catalog to discover available options
622
622
  - Run lint after changes. Fix all errors before proceeding.
623
623
  - Efficient loop: edit files → lint → fix errors → screenshot to verify
624
624
 
@@ -630,15 +630,15 @@ Once polished and lint passes, Stage 5 (Export Ready) begins.`;
630
630
 
631
631
  Visually verify and polish the course using the preview server.
632
632
 
633
- 1. Ensure the preview server is running (\`coursecode preview\` in a terminal, or AI uses run_command)
633
+ 1. Ensure the preview server is running (\`coursecode preview\` in a terminal, or AI uses a terminal/command execution tool)
634
634
  2. Do NOT open a browser yourself — the MCP has its own headless Chrome
635
635
  3. Workflow (all tools execute instantly via internal headless browser):
636
- - state — get course structure, current slide, interactions, engagement
637
- - navigate — go to any slide by ID (get IDs from state)
638
- - screenshot — capture visual state (accepts slideId to navigate+capture in one call)
639
- - interact — test interactions with responses
640
- - export_content — extract all text content to review or compare against outline
641
- - lint — validate after file changes
636
+ - coursecode_state — get course structure, current slide, interactions, engagement
637
+ - coursecode_navigate — go to any slide by ID (get IDs from coursecode_state)
638
+ - coursecode_screenshot — capture visual state (accepts slideId to navigate+capture in one call)
639
+ - coursecode_interact — test interactions with responses
640
+ - coursecode_export_content — extract all text content to review or compare against outline
641
+ - coursecode_lint — validate after file changes
642
642
 
643
643
  4. Efficient iteration loop:
644
644
  Edit files → lint → fix errors → screenshot to verify visual result
@@ -647,7 +647,7 @@ ${refList ? `\n5. REFERENCE MATERIALS (for verifying content accuracy):\n${refLi
647
647
  Run lint and ensure zero errors before moving to export.
648
648
 
649
649
  ## Next Stage
650
- Once the course is polished and lint passes, Stage 5 (Export Ready) begins. Call workflow_status for export guidance.`;
650
+ Once the course is polished and lint passes, Stage 5 (Export Ready) begins. Call coursecode_workflow_status for export guidance.`;
651
651
  }
652
652
 
653
653
  case 5: // Export Ready
@@ -660,10 +660,10 @@ Export the finished course for LMS deployment.
660
660
  - cmi5 (default) for modern LMS
661
661
  - scorm1.2 for legacy systems
662
662
 
663
- The build produces a ZIP file in dist/ ready for LMS upload.`;
663
+ The build produces a dist/ output for deployment. If you need a packaged ZIP for LMS upload, use the CLI packaging commands outside MCP (for example \`coursecode package\`).`;
664
664
 
665
665
  default:
666
- return 'Call workflow_status to determine the current authoring stage.';
666
+ return 'Call coursecode_workflow_status to determine the current authoring stage.';
667
667
  }
668
668
  }
669
669
 
@@ -693,7 +693,7 @@ All runtime tools (state, navigate, interact, screenshot, viewport, reset) execu
693
693
 
694
694
  ### Preview Server Ownership
695
695
  - The MCP does NOT start or manage the preview server
696
- - The preview must be started externally: run \`coursecode preview\` in a terminal (human) or via run_command (AI agent)
696
+ - The preview must be started externally: run \`coursecode preview\` in a terminal (human) or via a terminal/command execution tool (AI agent)
697
697
  - If preview is not running, runtime tools will fail with a clear error message
698
698
  - The headless browser auto-reconnects when Vite rebuilds (file changes)
699
699
 
@@ -713,8 +713,8 @@ All runtime tools (state, navigate, interact, screenshot, viewport, reset) execu
713
713
  - Efficient loop: edit files → lint → fix errors → screenshot specific slides to verify
714
714
 
715
715
  ### Customization (all in course/, never in framework/)
716
- - **CSS overrides**: Edit \`course/theme.css\` — override palette tokens to rebrand (all colors cascade via color-mix). Use framework utility classes first (css_catalog), theme.css only for brand-specific overrides.
717
- - **Custom components**: Add \`.js\` files to \`course/components/\` — auto-discovered at build time. Use component_catalog for built-in options first.
716
+ - **CSS overrides**: Edit \`course/theme.css\` — override palette tokens to rebrand (all colors cascade via color-mix). Use framework utility classes first (\`coursecode_css_catalog\`), \`theme.css\` only for brand-specific overrides.
717
+ - **Custom components**: Add \`.js\` files to \`course/components/\` — auto-discovered at build time. Use \`coursecode_component_catalog\` for built-in options first.
718
718
  - **Custom interactions**: Add \`.js\` files to \`course/interactions/\` — auto-discovered. See \`course/interactions/PLUGIN_GUIDE.md\` for the template.
719
719
  - **Custom icons**: Add SVG definitions to \`course/icons.js\` — merged with built-in icons. Use icon_catalog to check existing icons first.`;
720
720
  }
@@ -769,7 +769,7 @@ export async function buildInstructions(port = 4173) {
769
769
  let automationWarning = '';
770
770
  if (checklist.hasCourseConfig && !checklist.hasAutomationEnabled) {
771
771
  automationWarning = `\n\n## ⚠️ Automation Disabled
772
- MCP runtime tools (state, navigate, interact, screenshot, reset) require \`environment.automation.enabled: true\` in course-config.js. Without it, the headless browser cannot access the course API and these tools will fail.
772
+ MCP runtime tools (\`coursecode_state\`, \`coursecode_navigate\`, \`coursecode_interact\`, and \`coursecode_reset\`, plus screenshot/navigation features that rely on course API access) require \`environment.automation.enabled: true\` in course-config.js. Without it, the headless browser cannot access the course API and these tools will fail.
773
773
  You MUST notify the author about this and let them decide whether to enable it. Do not silently modify the config.`;
774
774
  }
775
775
 
package/lib/mcp-server.js CHANGED
@@ -47,7 +47,7 @@ async function ensureHeadless(port) {
47
47
  throw new Error(
48
48
  'Preview server not running. Start it first:\n' +
49
49
  ' • Human: run `coursecode preview` in a terminal\n' +
50
- ' • AI agent: use run_command to execute `npm run preview`\n' +
50
+ ' • AI agent: use your terminal/command execution tool to run `coursecode preview`\n' +
51
51
  'Then retry this tool call.'
52
52
  );
53
53
  }
@@ -11,7 +11,7 @@ import os from 'os';
11
11
  import { fileURLToPath, pathToFileURL } from 'url';
12
12
  import { generateContentHtml } from './stub-player/content-generator.js';
13
13
  import { parseCourse } from './course-parser.js';
14
- import { getComponentCatalog, getInteractionCatalog, getIconCatalog, getWorkflowStatus, getRefsStatus } from './authoring-api.js';
14
+ import { getComponentCatalog, getInteractionCatalog, getIconCatalog, getWorkflowStatus, getRefsStatus, buildCourse } from './authoring-api.js';
15
15
  import { getAllIcons, getAllSchemas } from './schema-extractor.js';
16
16
 
17
17
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -612,6 +612,46 @@ export async function handleApiRoutes(ctx, req, res, url) {
612
612
  return true;
613
613
  }
614
614
 
615
+ // Preview a converted reference file (for dashboard Stage 2 links)
616
+ if (url === '/__stub-player/ref-preview') {
617
+ const params = new URL(req.url, 'http://localhost').searchParams;
618
+ const fileName = params.get('file');
619
+ if (!fileName || fileName.includes('..')) {
620
+ res.writeHead(400, { 'Content-Type': 'text/plain' });
621
+ res.end('Invalid file parameter');
622
+ return true;
623
+ }
624
+ const filePath = path.join(paths.coursePath, 'references', 'converted', fileName);
625
+ if (!fs.existsSync(filePath)) {
626
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
627
+ res.end('File not found');
628
+ return true;
629
+ }
630
+ const raw = fs.readFileSync(filePath, 'utf-8');
631
+ const html = ctx.simpleMarkdownToHtml(raw);
632
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-cache' });
633
+ res.end(`<!DOCTYPE html>
634
+ <html lang="en">
635
+ <head>
636
+ <meta charset="UTF-8">
637
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
638
+ <title>${fileName} – Reference Preview</title>
639
+ <link rel="stylesheet" href="/__stub-player/styles.css">
640
+ <style>
641
+ body { padding: 40px; max-width: 900px; margin: 0 auto; font-family: system-ui, -apple-system, sans-serif; line-height: 1.6; background: var(--color-primary-deep, #0b1628); color: var(--color-gray-200, #d1d5db); }
642
+ h1 { font-size: 18px; color: var(--color-white, #fff); border-bottom: 1px solid rgba(255,255,255,0.1); padding-bottom: 8px; margin-bottom: 24px; }
643
+ pre { background: rgba(0,0,0,0.3); padding: 12px; border-radius: 6px; overflow-x: auto; }
644
+ code { background: rgba(0,0,0,0.2); padding: 2px 6px; border-radius: 3px; font-size: 13px; }
645
+ </style>
646
+ </head>
647
+ <body>
648
+ <h1>${fileName}</h1>
649
+ ${html}
650
+ </body>
651
+ </html>`);
652
+ return true;
653
+ }
654
+
615
655
  // Stub player static files
616
656
  if (url.startsWith('/__stub-player/')) {
617
657
  const relativePath = url.slice('/__stub-player/'.length);
@@ -639,6 +679,24 @@ export async function handleApiRoutes(ctx, req, res, url) {
639
679
  return true;
640
680
  }
641
681
 
682
+ // Build course (triggered from dashboard)
683
+ if (url.startsWith('/__build') && req.method === 'POST') {
684
+ (async () => {
685
+ try {
686
+ const params = new URL(req.url, 'http://localhost').searchParams;
687
+ const format = params.get('format') || 'cmi5';
688
+ const result = await buildCourse({ format });
689
+ res.writeHead(200, { 'Content-Type': 'application/json' });
690
+ res.end(JSON.stringify(result));
691
+ } catch (err) {
692
+ res.writeHead(500, { 'Content-Type': 'application/json' });
693
+ res.end(JSON.stringify({ success: false, error: err.message }));
694
+ }
695
+ })();
696
+ return true;
697
+ }
698
+
699
+
642
700
  // Reset (clear storage + redirect)
643
701
  if (url === '/__reset') {
644
702
  const resetHtml = `<!DOCTYPE html>