This commit is contained in:
+6
-10
@@ -1,5 +1,5 @@
|
||||
/* ==========================================================================
|
||||
Vantage marketing site — design tokens
|
||||
Vantage marketing site design tokens
|
||||
Palette is anchored on the logo navy (#0B2A58). The accent IS the brand
|
||||
navy, lifting to a readable blue on dark grounds; status colours (up/down/
|
||||
pending) are semantic and deliberately never reused as the accent.
|
||||
@@ -346,7 +346,10 @@ code {
|
||||
border-radius: 4px;
|
||||
border: 1px solid transparent;
|
||||
cursor: pointer;
|
||||
transition: transform 0.1s ease, filter 0.15s ease, border-color 0.15s ease;
|
||||
transition:
|
||||
transform 0.1s ease,
|
||||
filter 0.15s ease,
|
||||
border-color 0.15s ease;
|
||||
}
|
||||
|
||||
.btn:active {
|
||||
@@ -439,14 +442,7 @@ code {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(
|
||||
100deg,
|
||||
#071628 0%,
|
||||
#071628 26%,
|
||||
rgba(7, 22, 40, 0.86) 42%,
|
||||
rgba(7, 22, 40, 0.35) 62%,
|
||||
rgba(7, 22, 40, 0.1) 100%
|
||||
);
|
||||
background: linear-gradient(100deg, #071628 0%, #071628 26%, rgba(7, 22, 40, 0.86) 42%, rgba(7, 22, 40, 0.35) 62%, rgba(7, 22, 40, 0.1) 100%);
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -9,8 +9,8 @@ export default function OverviewPage() {
|
||||
<span className="tag">Self-hosted fleet control plane</span>
|
||||
<h1>Your servers, under one pane of glass you actually own.</h1>
|
||||
<p className="lede">
|
||||
Vantage holds SSH keys, runs scripts, watches services, stores secrets and opens consoles — across every machine you manage. One agent per server, outbound connections only,
|
||||
all state in your own database.
|
||||
Vantage holds SSH keys, runs scripts, watches services, stores secrets and opens consoles across every machine you manage. One agent per server, outbound connections only, all
|
||||
state in your own database.
|
||||
</p>
|
||||
<div className="hero__acts">
|
||||
<Link className="btn btn--solid" href="/start">
|
||||
@@ -32,7 +32,7 @@ export default function OverviewPage() {
|
||||
<div className="split" style={{ marginTop: "2rem" }}>
|
||||
<p className="prose">
|
||||
Most small fleets end up with keys in a spreadsheet, scripts in someone's home directory, uptime checks in a separate service, secrets in a chat thread, and no record of
|
||||
who ran what. None of those systems know about each other, so every question — who can reach this box, what ran on it last, is it even up — gets answered by hand.
|
||||
who ran what. None of those systems know about each other, so every question who can reach this box, what ran on it last, is it even up gets answered by hand.
|
||||
</p>
|
||||
<div className="specs specs--flush">
|
||||
<div className="spec">
|
||||
|
||||
+139
-132
@@ -4,163 +4,170 @@ import { useEffect, useState } from "react";
|
||||
|
||||
/*
|
||||
* The hero's signature element: a fleet panel that plays one honest cycle of
|
||||
* what the product actually does — a workflow runs three steps, a TLS monitor
|
||||
* fails and opens an incident, a key revocation lands — then rests. It is a
|
||||
* what the product actually does a workflow runs three steps, a TLS monitor
|
||||
* fails and opens an incident, a key revocation lands then rests. It is a
|
||||
* dramatisation, not live data, so nothing here talks to an API.
|
||||
*/
|
||||
|
||||
type LogLine = { time: string; body: React.ReactNode };
|
||||
|
||||
type Beat = {
|
||||
at: number;
|
||||
line: LogLine;
|
||||
effect?: "incident" | "runDone" | "revoked";
|
||||
at: number;
|
||||
line: LogLine;
|
||||
effect?: "incident" | "runDone" | "revoked";
|
||||
};
|
||||
|
||||
const BEATS: Beat[] = [
|
||||
{ at: 600, line: { time: "14:22:02", body: "running · step 1/3 · pull image" } },
|
||||
{ at: 1500, line: { time: "14:22:04", body: "running · step 2/3 · migrate database" } },
|
||||
{
|
||||
at: 2600,
|
||||
line: { time: "14:22:07", body: <><span className="ok">ok</span> · migrate database · exit 0</> },
|
||||
},
|
||||
{
|
||||
at: 3400,
|
||||
line: { time: "14:22:08", body: "running · step 3/3 · restart service" },
|
||||
effect: "incident",
|
||||
},
|
||||
{
|
||||
at: 4300,
|
||||
line: { time: "14:22:10", body: <><span className="er">monitor</span> · edge-gw-02 tls · connection refused</> },
|
||||
},
|
||||
{
|
||||
at: 5200,
|
||||
line: { time: "14:22:11", body: <><span className="ok">ok</span> · restart service · exit 0</> },
|
||||
effect: "runDone",
|
||||
},
|
||||
{
|
||||
at: 6000,
|
||||
line: { time: "14:22:12", body: <>run finished · <span className="ok">success</span> · 3 steps · 1 server</> },
|
||||
effect: "revoked",
|
||||
},
|
||||
{ at: 600, line: { time: "14:22:02", body: "running · step 1/3 · pull image" } },
|
||||
{ at: 1500, line: { time: "14:22:04", body: "running · step 2/3 · migrate database" } },
|
||||
{
|
||||
at: 2600,
|
||||
line: {
|
||||
time: "14:22:07",
|
||||
body: (
|
||||
<>
|
||||
<span className="ok">ok</span> · migrate database · exit 0
|
||||
</>
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
at: 3400,
|
||||
line: { time: "14:22:08", body: "running · step 3/3 · restart service" },
|
||||
effect: "incident",
|
||||
},
|
||||
{
|
||||
at: 4300,
|
||||
line: {
|
||||
time: "14:22:10",
|
||||
body: (
|
||||
<>
|
||||
<span className="er">monitor</span> · edge-gw-02 tls · connection refused
|
||||
</>
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
at: 5200,
|
||||
line: {
|
||||
time: "14:22:11",
|
||||
body: (
|
||||
<>
|
||||
<span className="ok">ok</span> · restart service · exit 0
|
||||
</>
|
||||
),
|
||||
},
|
||||
effect: "runDone",
|
||||
},
|
||||
{
|
||||
at: 6000,
|
||||
line: {
|
||||
time: "14:22:12",
|
||||
body: (
|
||||
<>
|
||||
run finished · <span className="ok">success</span> · 3 steps · 1 server
|
||||
</>
|
||||
),
|
||||
},
|
||||
effect: "revoked",
|
||||
},
|
||||
];
|
||||
|
||||
const FIRST_LINE: LogLine = { time: "14:22:01", body: "queued · deploy-app · 1 server" };
|
||||
const MAX_LINES = 7;
|
||||
|
||||
export function InstrumentPanel() {
|
||||
const [lines, setLines] = useState<LogLine[]>([FIRST_LINE]);
|
||||
const [incident, setIncident] = useState(false);
|
||||
const [runActive, setRunActive] = useState(true);
|
||||
const [revoked, setRevoked] = useState(false);
|
||||
const [resting, setResting] = useState(false);
|
||||
const [lines, setLines] = useState<LogLine[]>([FIRST_LINE]);
|
||||
const [incident, setIncident] = useState(false);
|
||||
const [runActive, setRunActive] = useState(true);
|
||||
const [revoked, setRevoked] = useState(false);
|
||||
const [resting, setResting] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||||
useEffect(() => {
|
||||
const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||||
|
||||
const apply = (effect: Beat["effect"]) => {
|
||||
if (effect === "incident") setIncident(true);
|
||||
if (effect === "runDone") setRunActive(false);
|
||||
if (effect === "revoked") setRevoked(true);
|
||||
};
|
||||
const apply = (effect: Beat["effect"]) => {
|
||||
if (effect === "incident") setIncident(true);
|
||||
if (effect === "runDone") setRunActive(false);
|
||||
if (effect === "revoked") setRevoked(true);
|
||||
};
|
||||
|
||||
|
||||
if (reduced) {
|
||||
setLines([FIRST_LINE, ...BEATS.map((b) => b.line)].slice(-MAX_LINES));
|
||||
BEATS.forEach((b) => apply(b.effect));
|
||||
setResting(true);
|
||||
return;
|
||||
}
|
||||
if (reduced) {
|
||||
setLines([FIRST_LINE, ...BEATS.map((b) => b.line)].slice(-MAX_LINES));
|
||||
BEATS.forEach((b) => apply(b.effect));
|
||||
setResting(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const timers = BEATS.map((beat) =>
|
||||
window.setTimeout(() => {
|
||||
setLines((prev) => [...prev, beat.line].slice(-MAX_LINES));
|
||||
apply(beat.effect);
|
||||
}, beat.at)
|
||||
);
|
||||
timers.push(window.setTimeout(() => setResting(true), 6600));
|
||||
const timers = BEATS.map((beat) =>
|
||||
window.setTimeout(() => {
|
||||
setLines((prev) => [...prev, beat.line].slice(-MAX_LINES));
|
||||
apply(beat.effect);
|
||||
}, beat.at),
|
||||
);
|
||||
timers.push(window.setTimeout(() => setResting(true), 6600));
|
||||
|
||||
return () => timers.forEach(window.clearTimeout);
|
||||
}, []);
|
||||
return () => timers.forEach(window.clearTimeout);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="instrument">
|
||||
<div className="rail">
|
||||
<div className="instrument__bar">
|
||||
<span>
|
||||
<b>northgate</b> · fleet
|
||||
</span>
|
||||
<span>12 servers</span>
|
||||
<span>{incident ? "10 up" : "11 up"}</span>
|
||||
<span>{incident ? "2 down" : "1 down"}</span>
|
||||
<span>3 monitors</span>
|
||||
<span>{runActive ? "1 run active" : "no runs active"}</span>
|
||||
<span className="instrument__clock">14:22:12 UTC</span>
|
||||
</div>
|
||||
|
||||
<div className="panes">
|
||||
<section className="pane" aria-label="Fleet status">
|
||||
<h2 className="pane__h">
|
||||
Fleet <span>{revoked ? "key revoked · 1 server updated" : "agents polling"}</span>
|
||||
</h2>
|
||||
|
||||
<Row host="proxmox-node-1" sub="4 keys · 12% cpu" state="up" label="Active" />
|
||||
<Row host="db-primary" sub="3 keys · 61% cpu" state="up" label="Active" />
|
||||
<Row
|
||||
host="edge-gw-02"
|
||||
sub={incident ? "2 keys · tls refused" : "2 keys · tls 41d"}
|
||||
state={incident ? "down" : "up"}
|
||||
label={incident ? "Incident" : "Active"}
|
||||
/>
|
||||
<Row host="win-build-01" sub="agent 1.4.1 · update ready" state="pend" label="Pending" />
|
||||
<Row
|
||||
host="app-worker-03"
|
||||
sub={revoked ? "4 keys · 1 revoked" : "5 keys · idle"}
|
||||
state="up"
|
||||
label="Active"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section className="pane" aria-label="Workflow run">
|
||||
<h2 className="pane__h">
|
||||
Run <span>run_8f31c2</span>
|
||||
</h2>
|
||||
<div className="stream" aria-live="polite">
|
||||
{lines.map((line, i) => (
|
||||
<div key={`${line.time}-${i}`}>
|
||||
<span className="t">{line.time}</span> {line.body}
|
||||
return (
|
||||
<div className="instrument">
|
||||
<div className="rail">
|
||||
<div className="instrument__bar">
|
||||
<span>
|
||||
<b>northgate</b> · fleet
|
||||
</span>
|
||||
<span>12 servers</span>
|
||||
<span>{incident ? "10 up" : "11 up"}</span>
|
||||
<span>{incident ? "2 down" : "1 down"}</span>
|
||||
<span>3 monitors</span>
|
||||
<span>{runActive ? "1 run active" : "no runs active"}</span>
|
||||
<span className="instrument__clock">14:22:12 UTC</span>
|
||||
</div>
|
||||
))}
|
||||
{resting && (
|
||||
<div>
|
||||
<span className="caret">_</span>
|
||||
|
||||
<div className="panes">
|
||||
<section className="pane" aria-label="Fleet status">
|
||||
<h2 className="pane__h">
|
||||
Fleet <span>{revoked ? "key revoked · 1 server updated" : "agents polling"}</span>
|
||||
</h2>
|
||||
|
||||
<Row host="proxmox-node-1" sub="4 keys · 12% cpu" state="up" label="Active" />
|
||||
<Row host="db-primary" sub="3 keys · 61% cpu" state="up" label="Active" />
|
||||
<Row host="edge-gw-02" sub={incident ? "2 keys · tls refused" : "2 keys · tls 41d"} state={incident ? "down" : "up"} label={incident ? "Incident" : "Active"} />
|
||||
<Row host="win-build-01" sub="agent 1.4.1 · update ready" state="pend" label="Pending" />
|
||||
<Row host="app-worker-03" sub={revoked ? "4 keys · 1 revoked" : "5 keys · idle"} state="up" label="Active" />
|
||||
</section>
|
||||
|
||||
<section className="pane" aria-label="Workflow run">
|
||||
<h2 className="pane__h">
|
||||
Run <span>run_8f31c2</span>
|
||||
</h2>
|
||||
<div className="stream" aria-live="polite">
|
||||
{lines.map((line, i) => (
|
||||
<div key={`${line.time}-${i}`}>
|
||||
<span className="t">{line.time}</span> {line.body}
|
||||
</div>
|
||||
))}
|
||||
{resting && (
|
||||
<div>
|
||||
<span className="caret">_</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
function Row({
|
||||
host,
|
||||
sub,
|
||||
state,
|
||||
label,
|
||||
}: {
|
||||
host: string;
|
||||
sub: string;
|
||||
state: "up" | "down" | "pend";
|
||||
label: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="frow">
|
||||
<span className={state === "up" ? "dot" : `dot dot--${state}`} />
|
||||
<span className="frow__host">{host}</span>
|
||||
<span className="frow__sub">{sub}</span>
|
||||
<span className={`chip chip--${state}`}>{label}</span>
|
||||
</div>
|
||||
);
|
||||
function Row({ host, sub, state, label }: { host: string; sub: string; state: "up" | "down" | "pend"; label: string }) {
|
||||
return (
|
||||
<div className="frow">
|
||||
<span className={state === "up" ? "dot" : `dot dot--${state}`} />
|
||||
<span className="frow__host">{host}</span>
|
||||
<span className="frow__sub">{sub}</span>
|
||||
<span className={`chip chip--${state}`}>{label}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ export function OrgForm() {
|
||||
We sent a confirmation link. Open it and <b>{slug || "your organisation"}</b> is created with you as its owner. The link works once and expires in 24 hours.
|
||||
</p>
|
||||
<p style={{ color: "var(--ink-3)", marginTop: "0.75rem", fontSize: "0.88rem" }}>
|
||||
Nothing exists until you confirm — if the email does not arrive, start again or contact support@hostxtra.co.uk.
|
||||
Nothing exists until you confirm if the email does not arrive, start again or contact support@hostxtra.co.uk.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
@@ -80,7 +80,7 @@ export function OrgForm() {
|
||||
<div className="field">
|
||||
<label htmlFor="o-pass">Password</label>
|
||||
<input id="o-pass" name="password" type="password" autoComplete="new-password" minLength={MIN_PASSWORD} required aria-describedby="o-pass-err" />
|
||||
<small>At least {MIN_PASSWORD} characters. Use a manager — you are about to manage SSH keys with it.</small>
|
||||
<small>At least {MIN_PASSWORD} characters. Use a manager you are about to manage SSH keys with it.</small>
|
||||
{fieldError("password") && (
|
||||
<small id="o-pass-err" className="field__err">
|
||||
{fieldError("password")}
|
||||
|
||||
Vendored
+4
-4
@@ -1,6 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
/
|
||||
/
|
||||
import "./.next/dev/types/routes.d.ts";
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
|
||||
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
// Standalone output, matching web/: the build emits a self-contained server
|
||||
// bundle that runs under Node in the runtime image.
|
||||
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
output: "standalone",
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user