This commit is contained in:
6
custom.d.ts
vendored
6
custom.d.ts
vendored
@ -1,4 +1,4 @@
|
||||
declare module "*.md" {
|
||||
const value: string;
|
||||
export default value;
|
||||
}
|
||||
const value: string;
|
||||
export default value;
|
||||
}
|
||||
|
@ -102,7 +102,7 @@ export class LNVpsApi {
|
||||
constructor(
|
||||
readonly url: string,
|
||||
readonly publisher: EventPublisher | undefined,
|
||||
) { }
|
||||
) {}
|
||||
|
||||
async getAccount() {
|
||||
const { data } = await this.#handleResponse<ApiResponse<AccountDetail>>(
|
||||
|
@ -36,4 +36,4 @@
|
||||
.markdown h5,
|
||||
.markdown h6 {
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
}
|
||||
|
@ -5,130 +5,178 @@ import { Token, Tokens, marked } from "marked";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
interface MarkdownProps {
|
||||
content: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
const Markdown = forwardRef<HTMLDivElement, MarkdownProps>((props: MarkdownProps, ref) => {
|
||||
const Markdown = forwardRef<HTMLDivElement, MarkdownProps>(
|
||||
(props: MarkdownProps, ref) => {
|
||||
let ctr = 0;
|
||||
function renderToken(t: Token): ReactNode {
|
||||
try {
|
||||
switch (t.type) {
|
||||
case "paragraph": {
|
||||
return <div key={ctr++}>{t.tokens ? t.tokens.map(renderToken) : t.raw}</div>;
|
||||
}
|
||||
case "image": {
|
||||
return <img key={ctr++} src={t.href} />;
|
||||
}
|
||||
case "heading": {
|
||||
switch (t.depth) {
|
||||
case 1:
|
||||
return <h1 key={ctr++}>{t.tokens ? t.tokens.map(renderToken) : t.raw}</h1>;
|
||||
case 2:
|
||||
return <h2 key={ctr++}>{t.tokens ? t.tokens.map(renderToken) : t.raw}</h2>;
|
||||
case 3:
|
||||
return <h3 key={ctr++}>{t.tokens ? t.tokens.map(renderToken) : t.raw}</h3>;
|
||||
case 4:
|
||||
return <h4 key={ctr++}>{t.tokens ? t.tokens.map(renderToken) : t.raw}</h4>;
|
||||
case 5:
|
||||
return <h5 key={ctr++}>{t.tokens ? t.tokens.map(renderToken) : t.raw}</h5>;
|
||||
case 6:
|
||||
return <h6 key={ctr++}>{t.tokens ? t.tokens.map(renderToken) : t.raw}</h6>;
|
||||
}
|
||||
throw new Error("Invalid heading");
|
||||
}
|
||||
case "codespan": {
|
||||
return <code key={ctr++}>{t.raw}</code>;
|
||||
}
|
||||
case "code": {
|
||||
return <pre key={ctr++}>{t.raw}</pre>;
|
||||
}
|
||||
case "br": {
|
||||
return <br key={ctr++} />;
|
||||
}
|
||||
case "hr": {
|
||||
return <hr key={ctr++} />;
|
||||
}
|
||||
case "strong": {
|
||||
return <b key={ctr++}>{t.tokens ? t.tokens.map(renderToken) : t.raw}</b>;
|
||||
}
|
||||
case "blockquote": {
|
||||
return <blockquote key={ctr++}>{t.tokens ? t.tokens.map(renderToken) : t.raw}</blockquote>;
|
||||
}
|
||||
case "link": {
|
||||
return (
|
||||
<Link to={t.href} key={ctr++}>
|
||||
{t.tokens ? t.tokens.map(renderToken) : t.raw}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
case "list": {
|
||||
if (t.ordered) {
|
||||
return <ol key={ctr++}>{t.items.map(renderToken)}</ol>;
|
||||
} else {
|
||||
return <ul key={ctr++}>{t.items.map(renderToken)}</ul>;
|
||||
}
|
||||
}
|
||||
case "list_item": {
|
||||
return <li key={ctr++}>{t.tokens ? t.tokens.map(renderToken) : t.raw}</li>;
|
||||
}
|
||||
case "em": {
|
||||
return <em key={ctr++}>{t.tokens ? t.tokens.map(renderToken) : t.raw}</em>;
|
||||
}
|
||||
case "del": {
|
||||
return <s key={ctr++}>{t.tokens ? t.tokens.map(renderToken) : t.raw}</s>;
|
||||
}
|
||||
case "table": {
|
||||
return (
|
||||
<table className="table-auto border-collapse" key={ctr++}>
|
||||
<thead>
|
||||
<tr>
|
||||
{(t.header as Tokens.TableCell[]).map(v => (
|
||||
<th className="border" key={ctr++}>
|
||||
{v.tokens ? v.tokens.map(renderToken) : v.text}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{(t.rows as Tokens.TableCell[][]).map(v => (
|
||||
<tr key={ctr++}>
|
||||
{v.map((d, d_key) => (
|
||||
<td className="border px-2 py-1" key={d_key}>
|
||||
{d.tokens ? d.tokens.map(renderToken) : d.text}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
case "text": {
|
||||
if ("tokens" in t) {
|
||||
return (t.tokens as Array<Token>).map(renderToken);
|
||||
}
|
||||
return t.raw;
|
||||
}
|
||||
case "space": {
|
||||
return " ";
|
||||
}
|
||||
default: {
|
||||
console.debug(`Unknown token ${t.type}`);
|
||||
}
|
||||
try {
|
||||
switch (t.type) {
|
||||
case "paragraph": {
|
||||
return (
|
||||
<div key={ctr++}>
|
||||
{t.tokens ? t.tokens.map(renderToken) : t.raw}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
case "image": {
|
||||
return <img key={ctr++} src={t.href} />;
|
||||
}
|
||||
case "heading": {
|
||||
switch (t.depth) {
|
||||
case 1:
|
||||
return (
|
||||
<h1 key={ctr++}>
|
||||
{t.tokens ? t.tokens.map(renderToken) : t.raw}
|
||||
</h1>
|
||||
);
|
||||
case 2:
|
||||
return (
|
||||
<h2 key={ctr++}>
|
||||
{t.tokens ? t.tokens.map(renderToken) : t.raw}
|
||||
</h2>
|
||||
);
|
||||
case 3:
|
||||
return (
|
||||
<h3 key={ctr++}>
|
||||
{t.tokens ? t.tokens.map(renderToken) : t.raw}
|
||||
</h3>
|
||||
);
|
||||
case 4:
|
||||
return (
|
||||
<h4 key={ctr++}>
|
||||
{t.tokens ? t.tokens.map(renderToken) : t.raw}
|
||||
</h4>
|
||||
);
|
||||
case 5:
|
||||
return (
|
||||
<h5 key={ctr++}>
|
||||
{t.tokens ? t.tokens.map(renderToken) : t.raw}
|
||||
</h5>
|
||||
);
|
||||
case 6:
|
||||
return (
|
||||
<h6 key={ctr++}>
|
||||
{t.tokens ? t.tokens.map(renderToken) : t.raw}
|
||||
</h6>
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
throw new Error("Invalid heading");
|
||||
}
|
||||
case "codespan": {
|
||||
return <code key={ctr++}>{t.raw}</code>;
|
||||
}
|
||||
case "code": {
|
||||
return <pre key={ctr++}>{t.raw}</pre>;
|
||||
}
|
||||
case "br": {
|
||||
return <br key={ctr++} />;
|
||||
}
|
||||
case "hr": {
|
||||
return <hr key={ctr++} />;
|
||||
}
|
||||
case "strong": {
|
||||
return (
|
||||
<b key={ctr++}>{t.tokens ? t.tokens.map(renderToken) : t.raw}</b>
|
||||
);
|
||||
}
|
||||
case "blockquote": {
|
||||
return (
|
||||
<blockquote key={ctr++}>
|
||||
{t.tokens ? t.tokens.map(renderToken) : t.raw}
|
||||
</blockquote>
|
||||
);
|
||||
}
|
||||
case "link": {
|
||||
return (
|
||||
<Link to={t.href} key={ctr++}>
|
||||
{t.tokens ? t.tokens.map(renderToken) : t.raw}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
case "list": {
|
||||
if (t.ordered) {
|
||||
return <ol key={ctr++}>{t.items.map(renderToken)}</ol>;
|
||||
} else {
|
||||
return <ul key={ctr++}>{t.items.map(renderToken)}</ul>;
|
||||
}
|
||||
}
|
||||
case "list_item": {
|
||||
return (
|
||||
<li key={ctr++}>
|
||||
{t.tokens ? t.tokens.map(renderToken) : t.raw}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
case "em": {
|
||||
return (
|
||||
<em key={ctr++}>
|
||||
{t.tokens ? t.tokens.map(renderToken) : t.raw}
|
||||
</em>
|
||||
);
|
||||
}
|
||||
case "del": {
|
||||
return (
|
||||
<s key={ctr++}>{t.tokens ? t.tokens.map(renderToken) : t.raw}</s>
|
||||
);
|
||||
}
|
||||
case "table": {
|
||||
return (
|
||||
<table className="table-auto border-collapse" key={ctr++}>
|
||||
<thead>
|
||||
<tr>
|
||||
{(t.header as Tokens.TableCell[]).map((v) => (
|
||||
<th className="border" key={ctr++}>
|
||||
{v.tokens ? v.tokens.map(renderToken) : v.text}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{(t.rows as Tokens.TableCell[][]).map((v) => (
|
||||
<tr key={ctr++}>
|
||||
{v.map((d, d_key) => (
|
||||
<td className="border px-2 py-1" key={d_key}>
|
||||
{d.tokens ? d.tokens.map(renderToken) : d.text}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
case "text": {
|
||||
if ("tokens" in t) {
|
||||
return (t.tokens as Array<Token>).map(renderToken);
|
||||
}
|
||||
return t.raw;
|
||||
}
|
||||
case "space": {
|
||||
return " ";
|
||||
}
|
||||
default: {
|
||||
console.debug(`Unknown token ${t.type}`);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
const parsed = useMemo(() => {
|
||||
return marked.lexer(props.content);
|
||||
return marked.lexer(props.content);
|
||||
}, [props.content]);
|
||||
return (
|
||||
<div className="markdown" ref={ref}>
|
||||
{parsed.filter(a => a.type !== "footnote" && a.type !== "footnotes").map(a => renderToken(a))}
|
||||
</div>
|
||||
<div className="markdown" ref={ref}>
|
||||
{parsed
|
||||
.filter((a) => a.type !== "footnote" && a.type !== "footnotes")
|
||||
.map((a) => renderToken(a))}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
export default Markdown;
|
||||
|
@ -3,16 +3,15 @@ import BytesSize from "./bytes";
|
||||
|
||||
export default function VpsResources({ vm }: { vm: VmInstance | VmTemplate }) {
|
||||
const diskType = "template" in vm ? vm.template?.disk_type : vm.disk_type;
|
||||
const region =
|
||||
"region" in vm ? vm.region.name : vm.template?.region?.name;
|
||||
const region = "region" in vm ? vm.region.name : vm.template?.region?.name;
|
||||
const status = "status" in vm ? vm.status : undefined;
|
||||
const template = "template" in vm ? vm.template : vm as VmTemplate;
|
||||
const template = "template" in vm ? vm.template : (vm as VmTemplate);
|
||||
return (
|
||||
<>
|
||||
<div className="text-xs text-neutral-400">
|
||||
{template?.cpu} vCPU, <BytesSize value={template?.memory ?? 0} /> RAM,{" "}
|
||||
<BytesSize value={template?.disk_size ?? 0} /> {diskType?.toUpperCase()},{" "}
|
||||
{region && <>Location: {region}</>}
|
||||
<BytesSize value={template?.disk_size ?? 0} /> {diskType?.toUpperCase()}
|
||||
, {region && <>Location: {region}</>}
|
||||
</div>
|
||||
{status && status.state === "running" && (
|
||||
<div className="text-sm text-neutral-200">
|
||||
|
@ -10,13 +10,17 @@ export default function useLogin() {
|
||||
() => LoginState.snapshot(),
|
||||
);
|
||||
const system = useContext(SnortContext);
|
||||
return useMemo(() => session
|
||||
? {
|
||||
type: session.type,
|
||||
publicKey: session.publicKey,
|
||||
system,
|
||||
api: new LNVpsApi(ApiUrl, LoginState.getSigner()),
|
||||
logout: () => LoginState.logout()
|
||||
}
|
||||
: undefined, [session, system]);
|
||||
return useMemo(
|
||||
() =>
|
||||
session
|
||||
? {
|
||||
type: session.type,
|
||||
publicKey: session.publicKey,
|
||||
system,
|
||||
api: new LNVpsApi(ApiUrl, LoginState.getSigner()),
|
||||
logout: () => LoginState.logout(),
|
||||
}
|
||||
: undefined,
|
||||
[session, system],
|
||||
);
|
||||
}
|
||||
|
@ -45,4 +45,4 @@ select {
|
||||
|
||||
input:disabled {
|
||||
@apply text-neutral-200/50;
|
||||
}
|
||||
}
|
||||
|
@ -43,7 +43,7 @@ const router = createBrowserRouter([
|
||||
},
|
||||
{
|
||||
path: "/account/settings",
|
||||
element: <AccountSettings />
|
||||
element: <AccountSettings />,
|
||||
},
|
||||
{
|
||||
path: "/order",
|
||||
@ -60,7 +60,7 @@ const router = createBrowserRouter([
|
||||
{
|
||||
path: "/status",
|
||||
element: <StatusPage />,
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
@ -5,48 +5,71 @@ import { AsyncButton } from "../components/button";
|
||||
import { Icon } from "../components/icon";
|
||||
|
||||
export function AccountSettings() {
|
||||
const login = useLogin();
|
||||
const [acc, setAcc] = useState<AccountDetail>();
|
||||
const [editEmail, setEditEmail] = useState(false);
|
||||
const login = useLogin();
|
||||
const [acc, setAcc] = useState<AccountDetail>();
|
||||
const [editEmail, setEditEmail] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
login?.api.getAccount().then(setAcc);
|
||||
}, [login]);
|
||||
|
||||
function notifications() {
|
||||
return <>
|
||||
<h3>Notification Settings</h3>
|
||||
<div className="flex gap-2 items-center">
|
||||
<input type="checkbox" checked={acc?.contact_email ?? false} onChange={(e) => {
|
||||
setAcc((s) => (s ? { ...s, contact_email: e.target.checked } : undefined));
|
||||
}} />
|
||||
Email
|
||||
<input type="checkbox" checked={acc?.contact_nip17 ?? false} onChange={(e) => {
|
||||
setAcc((s) => (s ? { ...s, contact_nip17: e.target.checked } : undefined));
|
||||
}} />
|
||||
Nostr DM
|
||||
</div>
|
||||
<div className="flex gap-2 items-center">
|
||||
<h4>Email</h4>
|
||||
<input type="text" disabled={!editEmail} value={acc?.email} onChange={e => setAcc(s => (s ? { ...s, email: e.target.value } : undefined))} />
|
||||
{!editEmail && <Icon name="pencil" onClick={() => setEditEmail(true)} />}
|
||||
</div>
|
||||
<div>
|
||||
<AsyncButton onClick={async () => {
|
||||
if (login?.api && acc) {
|
||||
await login.api.updateAccount(acc);
|
||||
const newAcc = await login.api.getAccount();
|
||||
setAcc(newAcc);
|
||||
setEditEmail(false);
|
||||
}
|
||||
}}>
|
||||
Save
|
||||
</AsyncButton>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
useEffect(() => {
|
||||
login?.api.getAccount().then(setAcc);
|
||||
}, [login]);
|
||||
|
||||
return <>
|
||||
{notifications()}
|
||||
</>
|
||||
}
|
||||
function notifications() {
|
||||
return (
|
||||
<>
|
||||
<h3>Notification Settings</h3>
|
||||
<div className="flex gap-2 items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={acc?.contact_email ?? false}
|
||||
onChange={(e) => {
|
||||
setAcc((s) =>
|
||||
s ? { ...s, contact_email: e.target.checked } : undefined,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
Email
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={acc?.contact_nip17 ?? false}
|
||||
onChange={(e) => {
|
||||
setAcc((s) =>
|
||||
s ? { ...s, contact_nip17: e.target.checked } : undefined,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
Nostr DM
|
||||
</div>
|
||||
<div className="flex gap-2 items-center">
|
||||
<h4>Email</h4>
|
||||
<input
|
||||
type="text"
|
||||
disabled={!editEmail}
|
||||
value={acc?.email}
|
||||
onChange={(e) =>
|
||||
setAcc((s) => (s ? { ...s, email: e.target.value } : undefined))
|
||||
}
|
||||
/>
|
||||
{!editEmail && (
|
||||
<Icon name="pencil" onClick={() => setEditEmail(true)} />
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<AsyncButton
|
||||
onClick={async () => {
|
||||
if (login?.api && acc) {
|
||||
await login.api.updateAccount(acc);
|
||||
const newAcc = await login.api.getAccount();
|
||||
setAcc(newAcc);
|
||||
setEditEmail(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</AsyncButton>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return <>{notifications()}</>;
|
||||
}
|
||||
|
@ -26,42 +26,49 @@ export default function AccountPage() {
|
||||
}
|
||||
}, [login]);
|
||||
|
||||
|
||||
const npub = hexToBech32("npub", login?.publicKey);
|
||||
const subjectLine = `[${npub}] Account Query`;
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
Your Public Key:
|
||||
<pre className="bg-neutral-900 rounded-md px-3 py-2 select-all text-sm">{npub}</pre>
|
||||
<pre className="bg-neutral-900 rounded-md px-3 py-2 select-all text-sm">
|
||||
{npub}
|
||||
</pre>
|
||||
<div className="flex justify-between">
|
||||
<AsyncButton onClick={() => navigate("settings")}>
|
||||
Settings
|
||||
</AsyncButton>
|
||||
<AsyncButton onClick={() => {
|
||||
login?.logout();
|
||||
navigate("/")
|
||||
}}>
|
||||
<AsyncButton onClick={() => navigate("settings")}>Settings</AsyncButton>
|
||||
<AsyncButton
|
||||
onClick={() => {
|
||||
login?.logout();
|
||||
navigate("/");
|
||||
}}
|
||||
>
|
||||
Logout
|
||||
</AsyncButton>
|
||||
</div>
|
||||
<h3>My Resources</h3>
|
||||
<div className="rounded-xl bg-red-400 text-black p-3">
|
||||
Something doesnt look right? <br />
|
||||
Please contact support on: {" "}
|
||||
<a href={`mailto:sales@lnvps.net?subject=${encodeURIComponent(subjectLine)}`} className="underline">
|
||||
Please contact support on:{" "}
|
||||
<a
|
||||
href={`mailto:sales@lnvps.net?subject=${encodeURIComponent(subjectLine)}`}
|
||||
className="underline"
|
||||
>
|
||||
sales@lnvps.net
|
||||
</a>
|
||||
<br />
|
||||
<b>Please include your public key in all communications.</b>
|
||||
</div>
|
||||
{vms.map((a) => (
|
||||
<VpsInstanceRow key={a.id} vm={a} onReload={() => {
|
||||
if (login?.api) {
|
||||
loadVms(login.api);
|
||||
}
|
||||
}} />
|
||||
<VpsInstanceRow
|
||||
key={a.id}
|
||||
vm={a}
|
||||
onReload={() => {
|
||||
if (login?.api) {
|
||||
loadVms(login.api);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -17,7 +17,8 @@ export default function HomePage() {
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="text-2xl">VPS Offers</div>
|
||||
<div>
|
||||
Virtual Private Server hosting with flexible plans, high uptime, and dedicated support, tailored to your needs.
|
||||
Virtual Private Server hosting with flexible plans, high uptime, and
|
||||
dedicated support, tailored to your needs.
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{offers.map((a) => (
|
||||
@ -36,7 +37,10 @@ export default function HomePage() {
|
||||
{" | "}
|
||||
<Link to="/tos">Terms</Link>
|
||||
{" | "}
|
||||
<a href={`https://snort.social/${NostrProfile.encode()}`} target="_blank">
|
||||
<a
|
||||
href={`https://snort.social/${NostrProfile.encode()}`}
|
||||
target="_blank"
|
||||
>
|
||||
Nostr
|
||||
</a>
|
||||
{" | "}
|
||||
@ -49,13 +53,13 @@ export default function HomePage() {
|
||||
</a>
|
||||
</div>
|
||||
<div className="text-xs text-center text-neutral-400">
|
||||
LNVPS is a trading name of Apex Strata Ltd, a company registered in Ireland.
|
||||
LNVPS is a trading name of Apex Strata Ltd, a company registered in
|
||||
Ireland.
|
||||
<br />
|
||||
Comany Number: 702423,
|
||||
Address: Suite 10628, 26/27 Upper Pembroke Street, Dublin 2, D02 X361, Ireland
|
||||
Comany Number: 702423, Address: Suite 10628, 26/27 Upper Pembroke
|
||||
Street, Dublin 2, D02 X361, Ireland
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
@ -5,7 +5,9 @@ export default function Layout() {
|
||||
return (
|
||||
<div className="w-[700px] mx-auto m-2 p-2">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<Link to="/" className="text-2xl">LNVPS</Link>
|
||||
<Link to="/" className="text-2xl">
|
||||
LNVPS
|
||||
</Link>
|
||||
<LoginButton />
|
||||
</div>
|
||||
|
||||
|
@ -1,49 +1,56 @@
|
||||
|
||||
import Markdown from "../components/markdown";
|
||||
import Status from "../status.json";
|
||||
|
||||
export function StatusPage() {
|
||||
const totalDowntime = Status.events.reduce((acc, v) => {
|
||||
if (v.end_time) {
|
||||
const end = new Date(v.end_time);
|
||||
const start = new Date(v.start_time);
|
||||
const duration = end.getTime() - start.getTime();
|
||||
acc += duration;
|
||||
}
|
||||
return acc;
|
||||
}, 0);
|
||||
const birth = new Date(Status.birth);
|
||||
const now = new Date();
|
||||
const age = now.getTime() - birth.getTime();
|
||||
const uptime = 1 - (totalDowntime / age);
|
||||
|
||||
function formatDuration(n: number) {
|
||||
if (n > 3600) {
|
||||
return `${(n / 3600).toFixed(0)}h ${((n % 3600) / 60).toFixed(0)}m`;
|
||||
} else if (n > 60) {
|
||||
return `${(n % 60).toFixed(0)}m`;
|
||||
} else {
|
||||
return `${n.toFixed(0)}s`;
|
||||
}
|
||||
const totalDowntime = Status.events.reduce((acc, v) => {
|
||||
if (v.end_time) {
|
||||
const end = new Date(v.end_time);
|
||||
const start = new Date(v.start_time);
|
||||
const duration = end.getTime() - start.getTime();
|
||||
acc += duration;
|
||||
}
|
||||
return acc;
|
||||
}, 0);
|
||||
const birth = new Date(Status.birth);
|
||||
const now = new Date();
|
||||
const age = now.getTime() - birth.getTime();
|
||||
const uptime = 1 - totalDowntime / age;
|
||||
|
||||
return <div className="flex flex-col gap-4">
|
||||
<div className="text-2xl">Uptime: {(100 * uptime).toFixed(5)}%</div>
|
||||
function formatDuration(n: number) {
|
||||
if (n > 3600) {
|
||||
return `${(n / 3600).toFixed(0)}h ${((n % 3600) / 60).toFixed(0)}m`;
|
||||
} else if (n > 60) {
|
||||
return `${(n % 60).toFixed(0)}m`;
|
||||
} else {
|
||||
return `${n.toFixed(0)}s`;
|
||||
}
|
||||
}
|
||||
|
||||
<div className="text-xl">Incidents:</div>
|
||||
{Status.events.map(e => {
|
||||
const end = e.end_time ? new Date(e.end_time) : undefined;
|
||||
const start = new Date(e.start_time);
|
||||
const duration = end ? end.getTime() - start.getTime() : undefined;
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="text-2xl">Uptime: {(100 * uptime).toFixed(5)}%</div>
|
||||
|
||||
return <div className="rounded-xl bg-neutral-900 px-3 py-4 flex flex-col gap-2">
|
||||
<div className="text-xl flex justify-between">
|
||||
<div>{e.title}</div>
|
||||
<div>{new Date(e.start_time).toLocaleString()}</div>
|
||||
</div>
|
||||
{duration && <div className="text-sm text-neutral-400">Duration: {formatDuration(duration / 1000)}</div>}
|
||||
<Markdown content={e.description} />
|
||||
<div className="text-xl">Incidents:</div>
|
||||
{Status.events.map((e) => {
|
||||
const end = e.end_time ? new Date(e.end_time) : undefined;
|
||||
const start = new Date(e.start_time);
|
||||
const duration = end ? end.getTime() - start.getTime() : undefined;
|
||||
|
||||
return (
|
||||
<div className="rounded-xl bg-neutral-900 px-3 py-4 flex flex-col gap-2">
|
||||
<div className="text-xl flex justify-between">
|
||||
<div>{e.title}</div>
|
||||
<div>{new Date(e.start_time).toLocaleString()}</div>
|
||||
</div>
|
||||
})}
|
||||
{duration && (
|
||||
<div className="text-sm text-neutral-400">
|
||||
Duration: {formatDuration(duration / 1000)}
|
||||
</div>
|
||||
)}
|
||||
<Markdown content={e.description} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@ -2,5 +2,5 @@ import Markdown from "../components/markdown";
|
||||
import TOS from "../tos.md?raw";
|
||||
|
||||
export function TosPage() {
|
||||
return <Markdown content={TOS} />
|
||||
}
|
||||
return <Markdown content={TOS} />;
|
||||
}
|
||||
|
@ -1,11 +1,11 @@
|
||||
{
|
||||
"birth": "2024-06-05T00:00:00Z",
|
||||
"events":[
|
||||
{
|
||||
"start_time": "2025-02-10T05:00:00Z",
|
||||
"end_time": "2025-02-10T10:08:00Z",
|
||||
"title": "VPS outage",
|
||||
"description": "Primary disk full, causing system to halt"
|
||||
}
|
||||
]
|
||||
}
|
||||
"birth": "2024-06-05T00:00:00Z",
|
||||
"events": [
|
||||
{
|
||||
"start_time": "2025-02-10T05:00:00Z",
|
||||
"end_time": "2025-02-10T10:08:00Z",
|
||||
"title": "VPS outage",
|
||||
"description": "Primary disk full, causing system to halt"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
117
src/tos.md
117
src/tos.md
@ -1,107 +1,126 @@
|
||||
# Terms of Service
|
||||
# Terms of Service
|
||||
|
||||
**LNVPS**
|
||||
*Last Updated: February 26, 2025*
|
||||
_Last Updated: February 26, 2025_
|
||||
|
||||
Welcome to LNVPS, a trading name of Apex Strata Ltd, a company registered in Ireland. These Terms of Service ("Terms") govern your use of our Virtual Private Server (VPS) hosting services, website, and related offerings (collectively, the "Services"). By accessing or using our Services, you agree to be bound by these Terms. If you do not agree, please do not use our Services.
|
||||
|
||||
---
|
||||
|
||||
## 1. Company Information
|
||||
LNVPS is a trading name of Apex Strata Ltd, a company registered in Ireland.
|
||||
## 1. Company Information
|
||||
|
||||
LNVPS is a trading name of Apex Strata Ltd, a company registered in Ireland.
|
||||
|
||||
- **Company Registration Number**: 702423
|
||||
- **Registered Office**: Suite 10628, 26/27 Upper Pembroke Street, Dublin 2, D02 X361, Ireland
|
||||
- **Email**: sales@lnvps.net
|
||||
|
||||
---
|
||||
|
||||
## 2. Definitions
|
||||
- **"You" or "Customer"**: The individual or entity subscribing to or using the Services.
|
||||
- **"We", "Us", or "LNVPS"**: Apex Strata Ltd, operating as LNVPS.
|
||||
- **"Services"**: VPS hosting, support, and any additional features provided by LNVPS.
|
||||
## 2. Definitions
|
||||
|
||||
- **"You" or "Customer"**: The individual or entity subscribing to or using the Services.
|
||||
- **"We", "Us", or "LNVPS"**: Apex Strata Ltd, operating as LNVPS.
|
||||
- **"Services"**: VPS hosting, support, and any additional features provided by LNVPS.
|
||||
|
||||
---
|
||||
|
||||
## 3. Eligibility
|
||||
## 3. Eligibility
|
||||
|
||||
You must be at least 18 years old and capable of entering into a legally binding agreement to use our Services. By signing up, you confirm that all information provided is accurate and that you are authorized to act on behalf of any entity you represent.
|
||||
|
||||
---
|
||||
|
||||
## 4. Services
|
||||
## 4. Services
|
||||
|
||||
LNVPS provides VPS hosting services, including server resources, bandwidth, and technical support, as outlined on our website ([lnvps.net](https://lnvps.net) or applicable domain). Specific features, pricing, and resource limits are detailed in your chosen service plan at the time of purchase.
|
||||
|
||||
- **Service Availability**: We strive for 99.9% uptime but do not guarantee uninterrupted service. Downtime may occur due to maintenance, upgrades, or unforeseen events.
|
||||
- **Modifications**: We reserve the right to modify or discontinue any aspect of the Services with reasonable notice.
|
||||
- **Service Availability**: We strive for 99.9% uptime but do not guarantee uninterrupted service. Downtime may occur due to maintenance, upgrades, or unforeseen events.
|
||||
- **Modifications**: We reserve the right to modify or discontinue any aspect of the Services with reasonable notice.
|
||||
|
||||
---
|
||||
|
||||
## 5. Account Responsibilities
|
||||
- **Account Security**: You are responsible for maintaining the confidentiality of your account credentials and for all activities under your account.
|
||||
- **Usage**: You agree to use the Services only for lawful purposes and in compliance with these Terms.
|
||||
- **Notification**: You must notify us immediately of any unauthorized use of your account.
|
||||
## 5. Account Responsibilities
|
||||
|
||||
- **Account Security**: You are responsible for maintaining the confidentiality of your account credentials and for all activities under your account.
|
||||
- **Usage**: You agree to use the Services only for lawful purposes and in compliance with these Terms.
|
||||
- **Notification**: You must notify us immediately of any unauthorized use of your account.
|
||||
|
||||
---
|
||||
|
||||
## 6. Acceptable Use Policy
|
||||
You agree not to use the Services to:
|
||||
- Host, store, or distribute illegal content, including but not limited to pirated software, child exploitation material, or content inciting violence or hate.
|
||||
- Engage in spamming, phishing, or other abusive activities.
|
||||
- Overload or disrupt our servers, networks, or other customers’ services (e.g., DDoS attacks).
|
||||
- Violate intellectual property rights or privacy laws.
|
||||
## 6. Acceptable Use Policy
|
||||
|
||||
You agree not to use the Services to:
|
||||
|
||||
- Host, store, or distribute illegal content, including but not limited to pirated software, child exploitation material, or content inciting violence or hate.
|
||||
- Engage in spamming, phishing, or other abusive activities.
|
||||
- Overload or disrupt our servers, networks, or other customers’ services (e.g., DDoS attacks).
|
||||
- Violate intellectual property rights or privacy laws.
|
||||
|
||||
We reserve the right to suspend or terminate your Services without notice if we detect violations, subject to applicable law.
|
||||
|
||||
---
|
||||
|
||||
## 7. Payment and Billing
|
||||
- **Fees**: You agree to pay the fees for your chosen plan as outlined at checkout. All prices are in Euro (€) and include VAT where applicable.
|
||||
- **Billing Cycle**: Payments are due in advance (monthly, quarterly, or annually, depending on your plan).
|
||||
## 7. Payment and Billing
|
||||
|
||||
- **Fees**: You agree to pay the fees for your chosen plan as outlined at checkout. All prices are in Euro (€) and include VAT where applicable.
|
||||
- **Billing Cycle**: Payments are due in advance (monthly, quarterly, or annually, depending on your plan).
|
||||
- **Late Payment**: Overdue accounts may be suspended until payment is received.
|
||||
- **Refunds**: Refunds are available within 7 days of initial purchase, provided no excessive usage has occurred, as determined by us.
|
||||
- **Refunds**: Refunds are available within 7 days of initial purchase, provided no excessive usage has occurred, as determined by us.
|
||||
|
||||
---
|
||||
|
||||
## 8. Termination
|
||||
- **By You**: You may terminate your account at any time via your control panel or by contacting us, subject to the billing cycle terms.
|
||||
- **By Us**: We may suspend or terminate your Services for non-payment, violation of these Terms, or if required by law, with or without notice depending on the severity of the breach.
|
||||
- **Effect of Termination**: Upon termination, your access to the Services ends, and we may delete your data after 7 days unless otherwise required by law.
|
||||
## 8. Termination
|
||||
|
||||
- **By You**: You may terminate your account at any time via your control panel or by contacting us, subject to the billing cycle terms.
|
||||
- **By Us**: We may suspend or terminate your Services for non-payment, violation of these Terms, or if required by law, with or without notice depending on the severity of the breach.
|
||||
- **Effect of Termination**: Upon termination, your access to the Services ends, and we may delete your data after 7 days unless otherwise required by law.
|
||||
|
||||
---
|
||||
|
||||
## 9. Data and Privacy
|
||||
- **Your Data**: You retain ownership of data uploaded to your VPS. We do not access it except as needed to provide the Services or comply with legal obligations.
|
||||
- **Backups**: You are responsible for maintaining backups of your data unless a backup service is included in your plan.
|
||||
- **GDPR Compliance**: We process personal data in accordance with our [Privacy Policy](#), which complies with the General Data Protection Regulation (GDPR).
|
||||
## 9. Data and Privacy
|
||||
|
||||
- **Your Data**: You retain ownership of data uploaded to your VPS. We do not access it except as needed to provide the Services or comply with legal obligations.
|
||||
- **Backups**: You are responsible for maintaining backups of your data unless a backup service is included in your plan.
|
||||
- **GDPR Compliance**: We process personal data in accordance with our [Privacy Policy](#), which complies with the General Data Protection Regulation (GDPR).
|
||||
|
||||
---
|
||||
|
||||
## 10. Limitation of Liability
|
||||
To the fullest extent permitted by Irish law:
|
||||
- Our liability for any claim arising from the Services is limited to the amount you paid us in the previous 12 months.
|
||||
- We are not liable for indirect, consequential, or incidental damages (e.g., loss of profits, data, or business opportunities).
|
||||
- We are not responsible for issues beyond our reasonable control, such as force majeure events (e.g., natural disasters, cyber-attacks).
|
||||
## 10. Limitation of Liability
|
||||
|
||||
To the fullest extent permitted by Irish law:
|
||||
|
||||
- Our liability for any claim arising from the Services is limited to the amount you paid us in the previous 12 months.
|
||||
- We are not liable for indirect, consequential, or incidental damages (e.g., loss of profits, data, or business opportunities).
|
||||
- We are not responsible for issues beyond our reasonable control, such as force majeure events (e.g., natural disasters, cyber-attacks).
|
||||
|
||||
---
|
||||
|
||||
## 11. Intellectual Property
|
||||
- **Our IP**: The LNVPS website, branding, and software remain our property or that of our licensors.
|
||||
- **Your IP**: You grant us a limited license to use your content solely to provide the Services.
|
||||
## 11. Intellectual Property
|
||||
|
||||
- **Our IP**: The LNVPS website, branding, and software remain our property or that of our licensors.
|
||||
- **Your IP**: You grant us a limited license to use your content solely to provide the Services.
|
||||
|
||||
---
|
||||
|
||||
## 12. Governing Law and Dispute Resolution
|
||||
- These Terms are governed by the laws of Ireland.
|
||||
- Any disputes will be subject to the exclusive jurisdiction of the courts of Ireland, though you may have additional rights under EU consumer law if applicable.
|
||||
## 12. Governing Law and Dispute Resolution
|
||||
|
||||
- These Terms are governed by the laws of Ireland.
|
||||
- Any disputes will be subject to the exclusive jurisdiction of the courts of Ireland, though you may have additional rights under EU consumer law if applicable.
|
||||
|
||||
---
|
||||
|
||||
## 13. Changes to Terms
|
||||
## 13. Changes to Terms
|
||||
|
||||
We may update these Terms from time to time. We will notify you of significant changes via email or on our website. Continued use of the Services after changes constitutes acceptance of the updated Terms.
|
||||
|
||||
---
|
||||
|
||||
## 14. Contact Us
|
||||
For questions or support:
|
||||
## 14. Contact Us
|
||||
|
||||
For questions or support:
|
||||
|
||||
- **Email**: sales@lnvps.net
|
||||
- **Address**: Suite 10628, 26/27 Upper Pembroke Street, Dublin 2, D02 X361, Ireland
|
||||
|
||||
---
|
||||
---
|
||||
|
Reference in New Issue
Block a user