Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .claude/settings.local.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
"Bash(python -m pytest tests/test_ws_smoke.py -q --tb=short)",
"Bash(tasklist //FI \"IMAGENAME eq node.exe\")",
"Bash(taskkill //F //PID 21928 //PID 11840)",
"Bash(python -c \"import uvicorn; print\\(uvicorn.__version__, uvicorn.__file__\\)\")"
"Bash(python -c \"import uvicorn; print\\(uvicorn.__version__, uvicorn.__file__\\)\")",
"PowerShell(Start-Process \"C:\\\\Users\\\\anand\\\\Downloads\\\\ORION\\\\design\\\\sample-mission-control.html\")"
]
}
}
120 changes: 120 additions & 0 deletions .github/workflows/sentinel-gate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# Sentinel promotion gate — runs on every PR, blocks merge unless Sentinel says "promote".
#
# Drop this file into any repo's .github/workflows/. It builds a DeliveryEvent from the PR
# context and POSTs it to a running Sentinel Gateway's /api/v1/simulate, which clones the repo
# server-side, runs the Neuro-SAN review/test/risk pipeline, and returns a promotion decision.
# The job then polls the run and fails the check unless the decision is `promote`.
#
# Required repo secrets:
# SENTINEL_GATEWAY_URL base URL of a reachable Gateway, e.g. https://sentinel.example.com
# (for local dev, expose scripts/run_gateway.py :8000 via a tunnel:
# cloudflared tunnel --url http://localhost:8000)
# SENTINEL_TOKEN an admin API token from the Gateway's API_TOKENS (simulate is admin-only)
#
# ponytail: same-repo PRs only. The Gateway clones repo.url and needs BOTH base and head SHAs
# reachable there; a fork's head SHA is not in the base repo, so fork PRs are skipped.

name: Sentinel gate

on:
pull_request:
types: [opened, synchronize, reopened]
workflow_dispatch:
inputs:
base_sha: { description: base commit SHA, required: true }
head_sha: { description: head commit SHA, required: true }

# One in-flight gate per PR; a new push cancels the previous run.
concurrency:
group: sentinel-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

env:
# Which promotion this gate represents. Override per-repo if merging means a later stage.
FROM_ENV: dev
TO_ENV: test
POLL_TIMEOUT_SECONDS: "1200"

jobs:
gate:
runs-on: ubuntu-latest
steps:
- name: Skip fork PRs
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork
run: |
echo "::warning::Sentinel gate skips fork PRs (head SHA not reachable from base repo)."
exit 0

- name: Submit run and gate on the decision
env:
GW: ${{ secrets.SENTINEL_GATEWAY_URL }}
TOKEN: ${{ secrets.SENTINEL_TOKEN }}
GH_TOKEN: ${{ github.token }}
BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.inputs.base_sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha || github.event.inputs.head_sha }}
PR_TITLE: ${{ github.event.pull_request.title || github.ref_name }}
PR_BRANCH: ${{ github.event.pull_request.head.ref || github.ref_name }}
PR_AUTHOR: ${{ github.actor }}
run: |
set -euo pipefail
[ -n "$GW" ] || { echo "::error::SENTINEL_GATEWAY_URL secret is not set"; exit 1; }
[ -n "$TOKEN" ] || { echo "::error::SENTINEL_TOKEN secret is not set"; exit 1; }

# Authenticated clone URL so the Gateway can reach private repos (token expires with the job).
REPO_URL="https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"

event=$(jq -nc \
--arg eid "gha-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" \
--arg url "$REPO_URL" \
--arg name "$GITHUB_REPOSITORY" \
--arg base "$BASE_SHA" --arg head "$HEAD_SHA" \
--arg branch "$PR_BRANCH" --arg title "$PR_TITLE" --arg author "$PR_AUTHOR" \
--arg from "$FROM_ENV" --arg to "$TO_ENV" \
'{event_id:$eid, source:"github",
repo:{url:$url, name:$name, default_branch:"main"},
change:{base_sha:$base, head_sha:$head, branch:$branch, title:$title, author:$author},
target_transition:{from_env:$from, to_env:$to},
requested_by:$author}')

resp=$(curl -sf -X POST "$GW/api/v1/simulate" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d "{\"event\": $event}")
run_id=$(echo "$resp" | jq -r '.run_id')
[ -n "$run_id" ] && [ "$run_id" != "null" ] || { echo "::error::no run_id in: $resp"; exit 1; }
echo "Sentinel run: $GW/runs/$run_id"

