feat(cli): add fanout push strategy and improve website responsiveness

- Add --fanout flag to push command for server-to-server deployment
- Implement agent forwarding for efficient multi-node distribution
- Update landing page scene heights and section padding for mobile devices
This commit is contained in:
anonpenguin23 2026-03-28 15:27:54 +02:00
parent b9b6e19bb8
commit 7284cb4578
16 changed files with 167 additions and 83 deletions

View File

@ -5,6 +5,7 @@ import (
"os"
"path/filepath"
"sort"
"strings"
"github.com/DeBrosOfficial/network/pkg/cli"
"github.com/DeBrosOfficial/network/pkg/cli/noderesolver"
@ -17,6 +18,7 @@ var (
envFlag string
ipFlag string
userFlag string
fanoutFlag bool
)
// Cmd is the top-level "push" command — upload binary archive to nodes.
@ -25,14 +27,13 @@ var Cmd = &cobra.Command{
Short: "Push binary archive to your nodes",
Long: `Upload the pre-built binary archive to nodes and extract it.
Use --ip to push to a single node, or omit it to push to all nodes
in the active environment.
By default, uploads from your machine to each node sequentially.
Use --fanout to upload to one node, then fan out server-to-server (faster).
Examples:
orama push --ip 1.2.3.4 # Push to one node
orama push --ip 1.2.3.4 --user ubuntu # Push with specific SSH user
orama push --env devnet # Push to all devnet nodes
orama push --env devnet --ip 1.2.3.4 # Push to one devnet node`,
orama push --env devnet # Sequential push to all devnet nodes
orama push --env devnet --fanout # Fan out server-to-server (faster)`,
RunE: func(cmd *cobra.Command, args []string) error {
archivePath := findNewestArchive()
if archivePath == "" {
@ -47,7 +48,6 @@ Examples:
var nodes []inspector.Node
if ipFlag != "" {
// Single node push
user := userFlag
if user == "" {
user = "root"
@ -55,8 +55,8 @@ Examples:
vaultTarget := fmt.Sprintf("%s/%s", ipFlag, user)
env := envFlag
if env == "" {
active, err := cli.GetActiveEnvironment()
if err == nil {
active, _ := cli.GetActiveEnvironment()
if active != nil {
env = active.Name
}
}
@ -64,13 +64,9 @@ Examples:
vaultTarget = "sandbox/root"
}
nodes = []inspector.Node{{
Host: ipFlag,
User: user,
VaultTarget: vaultTarget,
Environment: env,
Host: ipFlag, User: user, VaultTarget: vaultTarget, Environment: env,
}}
} else {
// All nodes in environment
env := envFlag
if env == "" {
active, err := cli.GetActiveEnvironment()
@ -79,7 +75,6 @@ Examples:
}
env = active.Name
}
resolved, err := noderesolver.ResolveNodes(env)
if err != nil {
return fmt.Errorf("failed to resolve nodes: %w", err)
@ -97,7 +92,26 @@ Examples:
}
defer cleanup()
fmt.Printf("Pushing to %d node(s)...\n\n", len(nodes))
// Single node or default: upload sequentially
if len(nodes) == 1 || !fanoutFlag {
return pushDirect(nodes, archivePath)
}
// Multi-node with --fanout: use agent forwarding
return pushFanout(nodes, archivePath)
},
}
func init() {
Cmd.Flags().StringVar(&envFlag, "env", "", "Target environment (default: active)")
Cmd.Flags().StringVar(&ipFlag, "ip", "", "Push to a single node by IP")
Cmd.Flags().StringVar(&userFlag, "user", "", "SSH user (default: root)")
Cmd.Flags().BoolVar(&fanoutFlag, "fanout", false, "Upload to first node, then fan out server-to-server (faster)")
}
// pushDirect uploads the archive from local machine to each node sequentially.
func pushDirect(nodes []inspector.Node, archivePath string) error {
fmt.Printf("Pushing to %d node(s) (direct)...\n\n", len(nodes))
remotePath := "/tmp/" + filepath.Base(archivePath)
extractCmd := fmt.Sprintf("sudo bash -c 'mkdir -p /opt/orama && tar xzf %s -C /opt/orama && rm -f %s && /opt/orama/bin/orama version'", remotePath, remotePath)
@ -118,13 +132,62 @@ Examples:
fmt.Println("\nPush complete")
return nil
},
}
func init() {
Cmd.Flags().StringVar(&envFlag, "env", "", "Target environment (default: active)")
Cmd.Flags().StringVar(&ipFlag, "ip", "", "Push to a single node by IP")
Cmd.Flags().StringVar(&userFlag, "user", "", "SSH user (default: root)")
// pushFanout uploads the archive to the first node, then fans out server-to-server
// using SSH agent forwarding.
func pushFanout(nodes []inspector.Node, archivePath string) error {
fmt.Printf("Pushing to %d node(s) (fanout)...\n\n", len(nodes))
hub := nodes[0]
targets := nodes[1:]
remotePath := "/tmp/" + filepath.Base(archivePath)
extractCmd := fmt.Sprintf("mkdir -p /opt/orama && tar xzf %s -C /opt/orama && rm -f %s", remotePath, remotePath)
// Load SSH keys into the system ssh-agent for agent forwarding
fmt.Println(" Loading SSH keys into agent...")
if err := remotessh.LoadAgentKeys(nodes); err != nil {
fmt.Printf(" Warning: failed to load agent keys: %v\n", err)
fmt.Println(" Falling back to direct push...")
return pushDirect(nodes, archivePath)
}
// Upload archive to hub
fmt.Printf(" %s (hub): uploading...", hub.Host)
if err := remotessh.UploadFile(hub, archivePath, remotePath); err != nil {
return fmt.Errorf("failed to upload to hub %s: %w", hub.Host, err)
}
fmt.Printf(" extracting...")
if err := remotessh.RunSSHStreaming(hub, "sudo bash -c '"+extractCmd+"'"); err != nil {
return fmt.Errorf("failed to extract on hub %s: %w", hub.Host, err)
}
fmt.Println(" OK")
// Build the fanout command — hub SCPs to all targets in parallel
var fanoutParts []string
for _, t := range targets {
scpCmd := fmt.Sprintf(
"scp -o StrictHostKeyChecking=accept-new -o IdentitiesOnly=no %s %s@%s:%s && ssh -o StrictHostKeyChecking=accept-new %s@%s 'sudo bash -c \"%s\"' && echo '%s: done'",
remotePath, t.User, t.Host, remotePath,
t.User, t.Host, extractCmd,
t.Host,
)
fanoutParts = append(fanoutParts, "("+scpCmd+") &")
}
fanoutParts = append(fanoutParts, "wait", "echo 'Fanout complete'")
fanoutScript := strings.Join(fanoutParts, "\n")
fmt.Printf(" Fanning out to %d nodes from %s...\n", len(targets), hub.Host)
if err := remotessh.RunSSHStreaming(hub, "bash -c '"+fanoutScript+"'", remotessh.WithAgentForward()); err != nil {
fmt.Printf(" Fanout failed: %v\n", err)
fmt.Println(" Some nodes may not have been updated")
}
// Clean up archive on hub
remotessh.RunSSHStreaming(hub, "rm -f "+remotePath)
fmt.Println("\nPush complete")
return nil
}
func findNewestArchive() string {

View File

@ -254,7 +254,7 @@ function AboutNetwork() {
export function AboutHeroScene() {
return (
<div className="w-full h-[550px] -mt-[400px]">
<div className="w-full h-[350px] md:h-[550px] -mt-[250px] md:-mt-[400px]">
<Canvas
camera={{ position: [0, 2, 2.5], fov: 42 }}
dpr={[1, 2]}

View File

@ -341,7 +341,7 @@ function ComputeMesh() {
export function ComputeMeshScene() {
return (
<div className="w-full h-[550px] -mt-[400px]">
<div className="w-full h-[350px] md:h-[550px] -mt-[250px] md:-mt-[400px]">
<Canvas
camera={{ position: [0, 3, 3.5], fov: 38 }}
dpr={[1, 2]}

View File

@ -230,7 +230,7 @@ function ConsensusNetwork() {
export function ConsensusScene() {
return (
<div className="w-full h-[550px] -mt-[400px]">
<div className="w-full h-[350px] md:h-[550px] -mt-[250px] md:-mt-[400px]">
<Canvas
camera={{ position: [0, 2, 2], fov: 45 }}
dpr={[1, 2]}

View File

@ -334,7 +334,7 @@ function GrowthVaultNetwork() {
export function GrowthVaultScene() {
return (
<div className="w-full h-[550px] -mt-[400px]">
<div className="w-full h-[350px] md:h-[550px] -mt-[250px] md:-mt-[400px]">
<Canvas
camera={{ position: [0, 2, 3], fov: 40 }}
dpr={[1, 2]}

View File

@ -180,8 +180,8 @@ function OramaOneNode() {
export function OramaOneScene() {
return (
<div
className="absolute left-0 right-0 bottom-0 pointer-events-none"
style={{ height: "70%", opacity: 0.75 }}
className="absolute left-0 right-0 bottom-0 pointer-events-none h-[50%] md:h-[70%]"
style={{ opacity: 0.75 }}
>
<Canvas
camera={{ position: [2.2, 2.2, 2.2], fov: 28 }}

View File

@ -4,7 +4,7 @@ import { cn } from "../../lib/utils";
const paddingVariants = {
default: "py-16 sm:py-24",
narrow: "py-8 sm:py-12",
wide: "py-24 sm:py-32",
wide: "py-12 sm:py-24 lg:py-32",
none: "py-0",
} as const;

View File

@ -1,5 +1,5 @@
import { Outlet } from "react-router";
import { Suspense } from "react";
import { Suspense, useState } from "react";
import { LoadingSpinner } from "../ui/loading-spinner";
import { WhitelistBanner } from "../navigation/whitelist-banner";
import { Navbar } from "../navigation/navbar";
@ -8,6 +8,8 @@ import { ScrollToTop } from "../ui/scroll-to-top";
import { FloatingCTA } from "../navigation/floating-cta";
export function Shell() {
const [bannerVisible, setBannerVisible] = useState(true);
return (
<div
className="min-h-screen bg-surface text-fg"
@ -17,8 +19,8 @@ export function Shell() {
backgroundSize: "24px 24px",
}}
>
<WhitelistBanner />
<Navbar />
<WhitelistBanner onDismiss={() => setBannerVisible(false)} />
<Navbar bannerVisible={bannerVisible} />
<Suspense
fallback={
<div className="flex items-center justify-center min-h-screen">
@ -26,7 +28,7 @@ export function Shell() {
</div>
}
>
<main className="pt-32">
<main className="pt-16 md:pt-32">
<Outlet />
</main>
</Suspense>

View File

@ -1,6 +1,6 @@
import { useEffect } from "react";
import { Link, useLocation } from "react-router";
import { ExternalLink } from "lucide-react";
import { ExternalLink, X } from "lucide-react";
import { NAV_LINKS, MORE_LINKS } from "../../data/navigation";
import { Button } from "../ui/button";
import { cn } from "../../lib/utils";
@ -33,13 +33,22 @@ export function MobileMenu({ open, onClose }: MobileMenuProps) {
return (
<div
className={cn(
"fixed inset-0 z-40 bg-bg/95 backdrop-blur-md md:hidden flex flex-col transition-all duration-300",
"fixed inset-0 z-[60] bg-bg/95 backdrop-blur-md md:hidden flex flex-col transition-all duration-300",
open
? "opacity-100 pointer-events-auto"
: "opacity-0 pointer-events-none",
)}
>
<div className="h-20 shrink-0" />
<div className="flex items-center justify-end px-6 pt-5 pb-2">
<button
type="button"
onClick={onClose}
className="flex items-center justify-center w-10 h-10 text-muted hover:text-fg transition-colors"
aria-label="Close menu"
>
<X size={24} />
</button>
</div>
<nav className="flex flex-col px-6 gap-1">
{NAV_LINKS.map((link) => {
@ -69,20 +78,18 @@ export function MobileMenu({ open, onClose }: MobileMenuProps) {
);
})}
<div className="border-t border-border/30 mt-3 pt-3">
{MORE_LINKS.map((link) => {
const isActive = location.pathname === link.href;
return (
<Link
key={link.href}
to={link.href}
className={cn(menuLinkClass, "text-lg", isActive ? "text-fg" : "text-muted hover:text-fg")}
className={cn(menuLinkClass, isActive ? "text-fg" : "text-muted hover:text-fg")}
>
{link.label}
</Link>
);
})}
</div>
</nav>
<div className="mt-auto px-6 pb-8">

View File

@ -1,4 +1,4 @@
import { useState, useEffect } from "react";
import { useState, useEffect, useCallback } from "react";
import { Link, useLocation } from "react-router";
import { Menu, X, ExternalLink, ChevronDown } from "lucide-react";
import { cn } from "../../lib/utils";
@ -11,10 +11,11 @@ const linkClass = "px-3 py-1.5 text-xs tracking-wider uppercase font-mono rounde
const activeClass = "text-fg bg-white/[0.06]";
const inactiveClass = "text-muted hover:text-fg hover:bg-white/[0.04]";
export function Navbar() {
export function Navbar({ bannerVisible = true }: { bannerVisible?: boolean }) {
const [mobileOpen, setMobileOpen] = useState(false);
const [moreOpen, setMoreOpen] = useState(false);
const { pathname } = useLocation();
const handleMobileClose = useCallback(() => setMobileOpen(false), []);
useEffect(() => {
setMoreOpen(false);
@ -22,7 +23,7 @@ export function Navbar() {
return (
<>
<header className="fixed top-16 left-0 right-0 z-50 flex justify-center px-4 pt-2">
<header className={cn("fixed left-0 right-0 z-50 flex justify-center px-4 pt-2 transition-[top] duration-300", bannerVisible ? "top-16" : "top-4")}>
<nav className="flex items-center justify-between w-full max-w-5xl h-12 px-5 bg-surface-2/80 backdrop-blur-xl border border-border/60 rounded-full shadow-[0_4px_24px_rgba(0,0,0,0.4)]">
<Link to="/" className="flex items-center gap-2 group">
<img src={oramaIcon} alt="Orama" className="h-8 w-8 shrink-0 transition-transform duration-700 ease-in-out group-hover:rotate-[360deg]" />
@ -125,7 +126,7 @@ export function Navbar() {
<MobileMenu
open={mobileOpen}
onClose={() => setMobileOpen(false)}
onClose={handleMobileClose}
/>
</>
);

View File

@ -2,7 +2,7 @@ import { useState } from "react";
import { X, ArrowRight } from "lucide-react";
import { cn } from "../../lib/utils";
export function WhitelistBanner() {
export function WhitelistBanner({ onDismiss }: { onDismiss?: () => void }) {
const [dismissed, setDismissed] = useState(false);
if (dismissed) return null;
@ -33,7 +33,7 @@ export function WhitelistBanner() {
</a>
<button
type="button"
onClick={() => setDismissed(true)}
onClick={() => { setDismissed(true); onDismiss?.(); }}
className="flex items-center justify-center w-6 h-6 text-black/40 hover:text-black transition-colors cursor-pointer ml-2"
aria-label="Dismiss banner"
>

View File

@ -111,8 +111,13 @@ orama push --ip 1.2.3.4
# Push to all nodes in an environment
orama push --env devnet
# Fan out server-to-server (faster for many nodes)
orama push --env devnet --fanout
```
Use `--fanout` to upload to the first node, then fan out server-to-server via SSH agent forwarding. Much faster when you have many nodes.
## Adding New Nodes
Add a new VPS to your cluster with one command:

View File

@ -158,15 +158,21 @@ orama push --ip 1.2.3.4
# Push to all nodes in an environment
orama push --env devnet
# Fan out server-to-server (faster for many nodes)
orama push --env devnet --fanout
# Push with a specific SSH user
orama push --ip 1.2.3.4 --user ubuntu
```
Use `--fanout` to upload to the first node, then distribute server-to-server via SSH agent forwarding. Much faster when pushing to many nodes.
| Flag | Default | Description |
|------|---------|-------------|
| `--ip` | | Push to a single node by IP |
| `--env` | active env | Push to all nodes in environment |
| `--user` | `root` | SSH user |
| `--fanout` | `false` | Fan out from first node server-to-server (faster) |
### Legacy Push

View File

@ -97,7 +97,7 @@ function BlockchainHero() {
</a>
</Button>
<Button asChild variant="ghost" size="lg">
<Link to="/node">
<Link to="/investors#participate">
Run a Node
<ArrowRight className="w-3.5 h-3.5 ml-2" />
</Link>

View File

@ -92,9 +92,9 @@ function ComputeHero() {
</p>
<div className="flex flex-wrap items-center justify-center gap-3">
<Link to="/investors">
<Button size="lg" className="silver-button text-black font-mono font-semibold tracking-wider uppercase px-8 py-3 text-sm rounded-sm cursor-pointer opacity-50 pointer-events-none">
COMING SOON <ArrowRight className="w-4 h-4 ml-2" />
<Link to="/dashboard">
<Button size="lg" className="silver-button text-black font-mono font-semibold tracking-wider uppercase px-8 py-3 text-sm rounded-sm cursor-pointer">
Start Deploying <ArrowRight className="w-4 h-4 ml-2" />
</Button>
</Link>
<a href="https://t.me/debrosportal" target="_blank" rel="noopener noreferrer">
@ -106,7 +106,7 @@ function ComputeHero() {
<div className="flex items-center gap-2 text-xs font-mono text-muted">
<StatusDot status="active" />
<span>TESTNET LIVE 300 NODES REQUIRED FOR GENESIS</span>
<span>TESTNET LIVE WITH 50+ NODES</span>
</div>
</div>
</Section>
@ -187,7 +187,7 @@ function OramaOneSection() {
{ label: "Consensus", value: "Hybrid PoS+PoC+PoI" },
{ label: "Block Time", value: "6 seconds" },
{ label: "OramaOS", value: "1.5x Multiplier" },
{ label: "Status", value: "Coming Soon" },
{ label: "Status", value: "Live" },
].map((stat) => (
<div key={stat.label} className="flex flex-col gap-1 text-center">
<span className="text-xs font-mono text-zinc-500 uppercase">{stat.label}</span>
@ -196,8 +196,10 @@ function OramaOneSection() {
))}
</div>
<Button className="silver-button text-black font-mono font-semibold tracking-wider uppercase px-8 py-3 text-sm rounded-sm opacity-50 pointer-events-none">
Coming Soon <ArrowRight className="w-4 h-4 ml-2" />
<Button asChild className="silver-button text-black font-mono font-semibold tracking-wider uppercase px-8 py-3 text-sm rounded-sm cursor-pointer">
<Link to="/dashboard">
Start Deploying <ArrowRight className="w-4 h-4 ml-2" />
</Link>
</Button>
{/* 3D Node */}

View File

@ -75,7 +75,7 @@ function HomeHero() {
Decentralized Cloud + L1 Blockchain
</span>
<h1 className="relative z-10 font-display font-bold text-4xl lg:text-6xl leading-tight">
<h1 className="relative z-10 font-display font-bold text-3xl md:text-4xl lg:text-6xl leading-tight">
<SplitText
text="Blockchain was step one."
className="text-fg"
@ -107,9 +107,7 @@ function HomeHero() {
`}</style>
<p className="relative z-10 text-muted text-sm leading-relaxed max-w-lg">
Bitcoin gave us decentralized money. Ethereum gave us decentralized
contracts. Orama gives us decentralized everything
an L1 blockchain fused with a full cloud platform.
An L1 blockchain fused with a full cloud platform decentralized everything.
</p>
<div className="relative z-10 flex flex-wrap gap-2 justify-center">
@ -149,7 +147,7 @@ function HomeHero() {
<div className="relative z-10 flex items-center gap-2 mt-2">
<span className="w-2.5 h-2.5 rounded-full bg-emerald-400 animate-pulse-dot" />
<span className="text-xs font-mono text-muted tracking-wider uppercase">
Testnet Live 300 Nodes Required for Genesis
Testnet Live WITH 50+ NODES
</span>
</div>