From d3e648a81b279c375d235d3a6f3ffee93081bbef Mon Sep 17 00:00:00 2001
From: Rob Macrae
Date: Wed, 8 Jul 2026 22:45:16 +0100
Subject: [PATCH 1/3] feat(web): on-site Download page pulling the latest
GitHub release
Adds an on-brand /download page so users get the latest release notes +
installers without leaving orcabot.com:
- controlplane: public, edge-cached GET /releases/latest that proxies the
GitHub Releases API (User-Agent + cacheTtl so we never hit the API rate limit)
- frontend: /download page (react-markdown release notes, featured macOS
installer button auto-picking .dmg or .zip, all-downloads list) skinned to
the splash/header palette; "Download" nav link; a download CTA on the splash
Co-Authored-By: Claude Opus 4.8 (1M context)
Claude-Session: https://claude.ai/code/session_014t8Ukp4NPWtqJ55X261wMZ
---
controlplane/src/index.ts | 7 +
controlplane/src/releases/handler.ts | 86 ++++++++++
frontend/src/app/download/download.css | 210 +++++++++++++++++++++++++
frontend/src/app/download/page.tsx | 142 +++++++++++++++++
frontend/src/app/page.tsx | 21 +++
frontend/src/components/SiteHeader.tsx | 1 +
frontend/src/config/env.ts | 1 +
frontend/src/lib/api/releases.ts | 63 ++++++++
8 files changed, 531 insertions(+)
create mode 100644 controlplane/src/releases/handler.ts
create mode 100644 frontend/src/app/download/download.css
create mode 100644 frontend/src/app/download/page.tsx
create mode 100644 frontend/src/lib/api/releases.ts
diff --git a/controlplane/src/index.ts b/controlplane/src/index.ts
index 2a31ff99..09fa8860 100644
--- a/controlplane/src/index.ts
+++ b/controlplane/src/index.ts
@@ -52,6 +52,7 @@ import { createApiToken, listApiTokens, revokeApiToken } from './auth/api-token'
import { checkAndCacheSandbоxHealth, getCachedHealth } from './health/checker';
import { sendEmail, buildInterestThankYouEmail, buildInterestNotificationEmail, buildTemplateReviewEmail } from './email/resend';
import * as blog from './blog/handler';
+import * as releases from './releases/handler';
import { sandboxHeaders, sandboxUrl } from './sandbox/fetch';
// Export Durable Objects
@@ -837,6 +838,12 @@ async function handleRequest(request: Request, env: EnvWithBindings, ctx: Pick {
+ try {
+ const resp = await fetch(GITHUB_LATEST, {
+ headers: {
+ // GitHub rejects API requests without a User-Agent.
+ 'User-Agent': 'orcabot-controlplane',
+ Accept: 'application/vnd.github+json',
+ },
+ // Cache at the edge so repeated page loads don't re-hit GitHub.
+ cf: { cacheTtl: 300, cacheEverything: true },
+ });
+
+ if (!resp.ok) {
+ return Response.json(
+ { error: `GitHub returned ${resp.status}` },
+ { status: 502, headers: { 'Cache-Control': 'public, max-age=60' } }
+ );
+ }
+
+ const gh = (await resp.json()) as {
+ tag_name?: string;
+ name?: string;
+ body?: string;
+ html_url?: string;
+ published_at?: string;
+ assets?: Array<{
+ name: string;
+ size: number;
+ browser_download_url: string;
+ content_type: string;
+ download_count: number;
+ }>;
+ };
+
+ const release: LatestRelease = {
+ version: (gh.tag_name || '').replace(/^v/, ''),
+ name: gh.name || gh.tag_name || '',
+ notes: gh.body || '',
+ htmlUrl: gh.html_url || `https://github.com/${RELEASES_REPO}/releases/latest`,
+ publishedAt: gh.published_at || '',
+ assets: (gh.assets || []).map((a) => ({
+ name: a.name,
+ size: a.size,
+ downloadUrl: a.browser_download_url,
+ contentType: a.content_type,
+ downloadCount: a.download_count,
+ })),
+ };
+
+ return Response.json(release, {
+ headers: { 'Cache-Control': 'public, max-age=300' },
+ });
+ } catch {
+ return Response.json(
+ { error: 'Failed to fetch latest release' },
+ { status: 502 }
+ );
+ }
+}
diff --git a/frontend/src/app/download/download.css b/frontend/src/app/download/download.css
new file mode 100644
index 00000000..0cd22a56
--- /dev/null
+++ b/frontend/src/app/download/download.css
@@ -0,0 +1,210 @@
+/* Copyright 2026 Rob Macrae. All rights reserved. */
+/* On-brand skin for the /download page (mirrors the splash/header palette). */
+
+.dl-page {
+ min-height: 100vh;
+ background:
+ radial-gradient(1200px 600px at 50% -10%, rgba(59, 130, 246, 0.15), transparent 60%),
+ linear-gradient(180deg, #06101f 0%, #081426 100%);
+ color: #e6eefc;
+}
+
+.dl-main {
+ max-width: 46rem;
+ margin: 0 auto;
+ padding: 3rem 1.5rem 5rem;
+}
+
+.dl-hero {
+ text-align: center;
+ padding: 2rem 0 1rem;
+}
+
+.dl-orca {
+ width: 84px;
+ height: 84px;
+ object-fit: contain;
+ margin: 0 auto 1rem;
+ display: block;
+}
+
+.dl-hero h1 {
+ font-size: 2.25rem;
+ font-weight: 800;
+ margin: 0 0 0.5rem;
+ color: #fff;
+}
+
+.dl-sub {
+ color: rgba(230, 238, 252, 0.7);
+ margin: 0 0 1.75rem;
+ font-size: 1.05rem;
+}
+
+.dl-cta {
+ display: flex;
+ justify-content: center;
+ margin-bottom: 1rem;
+}
+
+.dl-btn {
+ display: inline-flex;
+ flex-direction: column;
+ align-items: center;
+ text-decoration: none;
+ font-weight: 700;
+ border-radius: 12px;
+ padding: 14px 28px;
+ line-height: 1.2;
+}
+
+.dl-btn-primary {
+ color: #06101f;
+ background: linear-gradient(180deg, #cfe0ff, #8fb4ff);
+ box-shadow:
+ 0 1px 0 rgba(255, 255, 255, 0.4) inset,
+ 0 10px 30px rgba(59, 130, 246, 0.35);
+ font-size: 1.05rem;
+ transition: filter 0.15s ease;
+}
+
+.dl-btn-primary:hover {
+ filter: brightness(1.05);
+}
+
+.dl-btn-sub {
+ font-weight: 600;
+ font-size: 0.8rem;
+ opacity: 0.75;
+ margin-top: 2px;
+}
+
+.dl-meta {
+ color: rgba(230, 238, 252, 0.6);
+ font-size: 0.9rem;
+}
+
+.dl-meta a {
+ color: #9fc0ff;
+ text-decoration: none;
+}
+
+.dl-meta a:hover {
+ text-decoration: underline;
+}
+
+.dl-version {
+ font-weight: 700;
+ color: #cfe0ff;
+}
+
+.dl-card {
+ margin-top: 2rem;
+ padding: 1.5rem 1.75rem;
+ background: rgba(255, 255, 255, 0.04);
+ border: 1px solid rgba(255, 255, 255, 0.08);
+ border-radius: 16px;
+ backdrop-filter: blur(12px);
+ -webkit-backdrop-filter: blur(12px);
+}
+
+.dl-card h2 {
+ font-size: 1.15rem;
+ font-weight: 700;
+ margin: 0 0 1rem;
+ color: #fff;
+}
+
+.dl-notes {
+ line-height: 1.7;
+ color: rgba(230, 238, 252, 0.85);
+ word-wrap: break-word;
+}
+
+.dl-notes h1,
+.dl-notes h2,
+.dl-notes h3 {
+ color: #fff;
+ margin: 1.25rem 0 0.5rem;
+ font-size: 1.05rem;
+}
+
+.dl-notes ul,
+.dl-notes ol {
+ padding-left: 1.25rem;
+}
+
+.dl-notes li {
+ margin: 0.25rem 0;
+}
+
+.dl-notes a {
+ color: #9fc0ff;
+}
+
+.dl-notes code {
+ background: rgba(255, 255, 255, 0.08);
+ padding: 0.1rem 0.35rem;
+ border-radius: 5px;
+ font-size: 0.9em;
+}
+
+.dl-notes pre {
+ background: rgba(0, 0, 0, 0.3);
+ padding: 1rem;
+ border-radius: 10px;
+ overflow-x: auto;
+}
+
+.dl-notes pre code {
+ background: none;
+ padding: 0;
+}
+
+.dl-assets {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+}
+
+.dl-assets li {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 0.6rem 0;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.06);
+ gap: 1rem;
+}
+
+.dl-assets li:last-child {
+ border-bottom: none;
+}
+
+.dl-assets a {
+ color: #cfe0ff;
+ text-decoration: none;
+ word-break: break-all;
+}
+
+.dl-assets a:hover {
+ text-decoration: underline;
+}
+
+.dl-muted {
+ color: rgba(230, 238, 252, 0.5);
+ font-size: 0.85rem;
+ white-space: nowrap;
+}
+
+.dl-error {
+ color: #ffb4b4;
+}
+
+.dl-error a {
+ color: #9fc0ff;
+}
+
+.dl-foot {
+ text-align: center;
+ margin-top: 2.5rem;
+}
diff --git a/frontend/src/app/download/page.tsx b/frontend/src/app/download/page.tsx
new file mode 100644
index 00000000..60f70a3f
--- /dev/null
+++ b/frontend/src/app/download/page.tsx
@@ -0,0 +1,142 @@
+// Copyright 2026 Rob Macrae. All rights reserved.
+// SPDX-License-Identifier: LicenseRef-Proprietary
+
+// REVISION: download-page-v1
+"use client";
+
+import "./download.css";
+import * as React from "react";
+import Markdown from "react-markdown";
+import { SiteHeader } from "@/components/SiteHeader";
+import {
+ getLatestRelease,
+ pickMacInstaller,
+ formatBytes,
+ type LatestRelease,
+} from "@/lib/api/releases";
+
+const GITHUB_RELEASES = "https://github.com/Hyper-Int/OrcaBot/releases/latest";
+
+export default function DownloadPage() {
+ const [release, setRelease] = React.useState(null);
+ const [error, setError] = React.useState(null);
+ const [loading, setLoading] = React.useState(true);
+
+ React.useEffect(() => {
+ let cancelled = false;
+ getLatestRelease()
+ .then((r) => {
+ if (!cancelled) {
+ setRelease(r);
+ setLoading(false);
+ }
+ })
+ .catch((e) => {
+ if (!cancelled) {
+ setError(e instanceof Error ? e.message : "Failed to load release");
+ setLoading(false);
+ }
+ });
+ return () => {
+ cancelled = true;
+ };
+ }, []);
+
+ const primary = release ? pickMacInstaller(release.assets) : null;
+ // User-facing installers only — hide updater artifacts (.tar.gz, .sig) and the manifest.
+ const installers = release
+ ? release.assets.filter(
+ (a) =>
+ a.name !== "latest.json" &&
+ !a.name.endsWith(".sig") &&
+ !a.name.endsWith(".tar.gz")
+ )
+ : [];
+
+ return (
+
+
+
+
+
+ Download Orcabot
+
+ Run Claude Code, Codex, and a full sandboxed stack locally — no setup.
+
+
+ {loading && Loading the latest release…
}
+
+ {error && (
+
+ Couldn’t load the release.{" "}
+ Download on GitHub →
+
+ )}
+
+ {release && (
+ <>
+
+
+
v{release.version}
+ {release.publishedAt && (
+
+ {" · "}
+ {new Date(release.publishedAt).toLocaleDateString(undefined, {
+ year: "numeric",
+ month: "short",
+ day: "numeric",
+ })}
+
+ )}
+ {" · "}
+
+ View on GitHub ↗
+
+
+ >
+ )}
+
+
+ {release && release.notes && (
+
+ What’s new{release.name ? ` — ${release.name}` : ""}
+
+ {release.notes}
+
+
+ )}
+
+ {release && installers.length > 1 && (
+
+ All downloads
+
+ {installers.map((a) => (
+ -
+ {a.name}
+ {formatBytes(a.size)}
+
+ ))}
+
+
+ )}
+
+
+ macOS 13+ · Apple Silicon. Existing installs update automatically.
+
+
+
+ );
+}
diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx
index 0db8801c..b36142f1 100644
--- a/frontend/src/app/page.tsx
+++ b/frontend/src/app/page.tsx
@@ -623,6 +623,27 @@ export default function Home() {
Run AI agents in secure sandboxes. The sane and secure alternative to
open-source claws.
+
By continuing, you agree to our{" "}
Terms of Service and{" "}
diff --git a/frontend/src/components/SiteHeader.tsx b/frontend/src/components/SiteHeader.tsx
index 5d12b5bb..0545a0fd 100644
--- a/frontend/src/components/SiteHeader.tsx
+++ b/frontend/src/components/SiteHeader.tsx
@@ -12,6 +12,7 @@ import { useAuthStore } from "@/stores/auth-store";
const HOME_LINK: NavLink = { href: "/", label: "Home" };
const BASE_NAV_LINKS: NavLink[] = [
+ { href: "/download", label: "Download" },
{ href: "/docs", label: "Docs" },
{ href: "/blog", label: "Blog" },
{ href: "/terms", label: "Terms" },
diff --git a/frontend/src/config/env.ts b/frontend/src/config/env.ts
index 799a1da0..ce1a378f 100644
--- a/frontend/src/config/env.ts
+++ b/frontend/src/config/env.ts
@@ -125,6 +125,7 @@ export const env = {
export const API = {
cloudflare: {
base: CLOUDFLARE_API_URL,
+ releasesLatest: `${CLOUDFLARE_API_URL}/releases/latest`,
dashboards: `${CLOUDFLARE_API_URL}/dashboards`,
recipes: `${CLOUDFLARE_API_URL}/recipes`,
schedules: `${CLOUDFLARE_API_URL}/schedules`,
diff --git a/frontend/src/lib/api/releases.ts b/frontend/src/lib/api/releases.ts
new file mode 100644
index 00000000..4ca4e8dc
--- /dev/null
+++ b/frontend/src/lib/api/releases.ts
@@ -0,0 +1,63 @@
+// Copyright 2026 Rob Macrae. All rights reserved.
+// SPDX-License-Identifier: LicenseRef-Proprietary
+
+// Client for the control plane's /releases/latest endpoint (a public, cached
+// proxy of the GitHub Releases API) — powers the on-site /download page.
+
+import { API } from "@/config/env";
+
+export interface ReleaseAsset {
+ name: string;
+ size: number;
+ downloadUrl: string;
+ contentType: string;
+ downloadCount: number;
+}
+
+export interface LatestRelease {
+ version: string;
+ name: string;
+ notes: string;
+ htmlUrl: string;
+ publishedAt: string;
+ assets: ReleaseAsset[];
+}
+
+export async function getLatestRelease(): Promise {
+ const res = await fetch(API.cloudflare.releasesLatest, {
+ headers: { Accept: "application/json" },
+ });
+ if (!res.ok) {
+ throw new Error(`Failed to load latest release (${res.status})`);
+ }
+ return (await res.json()) as LatestRelease;
+}
+
+/** Human-readable file size, e.g. 1038066117 → "990 MB". */
+export function formatBytes(bytes: number): string {
+ if (!bytes || bytes < 0) return "";
+ const units = ["B", "KB", "MB", "GB"];
+ let n = bytes;
+ let i = 0;
+ while (n >= 1024 && i < units.length - 1) {
+ n /= 1024;
+ i++;
+ }
+ return `${n < 10 && i > 0 ? n.toFixed(1) : Math.round(n)} ${units[i]}`;
+}
+
+/**
+ * The primary macOS installer to feature on the download button. Prefers a
+ * `.dmg`, falls back to a `.zip` (salvaged builds ship a zip). Ignores the
+ * updater tarball (`.app.tar.gz`) and signature/manifest files.
+ */
+export function pickMacInstaller(assets: ReleaseAsset[]): ReleaseAsset | null {
+ const installable = assets.filter(
+ (a) => !a.name.endsWith(".tar.gz") && !a.name.endsWith(".sig") && a.name !== "latest.json"
+ );
+ return (
+ installable.find((a) => a.name.endsWith(".dmg")) ||
+ installable.find((a) => a.name.endsWith(".zip")) ||
+ null
+ );
+}
From 80c6ca4db97d99b34a2dae0bc4fa35034af8ba2d Mon Sep 17 00:00:00 2001
From: Rob Macrae
Date: Wed, 8 Jul 2026 23:35:00 +0100
Subject: [PATCH 2/3] fix(web): download page shows full asset list; minimalist
splash CTA
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- /download now lists every release asset (matching what GitHub shows) instead
of hiding all but the featured installer, plus a "Latest" badge — the goal
was parity with the GitHub release, just reskinned.
- Splash download CTA moved below the Terms/Privacy line and reduced to a small,
minimalist text link (was a boxed button shoehorned above it).
Co-Authored-By: Claude Opus 4.8 (1M context)
Claude-Session: https://claude.ai/code/session_014t8Ukp4NPWtqJ55X261wMZ
---
frontend/src/app/download/download.css | 15 ++++++++++++
frontend/src/app/download/page.tsx | 17 ++++---------
frontend/src/app/page.tsx | 34 ++++++++++----------------
3 files changed, 33 insertions(+), 33 deletions(-)
diff --git a/frontend/src/app/download/download.css b/frontend/src/app/download/download.css
index 0cd22a56..00ec9301 100644
--- a/frontend/src/app/download/download.css
+++ b/frontend/src/app/download/download.css
@@ -98,6 +98,21 @@
color: #cfe0ff;
}
+.dl-badge {
+ display: inline-block;
+ margin-left: 0.5rem;
+ padding: 1px 8px;
+ font-size: 0.7rem;
+ font-weight: 700;
+ letter-spacing: 0.02em;
+ text-transform: uppercase;
+ color: #7ee0a8;
+ background: rgba(46, 204, 113, 0.14);
+ border: 1px solid rgba(46, 204, 113, 0.35);
+ border-radius: 999px;
+ vertical-align: middle;
+}
+
.dl-card {
margin-top: 2rem;
padding: 1.5rem 1.75rem;
diff --git a/frontend/src/app/download/page.tsx b/frontend/src/app/download/page.tsx
index 60f70a3f..677f5a3f 100644
--- a/frontend/src/app/download/page.tsx
+++ b/frontend/src/app/download/page.tsx
@@ -43,15 +43,7 @@ export default function DownloadPage() {
}, []);
const primary = release ? pickMacInstaller(release.assets) : null;
- // User-facing installers only — hide updater artifacts (.tar.gz, .sig) and the manifest.
- const installers = release
- ? release.assets.filter(
- (a) =>
- a.name !== "latest.json" &&
- !a.name.endsWith(".sig") &&
- !a.name.endsWith(".tar.gz")
- )
- : [];
+ const assets = release ? release.assets : [];
return (
@@ -91,6 +83,7 @@ export default function DownloadPage() {
v{release.version}
+
Latest
{release.publishedAt && (
{" · "}
@@ -119,11 +112,11 @@ export default function DownloadPage() {
)}
- {release && installers.length > 1 && (
+ {release && assets.length > 0 && (