# Poll until the run reaches a terminal state.
deadline=$(( $(date +%s) + POLL_TIMEOUT_SECONDS ))
state=""
while [ "$(date +%s)" -lt "$deadline" ]; do
detail=$(curl -sf "$GW/api/v1/runs/$run_id" -H "Authorization: Bearer $TOKEN")
state=$(echo "$detail" | jq -r '.run.state')
echo " state=$state"
[ "$state" = "done" ] || [ "$state" = "failed" ] && break
sleep 10
done

decision=$(echo "$detail" | jq -r '.decision.decision // "unknown"')
risk=$(echo "$detail" | jq -r '.risk_score.score // "n/a"')
crit=$(echo "$detail" | jq -r '.review_report.counts.critical // 0')

{
echo "## Sentinel gate"
echo ""
echo "| field | value |"
echo "|---|---|"
echo "| decision | \`$decision\` |"
echo "| risk | $risk |"
echo "| critical findings | $crit |"
echo "| run | [$run_id]($GW/runs/$run_id) |"
} >> "$GITHUB_STEP_SUMMARY"

if [ "$state" != "done" ]; then
echo "::error::Sentinel run did not finish (state=$state). See $GW/runs/$run_id"
exit 1
fi
if [ "$decision" != "promote" ]; then
echo "::error::Sentinel decision is '$decision' (risk $risk, $crit critical). Review at $GW/runs/$run_id"
exit 1
fi
echo "Sentinel: promote ✅ (risk $risk)"
26 changes: 23 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@ Two roadmap documents exist. Know the difference:

- **`ORION_SAAS_ROADMAP.md`** (v2.0) — governs **priority ordering**. What to build next. When two tasks compete, this doc wins. Source of truth for Phase 0–5 scope, the known-defect register (D-01…D-13), and honest market positioning.
- **`AREP_IMPLEMENTATION_ROADMAP.md`** (v1.1) — governs **technical implementation detail**. How to build it. Specific data structures, acceptance criteria, file names.
- **`ORION_UI_DESIGN.md`** (v1.0) — governs **ALL frontend/UI visual work**. The binding design system ("Mission Control"): tokens, typography, components, theming, page-by-page specs. Any change under `orion-frontend/` must conform. Companion `ORION_UI_REDESIGN_PLAN.md` = the migration sequencing; approved sample = `design/sample-mission-control.html`. See Section 9.

When they conflict on priority: SaaS roadmap wins. When you need implementation depth: read the technical roadmap.
When they conflict on priority: SaaS roadmap wins. When you need implementation depth: read the technical roadmap. For anything visual/frontend: `ORION_UI_DESIGN.md` is authoritative.

**Current priority: Phase 0 — Security & Score Integrity** (SaaS roadmap v2.0). No Stripe, no new features until Phase 0 exits. See Section 13.

Expand Down Expand Up @@ -300,13 +301,32 @@ All API errors return `{"detail": "..."}` — match this shape in new error hand

React 18, Vite 5, React Router 6. No TypeScript — plain JSX.

### Design system — `ORION_UI_DESIGN.md` is BINDING (read before ANY frontend work)

**Every visual change under `orion-frontend/` MUST conform to `ORION_UI_DESIGN.md`** (the "Mission Control" design system, approved 2026-06-25). Read it before writing any page, component, or style. It is the source of truth for colors, typography, spacing, radius, components, theming, and per-page layout — the approved render is `design/sample-mission-control.html`.

- **Use tokens only.** Never hardcode a hex, font size, radius, or one-off color — use the CSS variables defined in `index.css` per the design doc. Old purple/glass tokens (`#6c63ff`, `--accent-primary` gradient, `.glass-card` glow) are retired.
- **Fonts:** Chakra Petch (display) / Saira (UI) / JetBrains Mono (all numerals, tabular). No Inter/Roboto/system body.
- **Theme:** dark-default, light available; `data-theme` on `<html>`, persisted to `localStorage` key `orion-theme`. This is the **only** sanctioned localStorage UI-pref key — it does NOT relax the D-04 auth-token rule below.
- **No emoji as UI icons** — use the inline-SVG set in `src/components/common/Icon.jsx`.
- **Banned aesthetics (permanent):** glassmorphism-as-primary-surface, purple/violet gradients, glow-pulse/float animations, rounded-2xl, generic AI-startup hero. See design doc §1.
- **Out of scope = don't.** Don't add visual features, routes, libraries, or styles not described in the design doc unless the user explicitly asks. New pattern needed → add it to `ORION_UI_DESIGN.md` in the same change, then implement.
- Migration status + phase order live in `ORION_UI_REDESIGN_PLAN.md`.

