From 1d318f5bc3da68190c44a44f1fc770ca2e0fa74a Mon Sep 17 00:00:00 2001 From: gbudjeakp Date: Mon, 22 Jun 2026 13:13:31 -0600 Subject: [PATCH] fix: use CI platform hint for provider detection, clean job names - catalog: DetectMachine now takes a providerHint so GitHub Actions runners are matched against the github catalog only, preventing false GCP/AWS identifications on Azure-hosted runners - agent: add ciPlatformToProvider helper; pass hint at both flush and heartbeat call sites - action: job-id default changed from run_id-run_attempt to github.job so jobs appear as 'build', 'test', etc. in the UI - catalog: add catalog_test.go (14 tests covering Query, FindByID, DetectMachine with and without provider hint) - web: remove marketing pages (LandingPage, ComparePage, InstallPage, PricingPage) and their CSS; fix pagination always visible --- action.yml | 4 +- action/action.yml | 4 +- internal/agent/collector.go | 17 +- internal/catalog/catalog.go | 8 +- internal/catalog/catalog_test.go | 182 ++++++ web/src/pages/ComparePage.css | 272 -------- web/src/pages/ComparePage.tsx | 108 ---- web/src/pages/InstallPage.css | 467 ------------- web/src/pages/InstallPage.tsx | 768 ---------------------- web/src/pages/JobGroupPage.tsx | 2 +- web/src/pages/LandingPage.css | 221 ------- web/src/pages/LandingPage.tsx | 1043 ------------------------------ web/src/pages/PricingPage.css | 254 -------- web/src/pages/PricingPage.tsx | 89 --- 14 files changed, 209 insertions(+), 3230 deletions(-) create mode 100644 internal/catalog/catalog_test.go delete mode 100644 web/src/pages/ComparePage.css delete mode 100644 web/src/pages/ComparePage.tsx delete mode 100644 web/src/pages/InstallPage.css delete mode 100644 web/src/pages/InstallPage.tsx delete mode 100644 web/src/pages/LandingPage.css delete mode 100644 web/src/pages/LandingPage.tsx delete mode 100644 web/src/pages/PricingPage.css delete mode 100644 web/src/pages/PricingPage.tsx diff --git a/action.yml b/action.yml index a4a2edc..2c18a7a 100644 --- a/action.yml +++ b/action.yml @@ -33,9 +33,9 @@ inputs: required: false default: "${{ github.workspace }}/.runright" job-id: - description: "Unique job identifier (defaults to GitHub run ID)" + description: "Unique job identifier (defaults to the GitHub job name)" required: false - default: "${{ github.run_id }}-${{ github.run_attempt }}" + default: "${{ github.job }}" http-url: description: "runright backend URL for http export" required: false diff --git a/action/action.yml b/action/action.yml index 0267928..7a54c7d 100644 --- a/action/action.yml +++ b/action/action.yml @@ -32,9 +32,9 @@ inputs: required: false default: "${{ github.workspace }}/.runright" job-id: - description: "Unique job identifier (defaults to GitHub run ID)" + description: "Unique job identifier (defaults to the GitHub job name)" required: false - default: "${{ github.run_id }}-${{ github.run_attempt }}" + default: "${{ github.job }}" http-url: description: "runright backend URL for http export" required: false diff --git a/internal/agent/collector.go b/internal/agent/collector.go index f9e6eeb..c2b9bbb 100644 --- a/internal/agent/collector.go +++ b/internal/agent/collector.go @@ -143,7 +143,7 @@ func (c *Collector) sendHeartbeat() { summary.Status = "heartbeat" summary.CIPlatform = detectCIPlatform() if v, m := detectResources(); v > 0 && m > 0 { - summary.DetectedMachine = catalog.DetectMachine(v, m) + summary.DetectedMachine = catalog.DetectMachine(v, m, ciPlatformToProvider(summary.CIPlatform)) } // Write partial file so OOM-killed jobs still have data for recommendations. @@ -254,7 +254,7 @@ func (c *Collector) Flush() error { // Detect current machine. Cgroup-aware: reads container/pod limits first // (K8s pods, DinD), falls back to OS-level counts on bare VMs. if v, m := detectResources(); v > 0 && m > 0 { - summary.DetectedMachine = catalog.DetectMachine(v, m) + summary.DetectedMachine = catalog.DetectMachine(v, m, ciPlatformToProvider(summary.CIPlatform)) } summaryPath := filepath.Join(c.cfg.OutputDir, "metrics-summary.json") @@ -374,6 +374,19 @@ func detectCIPlatform() string { } } +// ciPlatformToProvider converts a CI platform name (from detectCIPlatform) to +// a catalog provider hint. Returns an empty Provider when the platform is not +// a known hosted-runner environment (e.g. Jenkins on a bare AWS VM — hardware +// detection should still match against the full catalog in that case). +func ciPlatformToProvider(platform string) types.Provider { + switch platform { + case "github": + return types.ProviderGitHub + default: + return "" + } +} + // detectResources returns the effective vCPU count and memory GiB for the // current execution environment. Priority: // 1. RUNRIGHT_VCPUS / RUNRIGHT_MEMORY_GIB env vars (manual override) diff --git a/internal/catalog/catalog.go b/internal/catalog/catalog.go index 720e89d..2c6e212 100644 --- a/internal/catalog/catalog.go +++ b/internal/catalog/catalog.go @@ -105,11 +105,17 @@ func FindByID(id string) *types.MachineType { // DetectMachine matches vCPU count and total memory against the catalog to // identify what machine the current runner is likely running on. -func DetectMachine(vcpus int, memTotalGiB float64) *types.MachineType { +// When providerHint is non-empty only machines from that provider are considered, +// which avoids false positives when the CI platform is known (e.g. GitHub Actions +// runners are hosted on Azure/GitHub infrastructure, not AWS or GCP). +func DetectMachine(vcpus int, memTotalGiB float64, providerHint types.Provider) *types.MachineType { const memToleranceGiB = 2.0 var best *types.MachineType for i := range allMachines { m := &allMachines[i] + if providerHint != "" && m.Provider != providerHint { + continue + } if m.VCPUs != vcpus { continue } diff --git a/internal/catalog/catalog_test.go b/internal/catalog/catalog_test.go new file mode 100644 index 0000000..9c70042 --- /dev/null +++ b/internal/catalog/catalog_test.go @@ -0,0 +1,182 @@ +package catalog + +import ( + "testing" + + "github.com/sgbudje/runright/internal/types" +) + +// TestAll_NonEmpty verifies the embedded catalog parsed correctly. +func TestAll_NonEmpty(t *testing.T) { + machines := All() + if len(machines) == 0 { + t.Fatal("catalog is empty") + } + + providers := map[types.Provider]int{} + for _, m := range machines { + providers[m.Provider]++ + } + for _, p := range []types.Provider{types.ProviderAWS, types.ProviderGCP, types.ProviderGitHub} { + if providers[p] == 0 { + t.Errorf("no machines found for provider %q", p) + } + } +} + +// TestQuery_FilterByProvider checks that provider filtering returns only the +// correct provider and nothing else. +func TestQuery_FilterByProvider(t *testing.T) { + for _, p := range []types.Provider{types.ProviderAWS, types.ProviderGCP, types.ProviderGitHub} { + results := Query(QueryOptions{Provider: p}) + if len(results) == 0 { + t.Errorf("no results for provider %q", p) + continue + } + for _, m := range results { + if m.Provider != p { + t.Errorf("provider filter %q returned machine with provider %q", p, m.Provider) + } + } + } +} + +// TestQuery_VCPURange checks that vCPU range filtering is applied correctly. +func TestQuery_VCPURange(t *testing.T) { + results := Query(QueryOptions{MinVCPUs: 4, MaxVCPUs: 8}) + if len(results) == 0 { + t.Fatal("expected results for vCPU range 4–8") + } + for _, m := range results { + if m.VCPUs < 4 || m.VCPUs > 8 { + t.Errorf("machine %s has %d vCPUs, outside range 4–8", m.ID, m.VCPUs) + } + } +} + +// TestQuery_MemoryRange checks that memory range filtering is applied correctly. +func TestQuery_MemoryRange(t *testing.T) { + results := Query(QueryOptions{MinMemoryGiB: 8, MaxMemoryGiB: 16}) + if len(results) == 0 { + t.Fatal("expected results for memory range 8–16 GiB") + } + for _, m := range results { + if m.MemoryGiB < 8 || m.MemoryGiB > 16 { + t.Errorf("machine %s has %.1f GiB, outside range 8–16", m.ID, m.MemoryGiB) + } + } +} + +// TestQuery_MaxPrice checks that the max price filter excludes expensive machines. +func TestQuery_MaxPrice(t *testing.T) { + limit := 0.10 + results := Query(QueryOptions{MaxPricePerHour: limit}) + if len(results) == 0 { + t.Fatal("expected results under $0.10/hr") + } + for _, m := range results { + if m.OnDemandPricePerHour > limit { + t.Errorf("machine %s costs $%.4f/hr, exceeds limit $%.2f", m.ID, m.OnDemandPricePerHour, limit) + } + } +} + +// TestQuery_SortedByPrice verifies results are returned in ascending price order. +func TestQuery_SortedByPrice(t *testing.T) { + results := Query(QueryOptions{Provider: types.ProviderAWS}) + for i := 1; i < len(results); i++ { + if results[i-1].OnDemandPricePerHour > results[i].OnDemandPricePerHour { + t.Errorf("results not sorted: index %d ($%.4f) > index %d ($%.4f)", + i-1, results[i-1].OnDemandPricePerHour, + i, results[i].OnDemandPricePerHour) + } + } +} + +// TestQuery_Architecture checks that architecture filtering works case-insensitively. +func TestQuery_Architecture(t *testing.T) { + arm := Query(QueryOptions{Architecture: "arm64"}) + if len(arm) == 0 { + t.Fatal("expected arm64 machines in catalog") + } + for _, m := range arm { + if m.Architecture != "arm64" && m.Architecture != "ARM64" { + t.Errorf("machine %s has architecture %q, not arm64", m.ID, m.Architecture) + } + } +} + +// TestFindByID_Known checks that a known machine ID is found. +func TestFindByID_Known(t *testing.T) { + // t3.micro is a well-known entry in the AWS catalog. + m := FindByID("t3.micro") + if m == nil { + t.Fatal("FindByID(\"t3.micro\") returned nil") + } + if m.Provider != types.ProviderAWS { + t.Errorf("expected provider aws, got %q", m.Provider) + } +} + +// TestFindByID_Missing checks that an unknown ID returns nil. +func TestFindByID_Missing(t *testing.T) { + if m := FindByID("does-not-exist"); m != nil { + t.Errorf("expected nil for unknown ID, got %+v", m) + } +} + +// TestDetectMachine_ExactMatch verifies a well-known GCP machine is detected by spec. +func TestDetectMachine_ExactMatch(t *testing.T) { + // e2-highcpu-16: 16 vCPUs, 16 GiB — from catalog + m := DetectMachine(16, 16.0, types.ProviderGCP) + if m == nil { + t.Fatal("expected a match for 16 vCPU / 16 GiB on GCP") + } + if m.Provider != types.ProviderGCP { + t.Errorf("expected GCP provider, got %q", m.Provider) + } +} + +// TestDetectMachine_GitHubHint verifies that when the provider hint is "github" +// a runner with 2 vCPUs and ~7 GiB does NOT match a GCP/AWS machine. +func TestDetectMachine_GitHubHint(t *testing.T) { + // 2 vCPUs, 7 GiB matches ubuntu-latest in the GitHub catalog. + m := DetectMachine(2, 7.0, types.ProviderGitHub) + if m == nil { + t.Fatal("expected ubuntu-latest match with github hint") + } + if m.Provider != types.ProviderGitHub { + t.Errorf("expected github provider, got %q (id=%s)", m.Provider, m.ID) + } +} + +// TestDetectMachine_NoHint_GCPWins verifies that without a provider hint a +// 16-vCPU / 16-GiB runner resolves to a GCP machine (e2-highcpu-16) because +// the GitHub catalog entry for 16 cores specifies 64 GiB. +func TestDetectMachine_NoHint_GCPWins(t *testing.T) { + m := DetectMachine(16, 16.0, "") + if m == nil { + t.Fatal("expected a match for 16 vCPU / 16 GiB without hint") + } + if m.Provider != types.ProviderGCP { + t.Logf("resolved to %s (%s) — may differ as catalog evolves", m.ID, m.Provider) + } +} + +// TestDetectMachine_NoMatch verifies that an impossible spec returns nil. +func TestDetectMachine_NoMatch(t *testing.T) { + m := DetectMachine(999, 9999.0, "") + if m != nil { + t.Errorf("expected nil for impossible spec, got %s", m.ID) + } +} + +// TestDetectMachine_ProviderHintExcludesOthers verifies that passing a provider +// hint prevents cross-provider matches. +func TestDetectMachine_ProviderHintExcludesOthers(t *testing.T) { + // e2-highcpu-16 is GCP — should NOT match when hint=aws. + m := DetectMachine(16, 16.0, types.ProviderAWS) + if m != nil && m.Provider != types.ProviderAWS { + t.Errorf("provider hint aws returned machine from %q", m.Provider) + } +} diff --git a/web/src/pages/ComparePage.css b/web/src/pages/ComparePage.css deleted file mode 100644 index 7c10d74..0000000 --- a/web/src/pages/ComparePage.css +++ /dev/null @@ -1,272 +0,0 @@ -/* ── Root ─────────────────────────────────────────────────────────────────── */ -.cp-root { - display: flex; - flex-direction: column; - min-height: 100vh; - background: var(--lp-bg); - color: var(--lp-text); - font-family: 'Lato', system-ui, sans-serif; -} - -/* ── Nav ──────────────────────────────────────────────────────────────────── */ -.cp-nav { - position: sticky; - top: 0; - z-index: 50; - display: flex; - justify-content: space-between; - align-items: center; - padding: 0 48px; - height: 56px; - border-bottom: 1px solid var(--lp-border); - background: var(--lp-bg); -} - -.cp-logo { - font-family: 'Bebas Neue', Impact, sans-serif; - font-size: 18px; - letter-spacing: 4px; - color: var(--lp-text); - text-decoration: none; - display: flex; - align-items: center; - gap: 8px; -} - -.cp-nav-right { - display: flex; - align-items: center; - gap: 24px; -} - -.cp-nav-link { - font-family: 'Bebas Neue', Impact, sans-serif; - font-size: 12px; - letter-spacing: 2px; - color: var(--lp-text-mid); - text-decoration: none; - transition: color .12s; -} -.cp-nav-link:hover { color: var(--lp-text); } -.cp-nav-link.active { color: var(--lp-text); } - -.cp-signin-btn { - font-family: 'Bebas Neue', Impact, sans-serif; - font-size: 12px; - letter-spacing: 2px; - color: #FBF0DC; - background: #2C1A0E; - border: 1.5px solid #2C1A0E; - padding: 7px 18px; - text-decoration: none; - cursor: pointer; - transition: background .15s; -} -.cp-signin-btn:hover { background: #C23B22; border-color: #C23B22; } - -/* ── Hero ─────────────────────────────────────────────────────────────────── */ -.cp-hero { - text-align: center; - padding: 72px 48px 56px; - border-bottom: 1px solid var(--lp-border); -} - -.cp-eyebrow { - font-family: 'Bebas Neue', Impact, sans-serif; - font-size: 12px; - letter-spacing: 5px; - color: #9A7B5A; - margin: 0 0 20px; -} - -.cp-title { - font-family: 'Playfair Display', Georgia, serif; - font-size: clamp(36px, 5vw, 56px); - font-weight: 900; - color: var(--lp-text); - line-height: 1.1; - margin: 0 0 20px; - letter-spacing: -0.5px; -} - -.cp-sub { - font-size: 17px; - color: var(--lp-text-mid); - max-width: 560px; - margin: 0 auto; - line-height: 1.8; -} - -.cp-sub em { font-style: italic; color: var(--lp-text); } - -/* ── Content ──────────────────────────────────────────────────────────────── */ -.cp-content { - flex: 1; - max-width: 1000px; - width: 100%; - margin: 72px auto; - padding: 0 80px; -} - -/* ── Table ────────────────────────────────────────────────────────────────── */ -.cp-table-scroll { - overflow-x: auto; - margin-bottom: 40px; -} - -.cp-table { - width: 100%; - border-collapse: collapse; - font-size: 13.5px; -} - -.cp-table thead th { - padding: 10px 18px; - font-family: 'Bebas Neue', Impact, sans-serif; - font-size: 11px; - letter-spacing: 1.5px; - text-align: center; - white-space: pre; - border-bottom: 2px solid var(--lp-border); - background: transparent; -} - -.cp-table thead th:first-child { - text-align: left; - padding-left: 12px; - color: #9A7B5A; - border-bottom-color: var(--lp-border); - white-space: nowrap; -} - -.cp-table thead th.col-rr { - color: #C23B22; - border-bottom-color: #C23B22; - background: rgba(194, 59, 34, .05); -} - -.cp-table tbody tr:nth-child(even) { - background: var(--lp-paper); -} - -.cp-table tbody tr { - transition: background .12s; -} -.cp-table tbody tr:hover { - background: rgba(44, 26, 14, .06) !important; -} -.lp-root.lp-dark .cp-table tbody tr:hover { - background: rgba(251, 240, 220, .08) !important; -} - -.cp-table tbody td { - padding: 12px 18px; - border-bottom: 1px solid var(--lp-border); - text-align: center; - color: var(--lp-border); - font-size: 15px; -} - -.cp-table tbody td:first-child { - text-align: left; - padding-left: 12px; - color: var(--lp-text); - font-size: 13.5px; - line-height: 1.4; - min-width: 220px; - padding-right: 24px; -} - -.cp-table tbody td.col-rr { - background: rgba(194, 59, 34, .05); - font-weight: 700; - color: #C23B22; -} - -.cp-table tbody td.check-other { color: var(--lp-text); } -.cp-table tbody td.partial { - font-size: 11px; - letter-spacing: 0.5px; - text-transform: uppercase; - color: #9A7B5A; -} - -/* ── Callout — dark brown, NOT red ───────────────────────────────────────── */ -.cp-callout { - background: var(--lp-paper); - border: 1px solid var(--lp-border); - border-left: 3px solid #2C1A0E; - padding: 24px 28px; - margin-bottom: 48px; -} - -.lp-root.lp-dark .cp-callout { - border-left-color: var(--lp-border); -} - -.cp-callout-label { - font-family: 'Bebas Neue', Impact, sans-serif; - font-size: 11px; - letter-spacing: 3px; - color: var(--lp-text); - margin-bottom: 10px; -} - -.cp-callout p { - font-size: 14px; - color: var(--lp-text-mid); - line-height: 1.85; - margin: 0; -} - -.cp-callout strong { color: var(--lp-text); } - -/* ── CTA strip ────────────────────────────────────────────────────────────── */ -.cp-cta { - text-align: center; - padding: 16px 0 8px; -} - -.cp-cta-btn { - display: inline-block; - background: #C23B22; - color: #FBF0DC; - border: none; - padding: 13px 40px; - font-family: 'Bebas Neue', Impact, sans-serif; - font-size: 16px; - letter-spacing: 3px; - cursor: pointer; - text-decoration: none; - box-shadow: 4px 4px 0 rgba(92, 58, 30, .18); - transition: background .15s, transform .08s; -} -.cp-cta-btn:hover { background: #9B2D17; transform: translate(2px, 2px); } - -/* ── Footer ───────────────────────────────────────────────────────────────── */ -.cp-footer { - border-top: 1px solid var(--lp-border); - padding: 20px 48px; - display: flex; - justify-content: space-between; - align-items: center; - font-family: 'Bebas Neue', Impact, sans-serif; - font-size: 11px; - letter-spacing: 2px; - color: var(--lp-text-light); -} - -.cp-footer-link { - color: var(--lp-text-light); - text-decoration: none; - transition: color .12s; -} -.cp-footer-link:hover { color: var(--lp-text); } - -/* ── Responsive ───────────────────────────────────────────────────────────── */ -@media (max-width: 768px) { - .cp-nav { padding: 0 20px; } - .cp-hero { padding: 48px 24px 40px; } - .cp-content { padding: 0 20px; margin: 40px auto; } - .cp-footer { padding: 16px 20px; flex-direction: column; gap: 10px; text-align: center; } -} diff --git a/web/src/pages/ComparePage.tsx b/web/src/pages/ComparePage.tsx deleted file mode 100644 index bd907d6..0000000 --- a/web/src/pages/ComparePage.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import { Link } from 'react-router-dom' -import LogoMark from '../components/LogoMark' -import PageNav from '../components/PageNav' -import './LandingPage.css' -import './ComparePage.css' - -type Row = [string, string, string, string, string, string] - -const TOOLS = ['Datadog CI', 'Grafana /\nPrometheus', 'Sentry /\nElastic APM', 'AWS & GCP\nCost Tools', 'RunRight'] - -const ROWS: Row[] = [ - ['Pipeline duration & trends', '✓', '✓', '—', '—', '—'], - ['Error & trace tracking', '✓', '—', '✓', '—', '—'], - ['Raw CPU / memory graphs', '✓', '✓', '—', '—', '✓'], - ['Works on ephemeral CI machines', '—', '—', '—', '—', '✓'], - ['Maps usage to a specific instance SKU', '—', '—', '—', 'partial', '✓'], - ['Recommends cheaper alternative + cost delta', '—', '—', '—', 'partial', '✓'], - ['No code changes needed', '—', '✓', '—', '✓', '✓'], - ['Self-hosted, no SaaS', '—', '✓', '—', '—', '✓'], -] - -export default function ComparePage({ dark, onToggleDark }: { dark: boolean; onToggleDark: () => void }) { - return ( -
- - {/* Nav */} - - - {/* Hero */} -
-

VS THE ALTERNATIVES

-

Built for a problem others don't solve.

-

- Datadog, Sentry, and AWS Cost Tools answer "what happened?"{' '} - RunRight answers "am I using the right machine?" -

-
- - {/* Content */} -
- - {/* Comparison table */} -
- - - - - {TOOLS.map((t, i) => ( - - ))} - - - - {ROWS.map(([label, ...vals], ri) => ( - - - {vals.map((v, ci) => { - const isRR = ci === TOOLS.length - 1 - const cls = [ - isRR ? 'col-rr' : '', - v === '✓' && !isRR ? 'check-other' : '', - v === 'partial' ? 'partial' : '', - ].filter(Boolean).join(' ') - return ( - - ) - })} - - ))} - -
FEATURE{t}
{label} - {v === 'partial' ? 'Partial' : v} -
-
- - {/* Callout — dark brown, not red */} -
-
Why cloud-native tools miss CI machines
-

- AWS Cost Optimizer and GCP Recommender require weeks of utilization history on{' '} - long-running instances. CI machines are ephemeral — - they spin up, run a 5–30 minute job, then terminate. The cloud has no visibility into - what your GitHub Actions runner or Jenkins agent actually did. - RunRight captures the entire run in real time and produces a recommendation the - moment the job finishes. Use it alongside Datadog or Sentry via OTLP — you don't - have to choose. -

-
- - {/* CTA */} -
- Open Dashboard → -
- -
- - {/* Footer */} -
- - RUNRIGHT - - Pricing - Back to home -
- -
- ) -} diff --git a/web/src/pages/InstallPage.css b/web/src/pages/InstallPage.css deleted file mode 100644 index a47f60d..0000000 --- a/web/src/pages/InstallPage.css +++ /dev/null @@ -1,467 +0,0 @@ -/* ── Root ──────────────────────────────────────────────────────────────────── */ -.ip-root { - display: flex; - flex-direction: column; - min-height: 100vh; - background: var(--lp-bg); - color: var(--lp-text); - font-family: 'Lato', system-ui, sans-serif; -} - -/* ── Top nav ───────────────────────────────────────────────────────────────── */ -.ip-topnav { - position: sticky; - top: 0; - z-index: 50; - display: flex; - justify-content: space-between; - align-items: center; - padding: 0 32px; - height: 56px; - border-bottom: 1px solid var(--lp-border); - background: var(--lp-bg); -} - -.ip-logo { - font-family: 'Bebas Neue', Impact, sans-serif; - font-size: 18px; - letter-spacing: 4px; - color: var(--lp-text); - text-decoration: none; -} - -.ip-topnav-right { - display: flex; - align-items: center; - gap: 20px; -} - -.ip-topnav-link { - font-family: 'Bebas Neue', Impact, sans-serif; - font-size: 12px; - letter-spacing: 2px; - color: var(--lp-text-mid); - text-decoration: none; - transition: color .12s; -} -.ip-topnav-link:hover { color: var(--lp-text); } - -.ip-theme-btn { - margin-left: 4px; -} - -/* ── Body: sidebar + content ──────────────────────────────────────────────── */ -.ip-body { - display: flex; - align-items: flex-start; /* required for position:sticky on sidebar */ - flex: 1; - max-width: 1200px; - margin: 0 auto; - width: 100%; - padding: 0 24px; - gap: 0; -} - -/* ── Sidebar ───────────────────────────────────────────────────────────────── */ -.ip-sidebar { - width: 220px; - flex-shrink: 0; - padding: 32px 0 40px; - position: sticky; - top: 56px; - height: calc(100vh - 56px); - overflow-y: auto; - border-right: 1px solid var(--lp-border); -} - -.ip-sidebar-eyebrow { - font-family: 'Bebas Neue', Impact, sans-serif; - font-size: 10px; - letter-spacing: 3px; - color: var(--lp-text-light); - text-transform: uppercase; - margin: 0 0 20px 16px; -} - -.ip-nav-group { - margin-bottom: 24px; -} - -.ip-nav-group-label { - font-family: 'Bebas Neue', Impact, sans-serif; - font-size: 10px; - letter-spacing: 2.5px; - text-transform: uppercase; - color: var(--lp-text-light); - margin: 0 0 4px 16px; - padding-bottom: 4px; - border-bottom: 1px solid var(--lp-border); -} - -.ip-nav-item { - display: block; - padding: 7px 16px; - font-size: 13.5px; - color: var(--lp-text-mid); - text-decoration: none; - border-left: 2px solid transparent; - transition: color .12s, border-color .12s, background .12s; - line-height: 1.3; -} - -.ip-nav-item:hover { - color: var(--lp-text); - background: var(--lp-paper-alt); -} - -.ip-nav-item.active { - color: var(--lp-text); - border-left-color: #B8860B; - background: var(--lp-paper-alt); - font-weight: 600; -} - -/* ── Main content ──────────────────────────────────────────────────────────── */ -.ip-content { - flex: 1; - min-width: 0; - padding: 40px 48px 80px; -} - -/* ── Section ───────────────────────────────────────────────────────────────── */ -.ip-section { - margin-bottom: 72px; - scroll-margin-top: 80px; -} - -.ip-eyebrow { - font-family: 'Bebas Neue', Impact, sans-serif; - font-size: 11px; - letter-spacing: 3px; - color: #9A7B5A; - text-transform: uppercase; - margin: 0 0 12px; -} - -.ip-h1 { - font-family: 'Playfair Display', Georgia, serif; - font-size: clamp(30px, 3.5vw, 44px); - font-weight: 900; - line-height: 1.1; - color: var(--lp-text); - margin: 0 0 20px; -} - -.ip-lead { - font-size: 16px; - color: var(--lp-text-mid); - line-height: 1.85; - margin: 0 0 16px; - max-width: 680px; -} - -.ip-h2 { - font-family: 'Playfair Display', Georgia, serif; - font-size: 24px; - font-weight: 700; - color: var(--lp-text); - margin: 0 0 12px; - padding-bottom: 10px; - border-bottom: 1px solid var(--lp-border); -} - -.ip-h3 { - font-family: 'Bebas Neue', Impact, sans-serif; - font-size: 14px; - letter-spacing: 2px; - color: var(--lp-text); - margin: 28px 0 8px; - text-transform: uppercase; -} - -.ip-p { - font-size: 14.5px; - color: var(--lp-text-mid); - line-height: 1.85; - margin: 0 0 14px; -} - -.ip-p code, -.ip-p strong, -.ip-callout code, -.ip-callout strong { - color: var(--lp-text); -} - -.ip-p code { - font-family: 'Courier New', monospace; - font-size: 13px; - background: var(--lp-paper-alt); - border: 1px solid var(--lp-border); - padding: 1px 5px; -} - -/* ── 60-second quickstart banner ───────────────────────────────────────────── */ -.ip-quickstart { - background: #1A0F02; - border: 1px solid #3a2510; - border-left: 3px solid #B8860B; - padding: 28px 32px; - margin-bottom: 40px; -} - -.ip-qs-header { - display: flex; - align-items: center; - gap: 16px; - margin-bottom: 16px; - flex-wrap: wrap; -} - -.ip-qs-badge { - font-family: 'Bebas Neue', Impact, sans-serif; - font-size: 11px; - letter-spacing: 3px; - color: #B8860B; - background: rgba(184,134,11,.12); - border: 1px solid rgba(184,134,11,.3); - padding: 3px 10px; - white-space: nowrap; -} - -.ip-qs-sub { - font-size: 13px; - color: #C4A882; -} - -.ip-qs-tabs { - display: flex; - gap: 0; - margin-bottom: 0; - border-bottom: 1px solid #3a2510; -} - -.ip-qs-tab { - background: none; - border: none; - color: #9A7B5A; - font-family: 'Bebas Neue', Impact, sans-serif; - font-size: 12px; - letter-spacing: 1.5px; - padding: 7px 16px 7px 0; - cursor: pointer; - transition: color .12s; - margin-right: 20px; - border-bottom: 2px solid transparent; - margin-bottom: -1px; -} -.ip-qs-tab.active, -.ip-qs-tab:hover { color: #E8C458; border-bottom-color: #B8860B; } - -.ip-qs-code { - font-family: 'Courier New', monospace; - font-size: 13px; - line-height: 2; - padding: 16px 0 8px; - white-space: pre; - overflow-x: auto; -} - -.ip-qs-key { color: #C4A882; } -.ip-qs-val { color: #E8C458; } -.ip-qs-punct { color: #9A7B5A; } -.ip-qs-comment { color: #5A4030; } - -.ip-qs-result { - font-size: 13px; - color: #9A7B5A; - margin: 8px 0 0; - line-height: 1.6; - border-top: 1px solid #3a2510; - padding-top: 12px; -} -.ip-qs-result strong { color: #C4A882; font-weight: 600; } - -/* ── Overview grid ─────────────────────────────────────────────────────────── */ -.ip-overview-grid { - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: 14px; - margin-top: 28px; -} - -.ip-overview-card { - border: 1px solid var(--lp-border); - background: var(--lp-paper-alt); - padding: 20px; - display: flex; - flex-direction: column; - gap: 6px; -} - -.ip-overview-title { - font-family: 'Bebas Neue', Impact, sans-serif; - font-size: 13px; - letter-spacing: 1.5px; - color: var(--lp-text); - text-transform: uppercase; -} - -.ip-overview-body { - font-size: 13px; - color: var(--lp-text-mid); - line-height: 1.6; - margin: 0; -} - -/* ── Code blocks ───────────────────────────────────────────────────────────── */ -.ip-code-block { - margin: 14px 0 20px; -} - -.ip-code-header { - display: flex; - justify-content: space-between; - align-items: center; - background: #2C1A0E; - padding: 7px 16px; - border-left: 3px solid #B8860B; -} - -.ip-code-label { - font-family: 'Bebas Neue', Impact, sans-serif; - font-size: 11px; - letter-spacing: 2px; - color: #9A7B5A; -} - -.ip-copy-btn { - background: transparent; - border: 1px solid #4a3520; - color: #9A7B5A; - padding: 3px 12px; - font-family: 'Bebas Neue', Impact, sans-serif; - font-size: 11px; - letter-spacing: 1.5px; - cursor: pointer; - transition: all .15s; -} -.ip-copy-btn:hover { color: #FBF0DC; border-color: #9A7B5A; } -.ip-copy-btn.copied { background: #2E7D32; border-color: #2E7D32; color: #FBF0DC; } - -.ip-pre { - background: #1A0F02; - border-left: 3px solid #B8860B; - border-right: 1px solid #3a2510; - border-bottom: 1px solid #3a2510; - padding: 16px 20px; - font-family: 'Courier New', monospace; - font-size: 13px; - color: #D4A82A; - line-height: 1.9; - margin: 0; - overflow-x: auto; - white-space: pre; -} - -/* ── Callouts ──────────────────────────────────────────────────────────────── */ -.ip-callout { - padding: 14px 18px; - margin: 18px 0; - font-size: 14px; - line-height: 1.75; - color: var(--lp-text-mid); -} - -.ip-callout-label { - display: block; - font-family: 'Bebas Neue', Impact, sans-serif; - font-size: 11px; - letter-spacing: 2px; - margin-bottom: 6px; -} - -.ip-callout-info { border-left: 3px solid #1B3361; background: rgba(27,51,97,.07); } -.ip-callout-info .ip-callout-label { color: #1B3361; } - -.ip-callout-warn { border-left: 3px solid #B8860B; background: rgba(184,134,11,.07); } -.ip-callout-warn .ip-callout-label { color: #B8860B; } - -.ip-callout-tip { border-left: 3px solid #2E7D32; background: rgba(46,125,50,.07); } -.ip-callout-tip .ip-callout-label { color: #2E7D32; } - -/* ── Tables ────────────────────────────────────────────────────────────────── */ -.ip-table-wrap { - overflow-x: auto; - margin: 16px 0; -} - -.ip-table { - width: 100%; - border-collapse: collapse; - font-size: 13.5px; -} - -.ip-table th { - text-align: left; - padding: 9px 14px; - font-family: 'Bebas Neue', Impact, sans-serif; - font-size: 11px; - letter-spacing: 1.5px; - text-transform: uppercase; - color: var(--lp-text-light); - border-bottom: 2px solid var(--lp-border); - background: var(--lp-paper-alt); - white-space: nowrap; -} - -.ip-table td { - padding: 10px 14px; - border-bottom: 1px solid var(--lp-border); - color: var(--lp-text-mid); - line-height: 1.6; - vertical-align: top; -} - -.ip-table td code { - font-family: 'Courier New', monospace; - font-size: 12.5px; - color: var(--lp-text); - background: var(--lp-paper-alt); - border: 1px solid var(--lp-border); - padding: 1px 5px; - white-space: nowrap; -} - -.ip-ok { color: #2E7D32 !important; } -.ip-warn-text { color: #B8860B !important; } - -/* ── Footer ────────────────────────────────────────────────────────────────── */ -.ip-footer { - display: flex; - justify-content: space-between; - align-items: center; - padding: 22px 56px; - border-top: 1px solid var(--lp-border); - font-family: 'Bebas Neue', Impact, sans-serif; - font-size: 12px; - letter-spacing: 2px; - color: var(--lp-text-light); -} - -.ip-footer-link { - color: var(--lp-text-light); - text-decoration: none; - transition: color .12s; -} -.ip-footer-link:hover { color: var(--lp-text); } - -/* ── Responsive ────────────────────────────────────────────────────────────── */ -@media (max-width: 768px) { - .ip-body { flex-direction: column; padding: 0; } - .ip-sidebar { display: none; } - .ip-content { padding: 28px 20px 60px; } - .ip-overview-grid { grid-template-columns: 1fr; } - .ip-topnav { padding: 0 20px; } - .ip-footer { padding: 20px; } -} diff --git a/web/src/pages/InstallPage.tsx b/web/src/pages/InstallPage.tsx deleted file mode 100644 index 112e42c..0000000 --- a/web/src/pages/InstallPage.tsx +++ /dev/null @@ -1,768 +0,0 @@ -import { useState, useEffect, useRef } from 'react' -import { Link } from 'react-router-dom' -import PageNav from '../components/PageNav' -import './LandingPage.css' -import './InstallPage.css' - -// ── Copy block ──────────────────────────────────────────────────────────────── -function CopyBlock({ label, code }: { label: string; code: string }) { - const [copied, setCopied] = useState(false) - function copy() { - navigator.clipboard.writeText(code).then(() => { - setCopied(true) - setTimeout(() => setCopied(false), 2000) - }) - } - return ( -
-
- {label} - -
-
{code}
-
- ) -} - -// ── Callout ─────────────────────────────────────────────────────────────────── -function Callout({ type, children }: { type: 'info' | 'warn' | 'tip'; children: React.ReactNode }) { - const cls = { info: 'ip-callout-info', warn: 'ip-callout-warn', tip: 'ip-callout-tip' } - const labels = { info: 'Note', warn: 'Important', tip: 'Tip' } - return ( -
- {labels[type]} - {children} -
- ) -} - -// ── Simple table ────────────────────────────────────────────────────────────── -function EnvTable({ rows }: { rows: { name: string; where: string; required: string; description: string }[] }) { - return ( -
- - - {['Variable', 'Where', 'Required', 'Description'].map(h => )} - - - {rows.map(r => ( - - - - - - - ))} - -
{h}
{r.name}{r.where}{r.required}{r.description}
-
- ) -} - -// ── Sidebar nav data ────────────────────────────────────────────────────────── -const NAV = [ - { - group: 'Getting Started', - items: [ - { id: 'overview', label: 'Overview' }, - { id: 'no-backend', label: 'Quick Start (file-only)' }, - ], - }, - { - group: 'CI Platforms', - items: [ - { id: 'github-actions', label: 'GitHub Actions' }, - { id: 'gitlab-ci', label: 'GitLab CI' }, - { id: 'jenkins', label: 'Jenkins' }, - ], - }, - { - group: 'Infrastructure', - items: [ - { id: 'kubernetes', label: 'Kubernetes / Self-Hosted' }, - { id: 'helm', label: 'Helm (EKS / GKE / any K8s)' }, - { id: 'terraform-ecs', label: 'Terraform — ECS Fargate' }, - { id: 'terraform-eks', label: 'Terraform — EKS' }, - { id: 'terraform-gcp', label: 'Terraform — GKE' }, - ], - }, - { - group: 'Backend', - items: [ - { id: 'backend-setup', label: 'Self-Hosting the Backend' }, - { id: 'api-key', label: 'API Key' }, - ], - }, - { - group: 'Reference', - items: [ - { id: 'env-vars', label: 'Environment Variables' }, - { id: 'cli', label: 'CLI Reference' }, - ], - }, -] - -// ── Main page ───────────────────────────────────────────────────────────────── -export default function InstallPage({ dark, onToggleDark }: { dark: boolean; onToggleDark: () => void }) { - const [activeId, setActiveId] = useState('overview') - const contentRef = useRef(null) - - // Scroll-spy: update active nav item as user scrolls - useEffect(() => { - const allIds = NAV.flatMap(g => g.items.map(i => i.id)) - const observer = new IntersectionObserver( - entries => { - // pick the topmost visible section - const visible = entries.filter(e => e.isIntersecting).sort((a, b) => a.boundingClientRect.top - b.boundingClientRect.top) - if (visible.length > 0) setActiveId(visible[0].target.id) - }, - { rootMargin: '-20% 0px -60% 0px', threshold: 0 } - ) - allIds.forEach(id => { - const el = document.getElementById(id) - if (el) observer.observe(el) - }) - return () => observer.disconnect() - }, []) - - return ( -
- - {/* ── Top nav ── */} - - - {/* ── Body (sidebar + content) ── */} -
- - {/* Sidebar */} - - - {/* Main content */} -
- - {/* ── 60-second quick-start banner ─────────────────────────────── */} -
-
- 60-SECOND INSTALL - Works on any CI runner. No backend required. -
-
- {(['GitHub Actions', 'Bare metal / other CI'] as const).map((tab, i) => ( - - ))} -
-
- # Add to any job in .github/workflows/ci.yml - {'\n'} - - uses - : - gbudjeakp/run-right@v1 - {'\n'} - {' '} - with - : - {'\n'} - {' '} - run - : - make build - {' '} - # ← your command here -
-

