2 Commits

Author SHA1 Message Date
Alejandro Gutiérrez
533dcc11f6 fix(web): remove turbostarter CTA popup + ship claudemesh OG image
Some checks failed
CI / Tests / 🧪 Test (push) Has been cancelled
Two visible launch-polish issues:

1. BuyCtaDialog popup was firing on an exponential backoff schedule
   (15s, 30s, 60s, …) pushing users toward turbostarter.dev/#pricing +
   Discord. Wrong product, wrong audience. Fully removed: mount point
   in [locale]/layout.tsx + the component file + localStorage keys will
   self-prune on next visit.

2. WhatsApp/Slack/Twitter link previews were pulling the TurboStarter
   boilerplate opengraph-image.png (from Jan 8). Replaced with a 1200×630
   claudemesh OG: "CLAUDEMESH" pixel wordmark left side, hero mesh
   composition (6 Claude Code terminals + pixel-crab hub + orange
   energy lattice + vaporwave grid floor) right side, "peer mesh for
   Claude Code sessions" tagline in mono beneath wordmark.

3. Default metadata description swapped from the dangling
   `common:product.description` i18n key (which rendered as the key
   itself because the key doesn't exist in our trimmed translations)
   to a hardcoded claudemesh description.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 23:11:34 +01:00
Alejandro Gutiérrez
fa23525c46 feat(broker): one-off owner_pubkey backfill script
Populates mesh.mesh.owner_pubkey for pre-18c rows by generating a
fresh ed25519 keypair per mesh + emitting the secret key to stdout
for out-of-band hand-off.

Idempotent: only patches rows WHERE owner_pubkey IS NULL. Machine-
readable output (tab-separated: mesh_id, slug, pubkey, secret_key)
so operators can pipe into a secure store.

Usage:
  DATABASE_URL=... bun apps/broker/scripts/backfill-owner-pubkey.ts > owners.tsv
  # then securely distribute secrets to mesh owners

Verified locally: nulled smoke-test mesh's owner_pubkey → ran backfill
→ fresh keypair written, secret emitted.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 23:08:07 +01:00
5 changed files with 66 additions and 122 deletions

View File

@@ -0,0 +1,65 @@
#!/usr/bin/env bun
/**
* One-off backfill: populate `mesh.mesh.owner_pubkey` for meshes
* created before Step 18c landed.
*
* Runs idempotently: only touches rows where owner_pubkey IS NULL.
* Generates a fresh ed25519 keypair per mesh and writes the owner
* SECRET KEY to stdout (paired with mesh_id) so an operator can
* hand it back to the mesh owner out-of-band.
*
* Usage:
* DATABASE_URL=... bun apps/broker/scripts/backfill-owner-pubkey.ts
*
* Output format (per row): `<mesh_id> <mesh_slug> <owner_pubkey> <owner_secret_key>`
* Redirect stdout to a secure file — the secret keys grant admin
* invite-signing power and must be stored carefully.
*/
import sodium from "libsodium-wrappers";
import { eq, isNull } from "drizzle-orm";
import { db } from "../src/db";
import { mesh } from "@turbostarter/db/schema/mesh";
async function main(): Promise<void> {
await sodium.ready;
const missing = await db
.select({ id: mesh.id, slug: mesh.slug, name: mesh.name })
.from(mesh)
.where(isNull(mesh.ownerPubkey));
if (missing.length === 0) {
console.error("[backfill] no rows to patch");
return;
}
console.error(`[backfill] patching ${missing.length} mesh(es)`);
for (const row of missing) {
const kp = sodium.crypto_sign_keypair();
const pubHex = sodium.to_hex(kp.publicKey);
const secHex = sodium.to_hex(kp.privateKey);
await db
.update(mesh)
.set({ ownerPubkey: pubHex })
.where(eq(mesh.id, row.id));
// stdout: machine-readable, one mesh per line
console.log(`${row.id}\t${row.slug}\t${pubHex}\t${secHex}`);
console.error(
`[backfill] patched mesh "${row.slug}" (${row.id}) — save its secret key`,
);
}
console.error(
"[backfill] done. SECURELY HAND OFF secret keys to mesh owners.",
);
}
main()
.then(() => process.exit(0))
.catch((e) => {
console.error(
"[backfill] error:",
e instanceof Error ? e.message : String(e),
);
process.exit(1);
});

View File

@@ -7,7 +7,6 @@ import { Providers } from "~/lib/providers/providers";
import { ImpersonatingBanner } from "~/modules/admin/users/user/impersonating-banner"; import { ImpersonatingBanner } from "~/modules/admin/users/user/impersonating-banner";
import { BaseLayout } from "~/modules/common/layout/base"; import { BaseLayout } from "~/modules/common/layout/base";
import { Toaster } from "~/modules/common/toast"; import { Toaster } from "~/modules/common/toast";
import { BuyCtaDialog } from "~/modules/marketing/layout/buy-cta-dialog";
export function generateStaticParams() { export function generateStaticParams() {
return config.locales.map((locale) => ({ locale })); return config.locales.map((locale) => ({ locale }));
@@ -33,7 +32,6 @@ export default async function RootLayout({
<Providers locale={locale}> <Providers locale={locale}>
<ImpersonatingBanner /> <ImpersonatingBanner />
{children} {children}
<BuyCtaDialog />
<Toaster /> <Toaster />
</Providers> </Providers>
</BaseLayout> </BaseLayout>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 947 KiB

View File

@@ -49,7 +49,7 @@ export const getMetadata =
( (
{ {
title, title,
description = "common:product.description", description = "Connect your Claude Code sessions to each other. Zero config. End-to-end encrypted. Peer mesh for Claude Code teams.",
url, url,
canonical, canonical,
images = [DEFAULT_IMAGE], images = [DEFAULT_IMAGE],

View File

@@ -1,119 +0,0 @@
"use client";
import { useEffect, useRef, useState } from "react";
import { useTranslation } from "@turbostarter/i18n";
import { cn } from "@turbostarter/ui";
import { buttonVariants } from "@turbostarter/ui-web/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@turbostarter/ui-web/dialog";
import { Icons } from "@turbostarter/ui-web/icons";
const MIN_DELAY_MS = 15_000;
const STORAGE_LAST_SHOWN_AT = "buyCtaDialog:lastShownAt";
const STORAGE_PREV_DELAY_MS = "buyCtaDialog:prevDelayMs";
export const BuyCtaDialog = () => {
const { t } = useTranslation(["common", "marketing"]);
const [open, setOpen] = useState(false);
const timeoutIdRef = useRef<number | null>(null);
useEffect(() => {
const scheduleNext = () => {
const now = Date.now();
const storedLastShown = Number(
window.localStorage.getItem(STORAGE_LAST_SHOWN_AT) ?? "0",
);
const prevDelayMs = Number(
window.localStorage.getItem(STORAGE_PREV_DELAY_MS) ?? "0",
);
const nextDelay = Math.max(
MIN_DELAY_MS,
prevDelayMs ? prevDelayMs * 2 : MIN_DELAY_MS,
);
const baseNextShow = storedLastShown
? storedLastShown + nextDelay
: now + nextDelay;
const delayFromNow = Math.max(MIN_DELAY_MS, baseNextShow - now);
if (timeoutIdRef.current) {
window.clearTimeout(timeoutIdRef.current);
}
timeoutIdRef.current = window.setTimeout(() => {
setOpen(true);
const shownAt = Date.now();
window.localStorage.setItem(STORAGE_LAST_SHOWN_AT, String(shownAt));
window.localStorage.setItem(STORAGE_PREV_DELAY_MS, String(nextDelay));
scheduleNext();
}, delayFromNow);
};
scheduleNext();
return () => {
if (timeoutIdRef.current) {
window.clearTimeout(timeoutIdRef.current);
}
};
}, []);
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="max-w-md">
<DialogHeader className="space-y-3">
<DialogTitle>{t("cta.buy.question")}</DialogTitle>
<DialogDescription className="text-foreground text-base">
{t("cta.buy.description")}
</DialogDescription>
</DialogHeader>
<a
href="https://turbostarter.dev/#pricing"
target="_blank"
rel="noopener noreferrer"
className={cn(buttonVariants(), "gap-2")}
>
<Icons.Code className="size-4" />
{t("cta.buy.button")}
</a>
<div className="bg-border relative -mx-6 my-3 h-px">
<span className="bg-background text-muted-foreground absolute left-1/2 -translate-x-1/2 -translate-y-1/2 px-3 text-sm">
{t("or")}
</span>
</div>
<div className="flex flex-col gap-4">
<p>{t("cta.buy.join.description")}</p>
<a
className={cn(
buttonVariants(),
"gap-2 bg-[#5865F2] px-7 no-underline hover:bg-[#5865F2]/95",
)}
href="https://discord.gg/KjpK2uk3JP"
rel="noopener noreferrer"
target="_blank"
>
<Icons.Discord className="size-[1.35rem] text-white" />
<span className="font-semibold text-white">
{t("cta.buy.join.button")}
</span>
</a>
</div>
</DialogContent>
</Dialog>
);
};