mirror of
https://github.com/DeBrosOfficial/orama.git
synced 2026-05-01 05:04:13 +00:00
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:
parent
b9b6e19bb8
commit
7284cb4578
@ -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 {
|
||||
|
||||
@ -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]}
|
||||
|
||||
@ -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]}
|
||||
|
||||
@ -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]}
|
||||
|
||||
@ -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]}
|
||||
|
||||
@ -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 }}
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@ -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"
|
||||
>
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 */}
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user