- Recommendations appear in the Job Summary tab after the run. No config, no API key, no database. -

-
- - {/* ── Overview ─────────────────────────────────────────────────── */} -
-

Documentation

-

Install RunRight

-

- Drop one step into any CI workflow. RunRight monitors your job in the background, - detects the machine it ran on, and recommends a right-sized alternative — saving - you money without guesswork. -

-

- No backend or API key needed to start. Just add one step, and recommendations - appear in your job log and step summary. -

-
- {[ - { title: '2 min setup', body: 'One action step. No sidecar, no DaemonSet, no infra changes.' }, - { title: 'No backend required', body: 'Recommendations work with file export alone. Backend is optional.' }, - { title: 'API key optional', body: 'Auth is disabled by default. Only enable it when you self-host.' }, - { title: 'Any runner', body: 'GitHub-hosted, self-hosted, GitLab, and Kubernetes runners are all supported.' }, - ].map(c => ( -
- {c.title} -

{c.body}

-
- ))} -
-
- - {/* ── Quick Start ──────────────────────────────────────────────── */} -
-

Quick Start — No Backend Needed

-

- The fastest path: file export only. No server, no database, no - API key. The agent writes a metrics-summary.json artifact and prints - recommendations directly to the job log and step summary. -

- -

Wrapper mode (recommended)

