---
name: staging
description: "Preview staged pages, push content to Canvas with backup, or rollback to a previous version."
---

# Staging

> **Plugin**: ASU Canvas Course Builder
> **Scripts**: `scripts/staging_manager.py`, `scripts/push_to_canvas.py`, `scripts/backup_manager.py`, `scripts/post_write_verify.py`

## HARD RULE — Stage Before Every Push
> **Edit discipline**: Preserve original structure. Replace only fields the user explicitly named. Do not delete placeholders, instructor notes, or template scaffolding unless told to.

**ALL student-facing HTML body changes must be staged and shown to the user before pushing to Canvas. No exceptions.**

This includes:
- Accessibility remediations (alt text, heading hierarchy, color contrast)
- Formatting fixes (template alignment, inline styles)
- Structural corrections (wrapping images, removing broken HTML)
- Copy edits or content updates of any size
- Bulk fixes across multiple pages

**The required sequence for every body change:**
1. Write corrected content via `staging_manager.py --stage`
2. Show the user the staged content (diff or preview)
3. Wait for explicit user approval
4. Push only after approval via `push_to_canvas.py`

**Metadata-only edits can skip staging** (points, dates, attempts, assignment group, published status).

---

## Storage Layout

```
courses/{COURSE_ID}/staging/
  pages/{slug}.html
  assignments/{assignment_id}.html
  discussions/{topic_id}.html
  quizzes/{quiz_id}.json

courses/{COURSE_ID}/backups/
  pages/{timestamp}_{slug}.html
  assignments/{timestamp}_{assignment_id}.html
  discussions/{timestamp}_{topic_id}.html
  quizzes/{timestamp}_{quiz_id}.json
```

---

## Mode 1: Preview

### Staging Preview Server

Start the staging preview server to render staged content in a Canvas-like shell. **Always launch via `ensure_staging_server.py`, never `staging_server.py` directly:**

```bash
${ASUCB_PLUGIN_ROOT:-${CODEX_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT}}}/bin/python3 scripts/ensure_staging_server.py
```

This script is idempotent and **always uses port 8111**. If 8111 is already bound, that means the staging server is already running, so it reuses it and prints the existing base URL. It never starts a second server and never falls back to another port (8112, etc.). If you see "port already occupied," the server is already up: reuse `http://127.0.0.1:8111/`, do not spawn a new one on a different port. The script prints the canonical base URL on success; announce that exact URL.

Use 127.0.0.1 (avoids macOS IPv6 localhost resolution issues); localhost still accepted by the Origin allowlist. Never announce a `localhost:` URL or any port other than 8111.

(`staging_server.py --port 8111` is the underlying server `ensure_staging_server.py` launches; the launch config `staging` in `.claude/launch.json` also targets 8111. Do not invoke `staging_server.py` directly with a different port.)

| Route | Renders |
|---|---|
| `http://127.0.0.1:8111/` | Index — all staged items across all courses, with "Review All" link per course |
| `http://127.0.0.1:8111/courses/{id}/review` | **Unified staging review** — all items for one course in a single scrollable view (see below) |
| `http://127.0.0.1:8111/courses/{id}/pages/{slug}` | Individual page HTML in Canvas shell |
| `http://127.0.0.1:8111/courses/{id}/assignments/{id}` | Individual assignment description in Canvas shell |
| `http://127.0.0.1:8111/courses/{id}/discussions/{id}` | Individual discussion prompt in Canvas shell |
| `http://127.0.0.1:8111/courses/{id}/quizzes/{id}` | Individual quiz as question cards with answers, feedback, Bloom's tags |

As of v7.5.0, the canonical preview shape is `http://127.0.0.1:8111/courses/{id}/preview/{type}/{key}` for all object types (`pages`, `assignments`, `discussions`, `quizzes`, `interactives`, `syllabus`). The per-object legacy routes above are permanently kept as aliases — either URL form works at the server. Always announce the `127.0.0.1` form to users (avoids macOS IPv6 `localhost` resolution races); `localhost` stays in the Origin allowlist for compat but should not appear in URLs the user sees.