**Implementation status: DONE** (redesign Phases A–I complete). The code now matches the design doc. Key infrastructure:
- Tokens + global utilities (`.panel`/`.panel--live`, `.btn*`, `.field`, `.chip*`, `.data-table`, `.mono-label`, `.num`, `.spec-strip`, grid backdrop, `.skeleton`, `.skip-link`) live in `src/index.css`. Legacy purple/`glass-card` aliases were removed — use canonical tokens only.
- Theme: `src/theme/ThemeContext.jsx` (`useTheme()` → `{theme, toggleTheme, setTheme}`), control = `src/components/common/ThemeToggle.jsx`, no-FOUC inline script in `index.html`, key `orion-theme`. Dark default.
- Icons: `src/components/common/Icon.jsx` (`<Icon name=.. size=.. />`). No emoji. Add new icons there + list in design doc §7.
- Shell: `src/components/common/HudBar.jsx` (sticky status strip) + real `src/components/common/Sidebar.jsx` (numbered nav). `src/components/common/ErrorBoundary.jsx` wraps the app in `main.jsx`; `src/pages/NotFoundPage.jsx` is the `*` route.
- Recharts theming: recompute palette from CSS vars keyed on `useTheme().theme` (see `useChartPalette` in `DashboardPage.jsx`). Never hardcode chart hex.

### Rules

- All HTTP calls through `src/services/api.js` — never use `fetch()` directly in component.
- Auth token lives in `AuthContext` — use `const { user, token, logout } = useAuth()` everywhere.
- **Token storage (D-04, being redesigned in Phase 0.4)**: today `AuthContext` persists the JWT in `localStorage` (`orion_token` key) — this is a known security defect, target is httpOnly cookie + `GET /api/auth/me` bootstrap. Don't add NEW `localStorage` token reads/writes outside `AuthContext`; don't build features that depend on reading the raw token in components.
- `OrgContext.jsx` exists but `OrgProvider` is NOT mounted anywhere — `useOrg()` throws. Mount it or don't call it (Phase 0.6 resolves).
- Dashboard sections: `overview`, `scenarios`, `runs`, `models`, `settings` — string keys used in `Sidebar`.
- Dashboard sections: `overview`, `scenarios`, `runs`, `models`, `batches`, `compare`, `settings` — string keys used in `Sidebar` (numbered nav). Non-`overview` views render styled "coming soon" placeholder panels until wired.
- Charts use Recharts (`LineChart`, `RadarChart`, `BarChart`) — don't add Chart.js or D3.
- 3D visualization uses `@react-three/fiber` + `@react-three/drei` — don't use raw Three.js imperative API in React components.
- CSS co-located: `Component.jsx` + `Component.css` same folder. No CSS modules, no Tailwind.
Expand All @@ -320,7 +340,7 @@ Add to `src/services/api.js` following existing pattern, then call `api.myNewMet
Backend streams at `WS /ws/simulation/{run_id}?token=<jwt>`; consumer wired end-to-end:

- `src/hooks/useSimulationStream.js` — owns WebSocket. Returns `{ frame, isConnected, status, error, latencyRef }`. Handles exponential-backoff reconnect (max 3 attempts), cleans up on unmount. Don't open sockets from components directly.
- `src/components/simulation/SimulationViewer.jsx` — R3F scene (road, ego, NPCs) + HTML HUD overlay (sim time, speed, g-force, metric bars, verdict badge). Mounted at `/simulation/:runId`. Has `← Dashboard` back button (glass style, centered top).
- `src/components/simulation/SimulationViewer.jsx` — R3F scene (road, ego, NPCs) + HTML HUD overlay (sim time, speed, g-force, metric bars, verdict badge). Mounted at `/simulation/:runId`. Has `← Dashboard` back button (`.btn-ghost`, top). Scene + overlay restyled to Mission Control palette; HUD = glassless instrument panels.
- Frame shape frozen in `SimulationEngine.get_tick_frame()`. To extend protocol: add fields there, consume in hook/viewer.
- Server closes with `{"event": "stream_end", ...}` — hook handles before deciding reconnect.
- Latency measured from `frame.emit_ts_ms` against client `Date.now()`; running average and max exposed via `latencyRef.current` for HUD display.
Expand Down
Loading
Loading