-

Wrap any command with run: — RunRight monitors it start to finish.

- - -

Standalone mode

-

Use step: start / step: stop to span multiple steps.

- - - - Recommendations appear in the Job Summary tab of your GitHub Actions - run — no extra setup required. - -
- - {/* ── GitHub Actions ───────────────────────────────────────────── */} -
-

GitHub Actions

-

All inputs and their defaults:

- - -

Using action outputs

-

Consume results in downstream steps:

- -
- - {/* ── GitLab CI ────────────────────────────────────────────────── */} -
-

GitLab CI

-

- Install the binary in before_script and send SIGTERM in - after_script — which always runs even when the job fails, - so data is captured on OOM kills and runner disconnects too. -

- .runright/monitor.pid - - script: - - make build - - after_script: - - kill \$(cat .runright/monitor.pid 2>/dev/null) 2>/dev/null || true - - artifacts: - paths: [.runright/] - expire_in: 30 days`} /> - - - Set RUNRIGHT_URL and optionally RUNRIGHT_API_KEY as - masked variables in Settings → CI/CD → Variables. - -
- - {/* ── Jenkins ────────────────────────────────────────────── */} -
-

Jenkins

-

- Use Jenkins credentials bindings to inject RUNRIGHT_URL and - RUNRIGHT_API_KEY, then run the monitor as a background process - inside a sh block. Since Jenkins doesn't have an - after_script, kill the monitor at the end of the same shell block. -

- .runright.pid - make build - kill \\$(cat .runright.pid) 2>/dev/null || true - """ - } - } - } -}`} /> - - Add credentials in Manage Jenkins → Credentials as - "Secret text" entries with IDs runright-url and - runright-api-key. - -
- - {/* ── Kubernetes ───────────────────────────────────────────────── */} -
-