The server accepts PUT requests to save source-view edits back to staging: `PUT /api/staging/{courseId}/{type}/{id}` (used by the HTML Source panel's Save button).

### Unified Staging Review (Preferred for Multiple Items)

When reviewing many staged items, use the unified review at `/courses/{id}/review` instead of individual routes:

- **Sidebar TOC** — collapsible module groups, color-coded type dots (blue=overview, green=lesson, orange=quiz, teal=discussion, etc.), search filter, preflight issue badges
- **Stitched sections** — all staged items rendered vertically in one scrollable document
- **Read-only preview** — the staging server renders content for review only; edits are made surgically via Claude/Codex or in Canvas Source mode, then re-staged via `staging_manager.py`.
- **Quizzes** — rendered as read-only question cards with Bloom's tags, answer highlighting, and feedback
- **Module grouping** — pages grouped by module from slug; non-page items (assignments, discussions, quizzes) in "Unmapped Staged Content" group
- **Keyboard navigation** — J/K to jump between sections
- **ASU maroon top bar** with course name, item count, timestamp, print button
- **Scroll progress bar** (ASU gold)

**When to use unified review vs individual routes:**
- Many staged items across types → unified review
- Quick spot-check of one item → individual route
- Small change in chat → chat review is fine

### Preflight Quality Checks

Preflight checks run automatically when content is staged via `staging_manager.py`. Two tiers:

**Tier 1 (automatic on stage)** — payload-only, no Canvas/config context needed:
- Heading hierarchy (no H1 in body, no skipped levels)
- Image alt text (missing, empty/decorative)
- Link text quality (no "click here", bare URLs)
- Table accessibility (missing `<th>`, missing `scope`)
- Video transcript presence
- Placeholder text detection (`[TODO]`, `[FACULTY]`, etc.)
- Quiz: question feedback presence, question type variety

**Tier 2 (on demand)** — requires Canvas API or course-config.json:
- Rubric presence, points set, due date set
- Bloom's alignment to module target
- Objective measurability

Results are written as sidecar files: `courses/{courseId}/staging/{type}/{id}.{ext}.issues.json`

Sidecars are automatically cleaned up when `clear_staged()` is called. Re-staging overwrites the previous sidecar.

**Vision audit** (on demand): `${ASUCB_PLUGIN_ROOT:-${CODEX_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT}}}/bin/python3 scripts/vision_audit.py --course-id {ID} --page-slug {slug} --semantic --output auto` extracts images, downloads them, and builds a manifest for Claude's vision analysis. Output goes to `courses/{courseId}/reports/vision/`.

### List Staged Content

```bash
${ASUCB_PLUGIN_ROOT:-${CODEX_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT}}}/bin/python3 scripts/staging_manager.py --course-id {COURSE_ID} --list
```

Shows all staged objects with type, ID, and size.

### Read Staged Content (CLI)

```bash
${ASUCB_PLUGIN_ROOT:-${CODEX_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT}}}/bin/python3 scripts/staging_manager.py --course-id {COURSE_ID} --get --type pages --id {slug}
```

### Canvas Preview (Live Content)

For previewing pages already on Canvas, navigate to the Canvas URL and screenshot.

| Input | Resolved URL |
|---|---|
| Page slug: `m1-overview` | `/courses/{id}/pages/m1-overview` |
| Module shorthand: `Module 3 quiz` | Resolve via course navigator |
| Full URL | Used as-is |

---

## Mode 2: Push

Push staged content to Canvas with diff review and automatic backup.

### Step 1 — List Staged Content

```bash
${ASUCB_PLUGIN_ROOT:-${CODEX_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT}}}/bin/python3 scripts/staging_manager.py --course-id {COURSE_ID} --list
```

### Step 2 — Show Diffs

For each staged item, fetch current Canvas content and show the changes.

### Step 3 — Confirm

**Always display the target Canvas instance before confirmation:**

> Pushing to **{CANVAS_DOMAIN}**
> {N} items ready to push:

If pushing **10 or more items**, warn:
> "This is a large batch ({N} items). Review the full list first, or push all at once?"

User can:
- "Push all" → push everything
- "Push m1-overview and m3-lesson" → specific items
- "Cancel" → abort

### Step 4 — Push

**Single page:**
```bash
${ASUCB_PLUGIN_ROOT:-${CODEX_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT}}}/bin/python3 scripts/push_to_canvas.py --type page --slug {slug} --course-id {COURSE_ID}
```

**Multiple pages:**
```bash
${ASUCB_PLUGIN_ROOT:-${CODEX_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT}}}/bin/python3 scripts/push_to_canvas.py --type page --slugs {slug1},{slug2},{slug3} --course-id {COURSE_ID}
```

**Assignment description:**
```bash
${ASUCB_PLUGIN_ROOT:-${CODEX_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT}}}/bin/python3 scripts/push_to_canvas.py --type assignment --id {assignment_id} --course-id {COURSE_ID}
```

**Discussion prompt:**
```bash
${ASUCB_PLUGIN_ROOT:-${CODEX_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT}}}/bin/python3 scripts/push_to_canvas.py --type discussion --id {topic_id} --course-id {COURSE_ID}
```

Each push atomically: backs up current → pushes staged → verifies → clears staging → updates build progress.

When calling push functions, pass `module_num` and `page_type` parameters when known so `build-progress.json` is updated automatically. For page pushes, the slug is parsed as a fallback if explicit parameters aren't provided.

### Step 5 — Report

Show push results with status per item, Canvas links, and any warnings from verification. After a successful batch push, log the event: `${ASUCB_PLUGIN_ROOT:-${CODEX_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT}}}/bin/python3 scripts/progress_tracker.py --course-id {COURSE_ID} --history "pushed" "{N} items via staging"`

---

## Mode 3: Rollback

Restore a Canvas page to a previous version from local backups.

### Step 1 — List Backups

```bash
${ASUCB_PLUGIN_ROOT:-${CODEX_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT}}}/bin/python3 scripts/backup_manager.py --course-id {COURSE_ID} --list
```

Optionally filter: `--type pages --id {slug}`

### Step 2 — Select Backup

User picks by timestamp, slug, or "latest {slug}".

### Step 3 — Show Diff

Compare backup content vs current Canvas content. Display unified diff.

### Step 4 — Restore

1. **Backup the current version first** (so rollback is reversible):
   ```bash
   ${ASUCB_PLUGIN_ROOT:-${CODEX_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT}}}/bin/python3 scripts/backup_manager.py --course-id {COURSE_ID} --save --type pages --id {slug} --html-file <current_content>
   ```
2. **Stage the backup content**:
   ```bash
   ${ASUCB_PLUGIN_ROOT:-${CODEX_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT}}}/bin/python3 scripts/staging_manager.py --course-id {COURSE_ID} --stage --type pages --id {slug} --html-file <backup_content>
   ```
3. **Push**: `${ASUCB_PLUGIN_ROOT:-${CODEX_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT}}}/bin/python3 scripts/push_to_canvas.py --type page --slug {slug} --course-id {COURSE_ID}`
4. **Confirm restoration**

---

## Safety

- Every push creates a backup BEFORE overwriting
- **Staged files are cleared ONLY after successful verification** — if the Canvas write succeeds but verification fails, staging is preserved so the user can retry
- Partial batch failures leave remaining files staged
- Rollbacks are reversible (current version backed up before restore)
- Quiz pushes include question creation/update — if any question fails, the entire push reports failure and staging is preserved

## Error Handling

| Error | Resolution |
|---|---|
| No staged content | Direct to content skills to generate content |
| Canvas API error | Show error, keep staged file, offer retry |
| No backups found | Backups are created by push operations |
| Push partially failed | Report which succeeded/failed, keep failed items staged |
