MISSION CONTROL · OPS
+ + + + +connecting…
+diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml
index c027ccf..c42259e 100644
--- a/.github/workflows/deploy.yaml
+++ b/.github/workflows/deploy.yaml
@@ -1,58 +1,44 @@
-name: Ship It
+name: cicd
+
on:
- push:
- branches: [main]
pull_request:
- branches: [main]
+ types: [opened, synchronize, reopened, closed]
-# Amali 5: permission untuk deploy GitHub Pages
-permissions:
- pages: write
- id-token: write
- contents: read
-
-concurrency:
- group: pages
- cancel-in-progress: true
-
-# The ship app lives in launchpad/ in the learner fork; run every step there.
defaults:
run:
working-directory: launchpad
jobs:
test:
+ if: github.event.pull_request.merged == false
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
- with:
- node-version: 20
- cache: npm
- cache-dependency-path: launchpad/package-lock.json
- run: npm clean-install
- - run: npm run test # node scripts/preflight.mjs — a bad ship.config.json ABORTS here
+ - run: npm run test
+ - run: bash scripts/report.sh
+ env:
+ BOARD_URL: ${{ vars.BOARD_URL }}
+ SHIPIT_TOKEN: ${{ secrets.SHIPIT_TOKEN }}
deploy:
- if: github.event_name == 'push' && github.ref == 'refs/heads/main'
- needs: test # deploy only runs if the pre-flight gate is green
+ needs: test
+ if: always() && github.event.pull_request.merged == true
runs-on: ubuntu-latest
- environment:
- name: github-pages
- url: ${{ steps.deploy.outputs.page_url }}
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
- with:
- node-version: 20
- cache: npm
- cache-dependency-path: launchpad/package-lock.json
- run: npm clean-install
- run: npm run build
env:
+ VITE_BOARD_URL: ${{ vars.BOARD_URL }}
VITE_CALLSIGN: ${{ github.actor }}
- uses: actions/upload-pages-artifact@v5
with:
path: launchpad/dist
- - id: deploy
- uses: actions/deploy-pages@v5
+ - uses: actions/deploy-pages@v5
+
+permissions:
+ pages: write
+ id-token: write
diff --git a/CLAUDE.md b/CLAUDE.md
index 824d6f9..1fac829 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -40,8 +40,8 @@ Distinct from its siblings (do not blur them):
Pages; extensible to CF Pages). **No image** — the ship is static-only. Three.js, like `devops-bootcamp-app`.
- **`shipit-board` is dual-role:** the *shared* Mission Control on instructor EC2, **and** the
artifact each learner **builds + deploys to their own EC2** in the S4 capstone.
-- The freed `shipit-launchpad` name is reused for the **forkable learner repo**
- `Infratify/shipit-launchpad` (see Distribution below), not an image.
+- Learners fork **this monorepo** (`Infratify/devops-bootcamp-shipit`) and work in `launchpad/`
+ (see Distribution below). The planned payload-only `shipit-launchpad` release repo was never used.
## PINNED — the 4-session arc (lean: one concept per session)
@@ -64,28 +64,59 @@ already did `docker build` + ECR by hand in the AWS sessions — S4 *automates t
The one integration point. Keep it stable; slides and the reference workflows depend on it.
- **Identity** = the learner's GitHub username (`${{ github.actor }}`), used as `callsign`.
-- **Config** the board needs comes from the learner's `ship.config.json`: `color` (hex **or** a
- named-palette colour; sets the ship's hue — every saturated texel takes that hue; greys/blacks stay
- neutral — and drives the UI accent) and `shipModel` (which of the 4 ships the board renders in
- orbit); `shipName` is a cosmetic label, not identity.
-- **Transport:** each workflow stage POSTs one event to the board.
+- **Config:** the report script reads `color` + `shipModel` from the learner's `ship.config.json`,
+ so the board renders each learner's real colour AND model (hex or a named-palette colour; board
+ normalizes, greys/blacks stay neutral). `shipName` is cosmetic, never identity.
+- **Transport:** `launchpad/scripts/report.sh` (shipped in this repo — learners call it, never write
+ it) POSTs one event per invocation. The learner workflow adds ONE step (CI/CD 3 Amali 2) — a
+ **single liftoff report** after the Pages deploy:
+
+```yaml
+- name: Lapor ke papan
+ run: bash scripts/report.sh
+ env:
+ BOARD_URL: ${{ vars.BOARD_URL }}
+ SHIPIT_TOKEN: ${{ secrets.SHIPIT_TOKEN }}
+```
+
+ That `env:` block IS the lesson surface — one `vars` line, one `secrets` line, nothing else. The
+ HTTP mechanics live in the script. Extra beats (`report.sh pad running`, abort-on-failure) are
+ optional operator flourishes, never required of learners.
+
+What the script sends (slides show this as *anatomy* — learners read it, never type it):
```
POST $BOARD_URL/api/event
Authorization: Bearer $SHIPIT_TOKEN
Content-Type: application/json
-{
- "callsign": "octocat", // GitHub username
- "stage": "build", // pad | build | test | clearance | liftoff
- "status": "passed", // running | passed | failed | aborted | shipped
- "color": "#22d3ee", // hex or a colour name (e.g. "red"); board normalizes → hex, sets the ship's hue
- "shipModel":"fighter", // from ship.config.json: fighter · interceptor · hauler · scout
- "version": "v3", // optional; image/site tag (for rollback demo)
- "siteUrl": "https://…" // optional; the live deployed site to link from orbit
-}
+{ "callsign": "octocat", "stage": "liftoff", "status": "shipped",
+ "color": "#22d3ee", "shipModel": "fighter",
+ "siteUrl": "https://octocat.github.io/devops-bootcamp-shipit/" }
```
+`callsign` = `GITHUB_ACTOR` (Actions sets it automatically — no templating in the learner file);
+`color`/`shipModel` read from `ship.config.json`; stage/status from the script args (default
+`liftoff shipped`). `siteUrl` is **derived inside `report.sh`** from the vars Actions already sets
+(`GITHUB_REPOSITORY_OWNER` + repo name → `https://Mission Control
+
connecting…
+diff --git a/board/client/main.js b/board/client/main.js index a17f19f..51f9fd5 100644 --- a/board/client/main.js +++ b/board/client/main.js @@ -1,19 +1,24 @@ import './style.css'; import { createScene } from './scene.js'; +import { createRaceTrack } from './race-track.js'; import { createFallback, detectWebGL, shouldUseFallback } from './fallback.js'; const app = document.getElementById('app'); const count = document.getElementById('count'); const toasts = document.getElementById('toasts'); +const hud = document.getElementById('race-hud'); +const orbitHud = document.getElementById('hud'); +const hudClients = document.getElementById('hud-clients'); const gl = detectWebGL(); const mql = window.matchMedia('(prefers-reduced-motion: reduce)'); -let lastShips = []; -let view = makeView(shouldUseFallback({ gl, reducedMotion: mql.matches })); +let lastShips = []; // roster (orbit) +let lastRace = { phase: 'idle', total: 12, ships: [] }; // race state (rows view) +let mode = 'orbit'; // 'orbit' | 'race' +let view = makeOrbit(shouldUseFallback({ gl, reducedMotion: mql.matches })); function showLiftoff(callsign) { if (!toasts) return; - // Cap the stack so a class-end launch burst can't run toasts off-screen over the scene. while (toasts.children.length >= 5) toasts.firstChild.remove(); const el = document.createElement('div'); el.className = 'toast'; @@ -23,22 +28,34 @@ function showLiftoff(callsign) { setTimeout(() => { el.classList.remove('show'); setTimeout(() => el.remove(), 400); }, 3000); } -function makeView(useFallback) { +function makeOrbit(useFallback) { const v = useFallback ? createFallback(app) : createScene(app, { onLiftoff: showLiftoff, - onPreloadError: () => { - view.dispose(); - view = createFallback(app); - view.update(lastShips); - }, + onPreloadError: () => { view.dispose(); view = createFallback(app); view.update(lastShips); }, }); v.update(lastShips); return v; } +function makeRace() { + const v = createRaceTrack(app); // its own fallback: DOM-only, reduced-motion via CSS + v.update(lastRace); + return v; +} + +function setMode(next) { + if (next === mode) return; + view.dispose(); + mode = next; + // The orbit HUD legend occludes the top rows at 40-ship density in race mode. + if (mode === 'race') { view = makeRace(); if (hud) hud.hidden = false; if (orbitHud) orbitHud.hidden = true; } + else { view = makeOrbit(shouldUseFallback({ gl, reducedMotion: mql.matches })); if (hud) hud.hidden = true; if (orbitHud) orbitHud.hidden = false; } +} + mql.addEventListener('change', (e) => { + if (mode !== 'orbit') return; // race track handles reduced motion in CSS view.dispose(); - view = makeView(shouldUseFallback({ gl, reducedMotion: e.matches })); + view = makeOrbit(shouldUseFallback({ gl, reducedMotion: e.matches })); }); window.addEventListener('pagehide', () => view.dispose()); @@ -48,8 +65,13 @@ function connect() { let m; try { m = JSON.parse(e.data); } catch { return; } if (m.t === 'roster' && Array.isArray(m.ships)) { lastShips = m.ships; - view.update(lastShips); + if (mode === 'orbit') view.update(lastShips); if (count) count.textContent = `${lastShips.length} ship${lastShips.length === 1 ? '' : 's'}`; + } else if (m.t === 'race') { + lastRace = { phase: m.phase, total: m.total, ships: m.ships || [] }; + setMode(m.view === 'race' ? 'race' : 'orbit'); + if (mode === 'race') view.update(lastRace); + if (hudClients) hudClients.textContent = String(m.clients ?? 0); } }; ws.onclose = () => setTimeout(connect, 1000); diff --git a/board/client/operator.css b/board/client/operator.css new file mode 100644 index 0000000..37615a2 --- /dev/null +++ b/board/client/operator.css @@ -0,0 +1,12 @@ +:root { color-scheme: dark; } +body { margin: 0; background: #0b1220; color: #e2e8f0; font: 16px/1.5 ui-monospace, Menlo, Consolas, monospace; } +#ops { max-width: 420px; margin: 0 auto; padding: 2rem 1rem; display: flex; flex-direction: column; gap: 1rem; } +h1 { font-size: 1.1rem; letter-spacing: 0.1em; color: #22d3ee; margin: 0; } +label { display: flex; flex-direction: column; gap: 0.3rem; color: #94a3b8; font-size: 0.9rem; } +input, select { font: inherit; padding: 0.5rem 0.75rem; background: #0f172a; color: #e2e8f0; border: 2px solid #334155; border-radius: 8px; } +input:focus, select:focus { outline: none; border-color: #22d3ee; } +.buttons { display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem; } +button { font: inherit; padding: 0.75rem; background: #111a2e; color: #e2e8f0; border: 2px solid #334155; border-radius: 8px; cursor: pointer; } +button:hover { border-color: #22d3ee; } +#start { grid-column: 1 / -1; border-color: #22d3ee; color: #22d3ee; } +#result, #live { margin: 0; color: #94a3b8; min-height: 1.5em; } diff --git a/board/client/operator.html b/board/client/operator.html new file mode 100644 index 0000000..7e684a8 --- /dev/null +++ b/board/client/operator.html @@ -0,0 +1,30 @@ + + +
+ + +
+ + +
+