Kubernetes / Self-Hosted Runners

-

- RunRight runs inside your CI job — not as a DaemonSet or sidecar. - It auto-detects CPU and memory limits from cgroup v2 with no - extra config on any K8s-hosted runner. -

- -

GitHub Actions self-hosted on K8s

- - -

Optional: Downward API for guaranteed limit detection

-

- If cgroup namespacing hides the container limits, inject them explicitly via - RUNRIGHT_VCPUS and RUNRIGHT_MEMORY_GIB: -

- - -

GitLab Runner on K8s

- .pid - script: - - make build - after_script: - - kill \$(cat .pid 2>/dev/null) 2>/dev/null || true`} /> -
- - {/* ── Helm ─────────────────────────────────────────────────────── */} -
-

Helm — EKS & Any Kubernetes Cluster

-

- The RunRight Helm chart deploys the backend and dashboard onto any Kubernetes - cluster — EKS, GKE, AKS, or self-managed. It bundles a PostgreSQL subchart by - default, or point it at an existing database with externalDSN. -

- -

Quick install

- - -

With an existing Postgres database

- - -

With an Ingress (EKS ALB controller)

- - - - - The Helm chart works identically on GKE (use nginx or gce ingress class) - and AKS (use azure/application-gateway or nginx). - - -

With an Ingress (GKE — Cloud Load Balancer)

- - - -

K8s resource recommendations

-

- Every recommendation in the RunRight dashboard now includes suggested Kubernetes - resource requests and limits based on observed p95 usage: -

- -

- Requests are set to p95 usage with headroom. Limits are set to peak usage with - a safety margin — enough to absorb a spike without triggering an OOM kill or - CPU throttle. -

-
- - {/* ── Terraform ECS ────────────────────────────────────────────── */} -
-

Terraform — AWS ECS Fargate

-

- The terraform/ module deploys RunRight on AWS Fargate with RDS - PostgreSQL, Secrets Manager for credentials, CloudWatch logging, and an optional - ALB target group attachment. All in one terraform apply. -

- - -

- Set create_rds = false and external_dsn to bring your - own Postgres (RDS in another account, Aurora, Neon, etc.). -

-
- - {/* ── Terraform EKS ────────────────────────────────────────────── */} -
-

Terraform — EKS

-

- The terraform/eks/ module targets an existing EKS cluster. It - provisions an optional RDS instance, creates a Kubernetes namespace and secret, - then installs RunRight via the Helm chart using the Terraform Helm provider — - so the whole stack is managed in one plan. -

- - - - Set create_rds = false and supply external_dsn to reuse - an existing Aurora or RDS cluster shared across your platform services. - -
- - {/* ── Terraform GKE ────────────────────────────────────────────── */} -
-

Terraform — GCP GKE

-

- The terraform/gke/ module provisions a GKE Autopilot cluster, a - Cloud SQL for PostgreSQL instance, and a Workload Identity binding so the - RunRight pod can read secrets from GCP Secret Manager without a service-account - key file. The Helm chart is then installed via the Terraform Helm provider. -

- - - - Workload Identity is enabled by default. The module creates a GCP service account, - binds it to the Kubernetes service account in the runright namespace, - and grants roles/secretmanager.secretAccessor so the pod can pull - credentials without a JSON key file. - - - Set create_cloud_sql = false and supply external_dsn - to reuse an existing Cloud SQL or AlloyDB instance. Useful when RunRight shares - infrastructure with other platform services. - -
- - {/* ── Backend Setup ────────────────────────────────────────────── */} -
-

Self-Hosting the Backend

-

- The backend is the recommended way to use RunRight at team scale. It gives you - persistent job history, a shared dashboard, trend charts, and PR comment - recommendations that update on every push. It's a single Go binary + Postgres — - spin it up with Docker Compose. -

- - Self-hosting takes about two minutes with Docker Compose. Once running, point - every workflow at it with http-url and all job data flows to a - single place your whole team can view. - - - -
- - {/* ── API Key ──────────────────────────────────────────────────── */} -
-

API Key — When You Need It

-

- The API key is a server-side guard. It has nothing to do with getting - recommendations — you get full machine sizing output without ever setting one. - It only matters when you self-host the backend and want to restrict who can - write to your dashboard. -

- -
- - - - - - - - {[ - ['File export only (no backend)', 'No', 'Data stays local — nothing authenticates'], - ['Backend, RUNRIGHT_API_KEY unset', 'No', 'Auth is disabled — dev mode'], - ['Backend, RUNRIGHT_API_KEY set', 'Yes — set the same key in CI', 'Agent sends it as Authorization: Bearer'], - ['Dashboard login', 'Yes — if backend has a key', 'Login screen checks it against the server'], - ].map(([s, n, w]) => ( - - - - - - ))} - -
ScenarioNeed API key?Why
{s}{n}{w}
-
- -

- The agent reads RUNRIGHT_API_KEY from the environment automatically — no flag needed. -

- - - - Treat the API key like a password. Use your platform's secret store (GitHub Secrets, - GitLab Variables, K8s Secrets) and never commit it to source control. - -
- - {/* ── Env Vars ─────────────────────────────────────────────────── */} -
-

Environment Variables

- -
- - {/* ── CLI Reference ────────────────────────────────────────────── */} -
-

CLI Reference

- -

runright monitor

- - -

runright recommend

- - -

runright serve

- -
- -
-
- - {/* Footer */} -
- RUNRIGHT -
- Home - GitHub -
-
-
- ) -} diff --git a/web/src/pages/JobGroupPage.tsx b/web/src/pages/JobGroupPage.tsx index 8058698..a7a7b30 100644 --- a/web/src/pages/JobGroupPage.tsx +++ b/web/src/pages/JobGroupPage.tsx @@ -266,7 +266,7 @@ export default function JobGroupPage() { - {totalPages > 1 && ( + {(totalPages > 1 || runs.length > 0) && (
ROWS diff --git a/web/src/pages/LandingPage.css b/web/src/pages/LandingPage.css deleted file mode 100644 index c800adb..0000000 --- a/web/src/pages/LandingPage.css +++ /dev/null @@ -1,221 +0,0 @@ -/* ── Theme custom properties ───────────────────────────────────── - Applied on the root div (.lp-root). The dark variant is toggled - by adding .lp-dark to that same element. */ -.lp-root { - --lp-bg: #FBF0DC; - --lp-paper: #FFFDF7; - --lp-paper-alt: #F3E2C2; - --lp-text: #2C1A0E; - --lp-text-mid: #6B4226; - --lp-text-light: #9A7B5A; - --lp-border: #D4B896; - --lp-works-bg: #F3E2C2; - --lp-dark-strip: #2C1A0E; -} -.lp-root.lp-dark { - --lp-bg: #130C05; - --lp-paper: #1E1209; - --lp-paper-alt: #251708; - --lp-text: #F5E4C8; - --lp-text-mid: #C09870; - --lp-text-light: #9A7B5A; - --lp-border: #3D2810; - --lp-works-bg: #1E1209; - --lp-dark-strip: #0A0603; -} - -/* ── Dark mode toggle button ─────────────────────────────────── */ -.lp-theme-toggle { - background: none; - border: 1.5px solid var(--lp-border); - color: var(--lp-text-light); - width: 36px; - height: 36px; - display: inline-flex; - align-items: center; - justify-content: center; - cursor: pointer; - transition: border-color .15s, color .15s; - padding: 0; - flex-shrink: 0; -} -.lp-theme-toggle:hover { - border-color: var(--lp-text); - color: var(--lp-text); -} - -/* ── Sign In button (managed via class, not inline style) ─────── */ -.lp-signin-btn { - background: #2C1A0E !important; - color: #FBF0DC !important; - border-color: #2C1A0E !important; -} -.lp-root.lp-dark .lp-signin-btn { - background: var(--lp-border) !important; - color: var(--lp-bg) !important; - border-color: var(--lp-border) !important; -} -.lp-signin-btn:hover, -.lp-root.lp-dark .lp-signin-btn:hover { - background: #C23B22 !important; - border-color: #C23B22 !important; - color: #FBF0DC !important; -} - -/* ── Nav GitHub link hover ───────────────────────────────────── */ -.lp-nav-github:hover { - background: #C23B22 !important; - color: #FBF0DC !important; - border-color: #C23B22 !important; -} - -/* ── LandingPage responsive overrides ──────────────────────────── - Inline styles control colours / typography; these classes handle - layout breakpoints. !important is intentional: overriding inline. */ - -/* ── Tablet (≤ 900px) ─────────────────────────────────────────── */ -@media (max-width: 900px) { - .lp-nav { - padding: 16px 28px !important; - } - - .lp-hero { - padding: 64px 32px 56px !important; - } - - .lp-hero-inner { - flex-direction: column !important; - gap: 40px !important; - padding: 64px 32px 56px !important; - } - - .lp-hero-visual { display: none !important; } - - .lp-proof-bar { - padding: 12px 28px !important; - gap: 24px !important; - } - - .lp-section { - padding: 56px 32px !important; - } - - .lp-steps { - grid-template-columns: 1fr !important; - gap: 20px !important; - } - - .lp-feature-strip { - padding: 52px 32px !important; - } - - .lp-features { - grid-template-columns: repeat(2, 1fr) !important; - gap: 24px !important; - } - - .lp-works { - padding: 56px 32px !important; - } - - .lp-card-grid { - grid-template-columns: 1fr !important; - } - - .lp-calc-grid { - grid-template-columns: 1fr !important; - gap: 28px !important; - } - - /* full-width span no longer needed in 1-col */ - .lp-card-grid > [style*="gridColumn"] { - grid-column: auto !important; - } - - .lp-cta { - padding: 56px 32px !important; - } - - .lp-footer { - padding: 24px 28px !important; - gap: 16px !important; - flex-wrap: wrap !important; - justify-content: center !important; - } -} - -/* ── Mobile (≤ 600px) ─────────────────────────────────────────── */ -@media (max-width: 600px) { - .lp-nav { - padding: 14px 20px !important; - } - - /* Hide the GitHub nav button on small screens to save space */ - .lp-nav-github { - display: none !important; - } - - .lp-hero { - padding: 48px 20px 40px !important; - } - - .lp-hero-inner { - padding: 48px 20px 40px !important; - } - - .lp-proof-bar { - padding: 10px 20px !important; - gap: 14px !important; - } - - .lp-section { - padding: 48px 20px !important; - } - - .lp-feature-strip { - padding: 44px 20px !important; - } - - .lp-features { - grid-template-columns: 1fr !important; - } - - .lp-works { - padding: 48px 20px !important; - } - - .lp-install-step { - flex-direction: column !important; - gap: 12px !important; - } - - .lp-install-num { - font-size: 36px !important; - width: auto !important; - } - - .lp-pricing-card { - padding: 32px 24px !important; - } - - .lp-cta { - padding: 48px 20px !important; - } - - .lp-faq-row { - grid-template-columns: 1fr !important; - gap: 12px !important; - } - - .lp-compare-wrap { - padding: 48px 20px !important; - } - - .lp-footer { - padding: 20px !important; - flex-direction: column !important; - align-items: center !important; - text-align: center !important; - gap: 12px !important; - } -} diff --git a/web/src/pages/LandingPage.tsx b/web/src/pages/LandingPage.tsx deleted file mode 100644 index c4ec500..0000000 --- a/web/src/pages/LandingPage.tsx +++ /dev/null @@ -1,1043 +0,0 @@ -import { useState } from 'react' -import { Link } from 'react-router-dom' -import './LandingPage.css' -import LogoMark from '../components/LogoMark' -import PageNav from '../components/PageNav' - -// ── Feature icons ───────────────────────────────────────────────── -function IconMetrics({ color = '#C23B22' }: { color?: string }) { - return ( - - - - - - - ) -} - -function IconCloud({ color = '#C23B22' }: { color?: string }) { - return ( - - - - ) -} - -function IconExport({ color = '#C23B22' }: { color?: string }) { - return ( - - - - - - - - ) -} - -function IconServer({ color = '#C23B22' }: { color?: string }) { - return ( - - - - - - - - - ) -} - -function GitHubIcon({ size = 18, color = 'currentColor' }: { size?: number; color?: string }) { - return ( - - - - ) -} - -// ── Moon / Sun icons for dark-mode toggle ──────────────────────── -interface Props { - onEnter: () => void - dark: boolean - onToggleDark: () => void -} - -const S = { - // Layout - page: { - background: 'var(--lp-bg)', - minHeight: '100vh', - fontFamily: "'Lato', system-ui, sans-serif", - color: 'var(--lp-text)', - } as React.CSSProperties, - - // Nav - nav: { - display: 'flex', - justifyContent: 'space-between', - alignItems: 'center', - padding: '20px 60px', - borderBottom: '1px solid var(--lp-border)', - background: 'var(--lp-bg)', - } as React.CSSProperties, - - navLogo: { - fontFamily: "'Bebas Neue', Impact, sans-serif", - fontSize: 22, - letterSpacing: 4, - color: 'var(--lp-text)', - display: 'flex', - alignItems: 'center', - gap: 8, - textDecoration: 'none', - } as React.CSSProperties, - - navBtn: { - background: 'none', - border: '1.5px solid var(--lp-text)', - color: 'var(--lp-text)', - padding: '8px 22px', - fontFamily: "'Bebas Neue', Impact, sans-serif", - fontSize: 14, - letterSpacing: 2, - cursor: 'pointer', - transition: 'background .15s, color .15s, border-color .15s', - } as React.CSSProperties, - - // Hero - hero: { - borderBottom: '1px solid var(--lp-border)', - } as React.CSSProperties, - - heroInner: { - maxWidth: 1100, - margin: '0 auto', - padding: '80px 60px', - display: 'flex', - alignItems: 'center', - gap: 72, - } as React.CSSProperties, - - heroLeft: { - flex: 1, - minWidth: 0, - } as React.CSSProperties, - - heroRight: { - flex: '0 0 320px', - background: '#1A0F02', - border: '1px solid #3a2510', - borderLeft: '3px solid #B8860B', - padding: '24px 28px', - fontFamily: "'Courier New', monospace", - fontSize: 13, - color: '#E8C458', - lineHeight: 1.8, - } as React.CSSProperties, - - eyebrow: { - display: 'inline-flex', - alignItems: 'center', - gap: 8, - fontFamily: "'Bebas Neue', Impact, sans-serif", - fontSize: 11, - letterSpacing: 4, - color: '#9A7B5A', - border: '1px solid var(--lp-border)', - padding: '5px 14px', - marginBottom: 24, - } as React.CSSProperties, - - heroTitle: { - fontFamily: "'Playfair Display', Georgia, serif", - fontSize: 'clamp(36px, 4.5vw, 56px)', - fontWeight: 900, - lineHeight: 1.1, - color: 'var(--lp-text)', - marginBottom: 20, - letterSpacing: '-0.5px', - } as React.CSSProperties, - - heroSub: { - fontSize: 16, - color: 'var(--lp-text-mid)', - lineHeight: 1.75, - maxWidth: 440, - margin: '0 0 32px', - fontWeight: 400, - } as React.CSSProperties, - - heroBtn: { - display: 'inline-block', - background: '#C23B22', - color: '#FBF0DC', - border: 'none', - padding: '13px 38px', - fontFamily: "'Bebas Neue', Impact, sans-serif", - fontSize: 15, - letterSpacing: 3, - cursor: 'pointer', - transition: 'background .15s, transform .08s', - textDecoration: 'none', - } as React.CSSProperties, - - // Divider - divider: { - display: 'flex', - alignItems: 'center', - gap: 16, - maxWidth: 700, - margin: '0 auto', - padding: '0 60px', - } as React.CSSProperties, - - dividerLine: { - flex: 1, - height: 1, - background: 'var(--lp-border)', - } as React.CSSProperties, - - dividerText: { - fontFamily: "'Bebas Neue', Impact, sans-serif", - fontSize: 13, - letterSpacing: 3, - color: 'var(--lp-text-light)', - whiteSpace: 'nowrap' as const, - }, - - // Sections - section: { - maxWidth: 900, - margin: '0 auto', - padding: '72px 60px', - } as React.CSSProperties, - - sectionTitle: { - fontFamily: "'Playfair Display', Georgia, serif", - fontSize: 32, - fontWeight: 700, - color: 'var(--lp-text)', - textAlign: 'center' as const, - marginBottom: 8, - }, - - sectionSub: { - textAlign: 'center' as const, - color: '#9A7B5A', - marginBottom: 52, - fontWeight: 400, - fontSize: 17, - }, - - steps: { - display: 'grid', - gridTemplateColumns: 'repeat(3, 1fr)', - gap: 28, - } as React.CSSProperties, - - step: { - background: 'var(--lp-paper)', - border: '1px solid var(--lp-border)', - padding: '28px 24px', - boxShadow: '3px 3px 0 rgba(92,58,30,.12)', - position: 'relative' as const, - }, - - stepNum: { - fontFamily: "'Bebas Neue', Impact, sans-serif", - fontSize: 48, - color: 'var(--lp-border)', - lineHeight: 1, - marginBottom: 12, - } as React.CSSProperties, - - stepTitle: { - fontFamily: "'Playfair Display', Georgia, serif", - fontWeight: 700, - fontSize: 18, - marginBottom: 10, - color: 'var(--lp-text)', - } as React.CSSProperties, - - stepText: { - fontSize: 15, - color: 'var(--lp-text-mid)', - lineHeight: 1.75, - fontWeight: 400, - } as React.CSSProperties, - - // Features strip - featureStrip: { - background: 'var(--lp-dark-strip)', - padding: '64px 60px', - } as React.CSSProperties, - - features: { - maxWidth: 900, - margin: '0 auto', - display: 'grid', - gridTemplateColumns: 'repeat(4, 1fr)', - gap: 32, - } as React.CSSProperties, - - feature: { - textAlign: 'left' as const, - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - } as React.CSSProperties, - - featureIcon: { - fontSize: 28, - marginBottom: 12, - } as React.CSSProperties, - - featureTitle: { - fontFamily: "'Bebas Neue', Impact, sans-serif", - fontSize: 17, - letterSpacing: 2, - color: '#FBF0DC', - marginBottom: 10, - } as React.CSSProperties, - - featureText: { - fontSize: 14, - color: 'rgba(251,240,220,.65)', - lineHeight: 1.75, - fontWeight: 400, - } as React.CSSProperties, - - // Code block - codeBlock: { - background: '#1A0F02', - border: '1px solid #3a2510', - borderLeft: '3px solid #B8860B', - padding: '20px 24px', - fontFamily: "'Courier New', monospace", - fontSize: 13, - color: '#D4A82A', - lineHeight: 2, - marginTop: 20, - overflowX: 'auto' as const, - }, - - // CTA section - ctaSection: { - background: 'var(--lp-paper-alt)', - borderTop: '1px solid var(--lp-border)', - borderBottom: '1px solid var(--lp-border)', - padding: '72px 60px', - textAlign: 'center' as const, - } as React.CSSProperties, - - // Footer - footer: { - padding: '28px 60px', - borderTop: '1px solid var(--lp-border)', - display: 'flex', - justifyContent: 'space-between', - alignItems: 'center', - fontFamily: "'Bebas Neue', Impact, sans-serif", - fontSize: 12, - letterSpacing: 2, - color: 'var(--lp-text-light)', - } as React.CSSProperties, -} - -function CITabs() { - const [active, setActive] = useState('github') - - const tabs = [ - { id: 'github', label: 'GitHub Actions' }, - { id: 'gitlab', label: 'GitLab CI' }, - { id: 'circleci', label: 'CircleCI' }, - { id: 'bitbucket', label: 'Bitbucket' }, - { id: 'jenkins', label: 'Jenkins' }, - ] - - const INSTALL = `curl -fsSL "https://github.com/gbudjeakp/run-right/releases/latest/download/runright_linux_amd64" -o runright && chmod +x runright` - - const snippets: Record = { - github: { - label: '.github/workflows/ci.yml', - code: -`- uses: gbudjeakp/run-right@v1 - with: - run: make build - export: file,http - http-url: \${{ vars.RUNRIGHT_URL }} - env: - RUNRIGHT_API_KEY: \${{ secrets.RUNRIGHT_API_KEY }}`, - }, - gitlab: { - label: '.gitlab-ci.yml', - code: -`build: - before_script: - - ${INSTALL} - - ./runright monitor --export http --http-url "$RUNRIGHT_URL" & - - echo $! > .runright.pid - script: - - make build - after_script: - - kill $(cat .runright.pid) 2>/dev/null || true`, - }, - circleci: { - label: '.circleci/config.yml', - code: -`jobs: - build: - docker: - - image: cimg/base:stable - steps: - - checkout - - run: - name: Install RunRight - command: ${INSTALL} - - run: - name: Build - command: | - ./runright monitor --export http --http-url "$RUNRIGHT_URL" & - echo $! > .runright.pid - make build - kill $(cat .runright.pid) 2>/dev/null || true`, - }, - bitbucket: { - label: 'bitbucket-pipelines.yml', - code: -`pipelines: - default: - - step: - script: - - ${INSTALL} - - ./runright monitor --export http --http-url "$RUNRIGHT_URL" & - - echo $! > .runright.pid - - make build - - kill $(cat .runright.pid) 2>/dev/null || true`, - }, - jenkins: { - label: 'Jenkinsfile', - code: -`pipeline { - agent any - environment { - RUNRIGHT_URL = credentials('runright-url') - RUNRIGHT_API_KEY = credentials('runright-api-key') - } - stages { - stage('Build') { - steps { - sh """ - ${INSTALL} - ./runright monitor --export http --http-url \\$RUNRIGHT_URL & - echo \\$! > .runright.pid - make build - kill \\$(cat .runright.pid) 2>/dev/null || true - """ - } - } - } -}`, - }, - } - - const current = snippets[active] - - return ( -
- {/* Tab bar */} -
- {tabs.map(t => ( - - ))} -
- - {/* Code block */} - -
- ) -} - - -function CopyBlock({ label, code }: { label: string; code: string }) { - const [copied, setCopied] = useState(false) - - function copy() { - navigator.clipboard.writeText(code).then(() => { - setCopied(true) - setTimeout(() => setCopied(false), 2000) - }) - } - - return ( -
-
- {label} - -
-
{code}
-
- ) -} - -// ── Savings Calculator ──────────────────────────────────────────── -const CALC_MACHINES = [ - { id: 'gh4', label: 'GitHub Actions — ubuntu-latest (4 vCPU)', pricePerMin: 0.008, savingsPct: 0.20 }, - { id: 'gh8', label: 'GitHub Actions — 8-core runner', pricePerMin: 0.016, savingsPct: 0.38 }, - { id: 'gh16', label: 'GitHub Actions — 16-core runner', pricePerMin: 0.064, savingsPct: 0.52 }, - { id: 'c5xl', label: 'Self-hosted EC2 c5.xlarge (4c / 8 GB)', pricePerMin: 0.00283, savingsPct: 0.42 }, - { id: 'c52x', label: 'Self-hosted EC2 c5.2xlarge (8c / 16 GB)', pricePerMin: 0.00567, savingsPct: 0.52 }, - { id: 'm5xl', label: 'Self-hosted EC2 m5.xlarge (4c / 16 GB)', pricePerMin: 0.00320, savingsPct: 0.36 }, - { id: 'm52x', label: 'Self-hosted EC2 m5.2xlarge (8c / 32 GB)', pricePerMin: 0.00640, savingsPct: 0.50 }, - { id: 'n2s2', label: 'Self-hosted GCP n2-standard-2 (2c / 8 GB)', pricePerMin: 0.00163, savingsPct: 0.30 }, - { id: 'n2s4', label: 'Self-hosted GCP n2-standard-4 (4c / 16 GB)', pricePerMin: 0.00325, savingsPct: 0.40 }, - { id: 'n2s8', label: 'Self-hosted GCP n2-standard-8 (8c / 32 GB)', pricePerMin: 0.00651, savingsPct: 0.50 }, -] - -function SavingsCalc() { - const [machineIdx, setMachineIdx] = useState(0) - const [duration, setDuration] = useState(12) - const [jobsPerDay, setJobsPerDay] = useState(80) - - const m = CALC_MACHINES[machineIdx] - const currentMonthly = m.pricePerMin * duration * jobsPerDay * 30 - const savings = currentMonthly * m.savingsPct - const recommended = currentMonthly - savings - - const fmt = (n: number) => - n >= 1000 ? `$${(n / 1000).toFixed(1)}k` : `$${n.toFixed(0)}` - - return ( -
-
- -

- SAVINGS CALCULATOR -

-

- How much could you save? -

-

- Based on typical CI job utilisation (p95 CPU ~30%, p95 RAM ~40%). -

- -
- - {/* Controls */} -
- -
- - -
- -
- - setDuration(Number(e.target.value))} - style={{ width: '100%', accentColor: '#C23B22', cursor: 'pointer' }} /> -
- 1 min60 min -
-
- -
- - setJobsPerDay(Number(e.target.value))} - style={{ width: '100%', accentColor: '#C23B22', cursor: 'pointer' }} /> -
- 1500 -
-
- -
- - {/* Output */} -
- -
-
-

CURRENT / MO

-

{fmt(currentMonthly)}

-
-
-

SAVINGS / MO

-

{fmt(savings)}

-
-
- -
-

AFTER RUNRIGHT / MO

-

{fmt(recommended)}

-

~{Math.round(m.savingsPct * 100)}% reduction on right-sized machines

-
- -
- Estimates use p95 usage at ~30% CPU / ~40% RAM headroom. Actual savings vary by workload. -
- -
-
-
-
- ) -} - -export default function LandingPage({ onEnter, dark, onToggleDark }: Props) { - return ( -
- - {/* Nav */} - - - {/* Hero */} -
-
- - {/* Left — copy */} -
-

OPEN SOURCE · FREE · AWS & GCP

-

- Stop guessing at
- CI machine sizes. -

-

- RunRight monitors every CI job and maps your actual p95 usage to the - right AWS or GCP instance type — automatically, on every run. -

- { (e.currentTarget as HTMLElement).style.background = '#9B2D17'; (e.currentTarget as HTMLElement).style.transform = 'translateY(-1px)' }} - onMouseLeave={e => { (e.currentTarget as HTMLElement).style.background = '#C23B22'; (e.currentTarget as HTMLElement).style.transform = 'none' }} - > - Install → - -
- - {/* Right — recommendation visual */} -
-
- RUNRIGHT · build-and-test -
-
- p95 cpu - 1.2 cores -
-
- p95 ram - 1.8 GB -
-
-
CURRENT
-
c5.xlarge
-
4 vCPU · 8 GB · $0.0194/min
-
-
-
RECOMMENDED ↓ 58%
-
t4g.small
-
2 vCPU · 2 GB · $0.0042/min
-
-
MONTHLY SAVINGS
-
~$18.40
-
- -
-
- - {/* Social proof bar */} -
- {['160+ AWS instances', '60+ GCP instances', 'Open Source', 'Self-Hosted'].map(s => ( - - ❖ {s} - - ))} -
- - {/* Divider ornament */} -
-
- ✦ HOW IT WORKS ✦ -
-
- - {/* How it works */} -
-

HOW IT WORKS

-

Three steps. No code changes.

-
-
-
01
-
Monitor Every Run
-

- Add one line to your GitHub Action. RunRight samples CPU, memory, - disk I/O, and threads throughout your build and stores a snapshot - for every run, automatically. -

-
-
-
02
-
Build a History
-

- The dashboard accumulates job history over time. As your build grows - or shrinks, the data reflects it. No one-off audits, no spreadsheets. - Everything is already there. -

-
-
-
03
-
Get Accurate Recommendations
-

- RunRight scores every instance in the AWS and GCP catalog against - your real workload history and ranks options by cost, headroom, and fit. - Pick the right machine with confidence. -

-
-
-
- - {/* Features strip */} -
-
- {[ - { Icon: IconMetrics, title: 'Real Metrics', text: 'CPU, memory, disk I/O and threads sampled live, not guessed.' }, - { Icon: IconCloud, title: 'AWS + GCP', text: 'Hundreds of instance types, kept fresh automatically.' }, - { Icon: IconExport, title: 'Any Exporter', text: 'File, OTLP, Prometheus, HTTP. Plug into your existing stack.' }, - { Icon: IconServer, title: 'Self-hosted', text: 'Your data stays on your infra. No third-party SaaS.' }, - ].map(f => ( -
-
-
{f.title}
-

{f.text}

-
- ))} -
-
- - {/* Works Everywhere */} -
-
-

Works Everywhere You Run CI

-

Bare VMs, containers, K8s pods, DinD — detection adapts to the environment automatically.

-
- -
-
-
EC2 / GCP VMs
- ZERO CONFIG -
-

- Reads vCPU count and total memory from the OS, then matches against the catalog within a 2 GiB tolerance. Works on GitHub-hosted runners, self-hosted EC2, and GCP VMs. -

-
- -
-
-
Kubernetes + DinD Pods
- CONTAINER-AWARE -
-

- Reads cgroup v2 (cpu.max, memory.max) or cgroup v1 equivalents. Reflects your pod's actual CPU quota and memory limit, not the underlying node's resources. -

-
{`# K8s Downward API override (optional)
-env:
-  - name: RUNRIGHT_VCPUS
-    valueFrom:
-      resourceFieldRef:
-        resource: limits.cpu
-  - name: RUNRIGHT_MEMORY_GIB
-    valueFrom:
-      resourceFieldRef:
-        resource: limits.memory
-        divisor: 1Gi`}
-
- -
-
-
Any Environment
- MANUAL OVERRIDE -
-

- Skip auto-detection with two env vars. Works for Azure VMs, ARM instances, on-prem, or any environment where OS-level resource counts are unreliable. -

-
{`RUNRIGHT_VCPUS=4\nRUNRIGHT_MEMORY_GIB=16`}
-
- -
-
-
- - {/* CI integrations */} -
-

Works in any CI

-

- Native GitHub Action or a two-line shell snippet. Pick whichever fits your stack. -

- - - {/* Install section */} -
-

SELF-HOST THE DASHBOARD

-

Install in minutes.

-

A Go binary + a Postgres database. One Docker Compose file brings it all up.

- -
- - {/* Step 01 */} -
-
01
-
-
Generate an API key
-

RunRight authenticates the agent to the backend with a single shared secret. Generate one now and keep it in your CI secrets.

- -
-
- - {/* Step 02 */} -
-
02
-
-
Start everything with Docker
-

One command starts the backend, dashboard, and Postgres. The dashboard is available at localhost:3000 once it's up.

- -
-
- - {/* Step 03 */} -
-
03
-
-
Add the step to your CI workflow
-

Drop the RunRight step into any job above. It runs as a background process, samples every few seconds, and posts a summary when the job finishes. Your existing steps are untouched.

- -
-
- -
-
-
- - {/* Compare teaser */} -
-

- How does RunRight compare to Datadog, Grafana, Sentry, and AWS Cost Tools? -

- - See the full comparison → - -
- - {/* Savings Calculator */} - - - {/* Free Forever / Pricing */} -
-

PRICING

-
It's free.
-

No seats, no usage caps, no SaaS. Self-hosted on your infrastructure.

- - See full pricing → - -
- - {/* FAQ */} -
-

Quick answers

-
- {[ - { - q: 'Does RunRight slow down my builds?', - a: 'No. It polls /proc every 5s as a background sidecar. Under 5 MB RSS, under 0.1% CPU on the sampler. Your build time is not affected.', - }, - { - q: 'Do I need to change my application code?', - a: 'No. RunRight wraps your existing CI step. No SDK, no annotations, no config file.', - }, - { - q: 'I already use Datadog. Do I need the RunRight backend?', - a: 'No. Use --export otlp with OTEL_EXPORTER_OTLP_ENDPOINT pointing at your existing collector. The backend and Postgres are only needed for the self-hosted RunRight dashboard.', - }, - ].map((item, i, arr) => ( -
-
{item.q}
-

{item.a}

-
- ))} -
-
- - {/* CTA */} -
-

- READY TO START SAVING? -

-

- Right-size your CI today. -

- -
- - {/* Footer */} - -
- ) -} diff --git a/web/src/pages/PricingPage.css b/web/src/pages/PricingPage.css deleted file mode 100644 index 7a871e1..0000000 --- a/web/src/pages/PricingPage.css +++ /dev/null @@ -1,254 +0,0 @@ -/* ── Root ─────────────────────────────────────────────────────────────────── */ -.pp-root { - display: flex; - flex-direction: column; - min-height: 100vh; - background: var(--lp-bg); - color: var(--lp-text); - font-family: 'Lato', system-ui, sans-serif; -} - -/* ── Nav ──────────────────────────────────────────────────────────────────── */ -.pp-nav { - position: sticky; - top: 0; - z-index: 50; - display: flex; - justify-content: space-between; - align-items: center; - padding: 0 48px; - height: 56px; - border-bottom: 1px solid var(--lp-border); - background: var(--lp-bg); -} - -.pp-logo { - font-family: 'Bebas Neue', Impact, sans-serif; - font-size: 18px; - letter-spacing: 4px; - color: var(--lp-text); - text-decoration: none; - display: flex; - align-items: center; - gap: 8px; -} - -.pp-nav-right { - display: flex; - align-items: center; - gap: 24px; -} - -.pp-nav-link { - font-family: 'Bebas Neue', Impact, sans-serif; - font-size: 12px; - letter-spacing: 2px; - color: var(--lp-text-mid); - text-decoration: none; - transition: color .12s; -} -.pp-nav-link:hover { color: var(--lp-text); } - -.pp-nav-link.active { color: var(--lp-text); } - -.pp-signin-btn { - font-family: 'Bebas Neue', Impact, sans-serif; - font-size: 12px; - letter-spacing: 2px; - color: #FBF0DC; - background: #2C1A0E; - border: 1.5px solid #2C1A0E; - padding: 7px 18px; - text-decoration: none; - cursor: pointer; - transition: background .15s; -} -.pp-signin-btn:hover { background: #C23B22; border-color: #C23B22; } - -/* ── Hero ─────────────────────────────────────────────────────────────────── */ -.pp-hero { - text-align: center; - padding: 32px 48px 20px; - border-bottom: 1px solid var(--lp-border); -} - -.pp-eyebrow { - font-family: 'Bebas Neue', Impact, sans-serif; - font-size: 12px; - letter-spacing: 5px; - color: #9A7B5A; - margin: 0 0 10px; -} - -.pp-title { - font-family: 'Playfair Display', Georgia, serif; - font-size: clamp(26px, 3.5vw, 40px); - font-weight: 900; - color: var(--lp-text); - line-height: 1.05; - margin: 0 0 10px; - letter-spacing: -1px; -} - -.pp-sub { - font-size: 15px; - color: var(--lp-text-mid); - max-width: 520px; - margin: 0 auto; - line-height: 1.7; -} - -/* ── Content ──────────────────────────────────────────────────────────────── */ -.pp-content { - flex: 1; - max-width: 480px; - width: 100%; - margin: 24px auto; - padding: 0 24px; -} - -/* ── Card ─────────────────────────────────────────────────────────────────── */ -.pp-card { - border: 2px solid var(--lp-text); - background: var(--lp-paper); - padding: 32px 36px; - box-shadow: 6px 6px 0 rgba(44, 26, 14, .1); - margin-bottom: 32px; -} - -.pp-plan-label { - font-family: 'Bebas Neue', Impact, sans-serif; - font-size: 11px; - letter-spacing: 4px; - color: #9A7B5A; - margin-bottom: 8px; -} - -.pp-price { - font-family: 'Playfair Display', Georgia, serif; - font-size: 40px; - font-weight: 900; - color: var(--lp-text); - line-height: 1; - letter-spacing: -2px; - margin-bottom: 4px; -} - -.pp-price-sub { - font-size: 13px; - color: #9A7B5A; - margin-bottom: 16px; -} - -.pp-features { - list-style: none; - padding: 0; - margin: 0 0 20px; -} - -.pp-feature-item { - display: flex; - align-items: center; - gap: 14px; - font-size: 13.5px; - color: var(--lp-text); - padding: 7px 0; - border-bottom: 1px solid var(--lp-border); -} -.pp-feature-item:first-child { border-top: 1px solid var(--lp-border); } - -.pp-check { - color: #C23B22; - font-weight: 700; - font-size: 16px; - flex-shrink: 0; - width: 16px; - text-align: center; -} - -.pp-github-btn { - display: inline-flex; - align-items: center; - gap: 8px; - background: #2C1A0E; - color: #FBF0DC; - padding: 10px 24px; - font-family: 'Bebas Neue', Impact, sans-serif; - font-size: 14px; - letter-spacing: 2px; - text-decoration: none; - box-shadow: 3px 3px 0 rgba(44, 26, 14, .15); - transition: background .15s; -} -.pp-github-btn:hover { background: #C23B22; } - -/* ── OTLP note ────────────────────────────────────────────────────────────── */ -.pp-note { - background: var(--lp-paper); - border: 1px solid var(--lp-border); - border-left: 3px solid #2C1A0E; - padding: 24px 28px; -} - -.pp-note-label { - font-family: 'Bebas Neue', Impact, sans-serif; - font-size: 11px; - letter-spacing: 3px; - color: var(--lp-text); - margin-bottom: 10px; -} - -.pp-note-text { - font-size: 14px; - color: var(--lp-text-mid); - line-height: 1.85; - margin: 0 0 14px; -} - -.pp-note-text code { - font-family: 'Courier New', monospace; - font-size: 12px; - background: var(--lp-paper-alt); - padding: 1px 5px; - color: var(--lp-text); -} - -.pp-note-link { - font-family: 'Bebas Neue', Impact, sans-serif; - font-size: 12px; - letter-spacing: 1.5px; - color: var(--lp-text); - text-decoration: none; - transition: color .12s; -} -.pp-note-link:hover { color: #C23B22; } - -/* ── Footer ───────────────────────────────────────────────────────────────── */ -.pp-footer { - border-top: 1px solid var(--lp-border); - padding: 20px 48px; - display: flex; - justify-content: space-between; - align-items: center; - font-family: 'Bebas Neue', Impact, sans-serif; - font-size: 11px; - letter-spacing: 2px; - color: var(--lp-text-light); -} - -.pp-footer-link { - color: var(--lp-text-light); - text-decoration: none; - transition: color .12s; -} -.pp-footer-link:hover { color: var(--lp-text); } - -/* ── Responsive ───────────────────────────────────────────────────────────── */ -@media (max-width: 600px) { - .pp-nav { padding: 0 20px; } - .pp-hero { padding: 56px 24px 48px; } - .pp-content { margin: 40px auto; } - .pp-card { padding: 32px 24px; } - .pp-price { font-size: 64px; } - .pp-footer { padding: 16px 20px; flex-direction: column; gap: 10px; text-align: center; } -} diff --git a/web/src/pages/PricingPage.tsx b/web/src/pages/PricingPage.tsx deleted file mode 100644 index 2c936c9..0000000 --- a/web/src/pages/PricingPage.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import { Link } from 'react-router-dom' -import LogoMark from '../components/LogoMark' -import PageNav from '../components/PageNav' -import './LandingPage.css' -import './PricingPage.css' - -function GitHubIcon({ size = 16 }: { size?: number }) { - return ( - - - - ) -} - -export default function PricingPage({ dark, onToggleDark }: { dark: boolean; onToggleDark: () => void }) { - return ( -
- - {/* Nav */} - - - {/* Hero */} -
-

PRICING

-

It's free.

-

- No seats. No usage caps. No third-party SaaS. Runs entirely on your infrastructure. -

-
- - {/* Content */} -
- - {/* Pricing card */} -
-
SELF-HOSTED
-
$0
-
per month · per seat · per run
- -
    - {[ - 'Unlimited job runs and history', - 'Unlimited team members', - 'Full AWS + GCP catalog (160+ types)', - 'All CI platform integrations', - 'Your data stays on your servers', - 'Source available on GitHub', - ].map(item => ( -
  • - + - {item} -
  • - ))} -
- - - - Star on GitHub - -
- - {/* Install note */} -
-
Ready to get started?
-

- Install in under two minutes. Works with GitHub Actions, Jenkins, CircleCI, and any - CI platform that runs shell commands. -

- See install options → -
- -
- - {/* Footer */} - - -
- ) -}