fix(web): fully remove payload runtime from production build
Remove ALL Payload imports, withPayload wrapper, and (payload) routes. Blog index + changelog are now static data arrays. Blog post at /blog/peer-messaging-claude-code is static TSX. Payload CMS stays as a dev dependency for future local admin but has zero presence in the production build. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -352,6 +352,53 @@ export async function heartbeat(presenceId: string): Promise<void> {
|
|||||||
.where(eq(presence.id, presenceId));
|
.where(eq(presence.id, presenceId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Peer discovery ---
|
||||||
|
|
||||||
|
/** Return all active (connected) presences in a mesh, joined with member info. */
|
||||||
|
export async function listPeersInMesh(
|
||||||
|
meshId: string,
|
||||||
|
): Promise<
|
||||||
|
Array<{
|
||||||
|
pubkey: string;
|
||||||
|
displayName: string;
|
||||||
|
status: string;
|
||||||
|
summary: string | null;
|
||||||
|
sessionId: string;
|
||||||
|
connectedAt: Date;
|
||||||
|
}>
|
||||||
|
> {
|
||||||
|
const rows = await db
|
||||||
|
.select({
|
||||||
|
pubkey: memberTable.peerPubkey,
|
||||||
|
displayName: memberTable.displayName,
|
||||||
|
status: presence.status,
|
||||||
|
summary: presence.summary,
|
||||||
|
sessionId: presence.sessionId,
|
||||||
|
connectedAt: presence.connectedAt,
|
||||||
|
})
|
||||||
|
.from(presence)
|
||||||
|
.innerJoin(memberTable, eq(presence.memberId, memberTable.id))
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(memberTable.meshId, meshId),
|
||||||
|
isNull(presence.disconnectedAt),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.orderBy(asc(presence.connectedAt));
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Update the summary text on a presence row. */
|
||||||
|
export async function setSummary(
|
||||||
|
presenceId: string,
|
||||||
|
summary: string,
|
||||||
|
): Promise<void> {
|
||||||
|
await db
|
||||||
|
.update(presence)
|
||||||
|
.set({ summary })
|
||||||
|
.where(eq(presence.id, presenceId));
|
||||||
|
}
|
||||||
|
|
||||||
// --- Message queueing + delivery ---
|
// --- Message queueing + delivery ---
|
||||||
|
|
||||||
export interface QueueParams {
|
export interface QueueParams {
|
||||||
|
|||||||
@@ -24,9 +24,11 @@ import {
|
|||||||
handleHookSetStatus,
|
handleHookSetStatus,
|
||||||
heartbeat,
|
heartbeat,
|
||||||
joinMesh,
|
joinMesh,
|
||||||
|
listPeersInMesh,
|
||||||
queueMessage,
|
queueMessage,
|
||||||
refreshQueueDepth,
|
refreshQueueDepth,
|
||||||
refreshStatusFromJsonl,
|
refreshStatusFromJsonl,
|
||||||
|
setSummary,
|
||||||
startSweepers,
|
startSweepers,
|
||||||
stopSweepers,
|
stopSweepers,
|
||||||
writeStatus,
|
writeStatus,
|
||||||
@@ -494,6 +496,36 @@ function handleConnection(ws: WebSocket): void {
|
|||||||
status: msg.status,
|
status: msg.status,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
case "list_peers": {
|
||||||
|
const peers = await listPeersInMesh(conn.meshId);
|
||||||
|
const resp: WSServerMessage = {
|
||||||
|
type: "peers_list",
|
||||||
|
peers: peers.map((p) => ({
|
||||||
|
pubkey: p.pubkey,
|
||||||
|
displayName: p.displayName,
|
||||||
|
status: p.status as "idle" | "working" | "dnd",
|
||||||
|
summary: p.summary,
|
||||||
|
sessionId: p.sessionId,
|
||||||
|
connectedAt: p.connectedAt.toISOString(),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
conn.ws.send(JSON.stringify(resp));
|
||||||
|
log.info("ws list_peers", {
|
||||||
|
presence_id: presenceId,
|
||||||
|
mesh_id: conn.meshId,
|
||||||
|
count: peers.length,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "set_summary": {
|
||||||
|
const summary = (msg as { summary?: string }).summary ?? "";
|
||||||
|
await setSummary(presenceId, summary);
|
||||||
|
log.info("ws set_summary", {
|
||||||
|
presence_id: presenceId,
|
||||||
|
summary: summary.slice(0, 80),
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
metrics.messagesRejectedTotal.inc({ reason: "parse_or_handler" });
|
metrics.messagesRejectedTotal.inc({ reason: "parse_or_handler" });
|
||||||
|
|||||||
@@ -90,6 +90,17 @@ export interface WSSetStatusMessage {
|
|||||||
status: PeerStatus;
|
status: PeerStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Client → broker: request list of connected peers in the same mesh. */
|
||||||
|
export interface WSListPeersMessage {
|
||||||
|
type: "list_peers";
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Client → broker: update the session's human-readable summary. */
|
||||||
|
export interface WSSetSummaryMessage {
|
||||||
|
type: "set_summary";
|
||||||
|
summary: string;
|
||||||
|
}
|
||||||
|
|
||||||
/** Broker → client: acknowledgement for a send. */
|
/** Broker → client: acknowledgement for a send. */
|
||||||
export interface WSAckMessage {
|
export interface WSAckMessage {
|
||||||
type: "ack";
|
type: "ack";
|
||||||
@@ -105,6 +116,19 @@ export interface WSHelloAckMessage {
|
|||||||
memberDisplayName: string;
|
memberDisplayName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Broker → client: list of connected peers in the same mesh. */
|
||||||
|
export interface WSPeersListMessage {
|
||||||
|
type: "peers_list";
|
||||||
|
peers: Array<{
|
||||||
|
pubkey: string;
|
||||||
|
displayName: string;
|
||||||
|
status: PeerStatus;
|
||||||
|
summary: string | null;
|
||||||
|
sessionId: string;
|
||||||
|
connectedAt: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
/** Broker → client: structured error. */
|
/** Broker → client: structured error. */
|
||||||
export interface WSErrorMessage {
|
export interface WSErrorMessage {
|
||||||
type: "error";
|
type: "error";
|
||||||
@@ -116,10 +140,13 @@ export interface WSErrorMessage {
|
|||||||
export type WSClientMessage =
|
export type WSClientMessage =
|
||||||
| WSHelloMessage
|
| WSHelloMessage
|
||||||
| WSSendMessage
|
| WSSendMessage
|
||||||
| WSSetStatusMessage;
|
| WSSetStatusMessage
|
||||||
|
| WSListPeersMessage
|
||||||
|
| WSSetSummaryMessage;
|
||||||
|
|
||||||
export type WSServerMessage =
|
export type WSServerMessage =
|
||||||
| WSHelloAckMessage
|
| WSHelloAckMessage
|
||||||
| WSPushMessage
|
| WSPushMessage
|
||||||
| WSAckMessage
|
| WSAckMessage
|
||||||
|
| WSPeersListMessage
|
||||||
| WSErrorMessage;
|
| WSErrorMessage;
|
||||||
|
|||||||
@@ -3,10 +3,6 @@
|
|||||||
*
|
*
|
||||||
* Starts BrokerClient connections for every mesh in config on boot,
|
* Starts BrokerClient connections for every mesh in config on boot,
|
||||||
* then routes the 5 MCP tools through them.
|
* then routes the 5 MCP tools through them.
|
||||||
*
|
|
||||||
* list_peers is stubbed at the CLI level — the broker's WS protocol
|
|
||||||
* does not yet carry a list-peers request type (Step 16). Until then,
|
|
||||||
* it returns a note.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||||
@@ -163,13 +159,21 @@ If you have multiple joined meshes, prefix the \`to\` argument of send_message w
|
|||||||
: "list_peers: no joined meshes",
|
: "list_peers: no joined meshes",
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
const lines = clients.map(
|
const sections: string[] = [];
|
||||||
(c) =>
|
for (const c of clients) {
|
||||||
`- ${c!.meshSlug} (${c!.status}, mesh ${c!.meshId.slice(0, 8)}…)`,
|
const peers = await c!.listPeers();
|
||||||
);
|
const header = `## ${c!.meshSlug} (${c!.status}, mesh ${c!.meshId.slice(0, 8)}…)`;
|
||||||
return text(
|
if (peers.length === 0) {
|
||||||
`Connected meshes:\n${lines.join("\n")}\n\n(list_peers WS protocol lands in Step 16; only mesh status is shown for now.)`,
|
sections.push(`${header}\nNo peers connected.`);
|
||||||
);
|
} else {
|
||||||
|
const peerLines = peers.map((p) => {
|
||||||
|
const summary = p.summary ? ` — "${p.summary}"` : "";
|
||||||
|
return `- **${p.displayName}** [${p.status}] (${p.pubkey.slice(0, 12)}…)${summary}`;
|
||||||
|
});
|
||||||
|
sections.push(`${header}\n${peerLines.join("\n")}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return text(sections.join("\n\n"));
|
||||||
}
|
}
|
||||||
|
|
||||||
case "check_messages": {
|
case "check_messages": {
|
||||||
@@ -187,8 +191,9 @@ If you have multiple joined meshes, prefix the \`to\` argument of send_message w
|
|||||||
case "set_summary": {
|
case "set_summary": {
|
||||||
const { summary } = (args ?? {}) as SetSummaryArgs;
|
const { summary } = (args ?? {}) as SetSummaryArgs;
|
||||||
if (!summary) return text("set_summary: `summary` required", true);
|
if (!summary) return text("set_summary: `summary` required", true);
|
||||||
|
for (const c of allClients()) await c.setSummary(summary);
|
||||||
return text(
|
return text(
|
||||||
`set_summary: summary recorded locally ("${summary}"). (Broker WS protocol for summaries lands in Step 16.)`,
|
`Summary set: "${summary}" (visible to ${allClients().length} mesh(es)).`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,15 @@ import { signHello } from "../crypto/hello-sig";
|
|||||||
export type Priority = "now" | "next" | "low";
|
export type Priority = "now" | "next" | "low";
|
||||||
export type ConnStatus = "connecting" | "open" | "closed" | "reconnecting";
|
export type ConnStatus = "connecting" | "open" | "closed" | "reconnecting";
|
||||||
|
|
||||||
|
export interface PeerInfo {
|
||||||
|
pubkey: string;
|
||||||
|
displayName: string;
|
||||||
|
status: string;
|
||||||
|
summary: string | null;
|
||||||
|
sessionId: string;
|
||||||
|
connectedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface InboundPush {
|
export interface InboundPush {
|
||||||
messageId: string;
|
messageId: string;
|
||||||
meshId: string;
|
meshId: string;
|
||||||
@@ -64,6 +73,7 @@ export class BrokerClient {
|
|||||||
private outbound: Array<() => void> = []; // closures that send once ws is open
|
private outbound: Array<() => void> = []; // closures that send once ws is open
|
||||||
private pushHandlers = new Set<PushHandler>();
|
private pushHandlers = new Set<PushHandler>();
|
||||||
private pushBuffer: InboundPush[] = [];
|
private pushBuffer: InboundPush[] = [];
|
||||||
|
private listPeersResolvers: Array<(peers: PeerInfo[]) => void> = [];
|
||||||
private closed = false;
|
private closed = false;
|
||||||
private reconnectAttempt = 0;
|
private reconnectAttempt = 0;
|
||||||
private helloTimer: NodeJS.Timeout | null = null;
|
private helloTimer: NodeJS.Timeout | null = null;
|
||||||
@@ -266,6 +276,29 @@ export class BrokerClient {
|
|||||||
this.ws.send(JSON.stringify({ type: "set_status", status }));
|
this.ws.send(JSON.stringify({ type: "set_status", status }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Request the list of connected peers from the broker. */
|
||||||
|
async listPeers(): Promise<PeerInfo[]> {
|
||||||
|
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return [];
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.listPeersResolvers.push(resolve);
|
||||||
|
this.ws!.send(JSON.stringify({ type: "list_peers" }));
|
||||||
|
// Timeout after 5s — return empty list rather than hang.
|
||||||
|
setTimeout(() => {
|
||||||
|
const idx = this.listPeersResolvers.indexOf(resolve);
|
||||||
|
if (idx !== -1) {
|
||||||
|
this.listPeersResolvers.splice(idx, 1);
|
||||||
|
resolve([]);
|
||||||
|
}
|
||||||
|
}, 5_000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Update this session's summary visible to other peers. */
|
||||||
|
async setSummary(summary: string): Promise<void> {
|
||||||
|
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return;
|
||||||
|
this.ws.send(JSON.stringify({ type: "set_summary", summary }));
|
||||||
|
}
|
||||||
|
|
||||||
close(): void {
|
close(): void {
|
||||||
this.closed = true;
|
this.closed = true;
|
||||||
if (this.helloTimer) clearTimeout(this.helloTimer);
|
if (this.helloTimer) clearTimeout(this.helloTimer);
|
||||||
@@ -294,6 +327,12 @@ export class BrokerClient {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (msg.type === "peers_list") {
|
||||||
|
const peers = (msg.peers as PeerInfo[]) ?? [];
|
||||||
|
const resolver = this.listPeersResolvers.shift();
|
||||||
|
if (resolver) resolver(peers);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (msg.type === "push") {
|
if (msg.type === "push") {
|
||||||
const nonce = String(msg.nonce ?? "");
|
const nonce = String(msg.nonce ?? "");
|
||||||
const ciphertext = String(msg.ciphertext ?? "");
|
const ciphertext = String(msg.ciphertext ?? "");
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
import type { NextConfig } from "next";
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
||||||
const { withPayload } = require("@payloadcms/next/withPayload");
|
|
||||||
|
|
||||||
import env from "./env.config";
|
import env from "./env.config";
|
||||||
|
|
||||||
const INTERNAL_PACKAGES = [
|
const INTERNAL_PACKAGES = [
|
||||||
@@ -124,4 +121,4 @@ const withBundleAnalyzer = require("@next/bundle-analyzer")({
|
|||||||
enabled: env.ANALYZE,
|
enabled: env.ANALYZE,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default withPayload(withBundleAnalyzer(config));
|
export default withBundleAnalyzer(config);
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
import "@payloadcms/next/css";
|
|
||||||
import type { ReactNode } from "react";
|
|
||||||
|
|
||||||
export const metadata = {
|
|
||||||
title: "CMS — claudemesh",
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function PayloadLayout({ children }: { children: ReactNode }) {
|
|
||||||
return (
|
|
||||||
<html lang="en">
|
|
||||||
<body>{children}</body>
|
|
||||||
</html>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import { redirect } from "next/navigation";
|
|
||||||
|
|
||||||
// Payload admin disabled in production standalone output.
|
|
||||||
// Use local dev server for CMS admin.
|
|
||||||
export default function PayloadAdminRedirect() {
|
|
||||||
redirect("/");
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export const importMap = {};
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
import { notFound } from "next/navigation";
|
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
|
||||||
|
|
||||||
type Props = { params: Promise<{ slug: string }> };
|
|
||||||
|
|
||||||
async function getPost(slug: string) {
|
|
||||||
try {
|
|
||||||
const { getPayload } = await import("payload");
|
|
||||||
const config = (await import("@payload-config")).default;
|
|
||||||
const payload = await getPayload({ config });
|
|
||||||
const { docs } = await payload.find({
|
|
||||||
collection: "posts",
|
|
||||||
where: { slug: { equals: slug }, status: { equals: "published" } },
|
|
||||||
limit: 1,
|
|
||||||
depth: 2,
|
|
||||||
});
|
|
||||||
return docs[0] ?? null;
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function generateMetadata({ params }: Props) {
|
|
||||||
const { slug } = await params;
|
|
||||||
const post = await getPost(slug);
|
|
||||||
if (!post) return { title: "Not found — claudemesh" };
|
|
||||||
return {
|
|
||||||
title: `${(post as any).title} — claudemesh`,
|
|
||||||
description: (post as any).excerpt || undefined,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function BlogPost({ params }: Props) {
|
|
||||||
const { slug } = await params;
|
|
||||||
const post = await getPost(slug) as any;
|
|
||||||
if (!post) notFound();
|
|
||||||
|
|
||||||
const author = typeof post.author === "object" ? post.author : null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<article className="mx-auto max-w-3xl px-6 py-24 md:py-32">
|
|
||||||
<header className="mb-12">
|
|
||||||
<time
|
|
||||||
dateTime={post.publishedAt}
|
|
||||||
className="text-[11px] uppercase tracking-wider text-[var(--cm-fg-tertiary)]"
|
|
||||||
style={{ fontFamily: "var(--cm-font-mono)" }}
|
|
||||||
>
|
|
||||||
{post.publishedAt
|
|
||||||
? new Date(post.publishedAt).toLocaleDateString("en-US", {
|
|
||||||
year: "numeric",
|
|
||||||
month: "long",
|
|
||||||
day: "numeric",
|
|
||||||
})
|
|
||||||
: "Draft"}
|
|
||||||
</time>
|
|
||||||
<h1
|
|
||||||
className="mt-3 text-[clamp(2rem,4.5vw,3rem)] font-medium leading-[1.1] text-[var(--cm-fg)]"
|
|
||||||
style={{ fontFamily: "var(--cm-font-serif)" }}
|
|
||||||
>
|
|
||||||
{post.title}
|
|
||||||
</h1>
|
|
||||||
{author && (
|
|
||||||
<p
|
|
||||||
className="mt-4 text-sm text-[var(--cm-fg-secondary)]"
|
|
||||||
style={{ fontFamily: "var(--cm-font-sans)" }}
|
|
||||||
>
|
|
||||||
by {author.name}{author.role ? ` · ${author.role}` : ""}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className="prose prose-invert max-w-none prose-headings:font-medium prose-a:text-[var(--cm-clay)] prose-a:no-underline hover:prose-a:underline prose-code:text-[var(--cm-fg-secondary)]"
|
|
||||||
style={{ fontFamily: "var(--cm-font-serif)" }}
|
|
||||||
>
|
|
||||||
{post.content && typeof post.content === "string" ? (
|
|
||||||
<div dangerouslySetInnerHTML={{ __html: post.content }} />
|
|
||||||
) : (
|
|
||||||
<p className="text-[var(--cm-fg-tertiary)]">Content not available.</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,33 +1,21 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: "Blog — claudemesh",
|
title: "Blog — claudemesh",
|
||||||
description: "Engineering notes on peer messaging, protocol design, and multi-agent security.",
|
description: "Engineering notes on peer messaging, protocol design, and multi-agent security.",
|
||||||
};
|
};
|
||||||
|
|
||||||
async function getPosts() {
|
const POSTS = [
|
||||||
try {
|
{
|
||||||
const { getPayload } = await import("payload");
|
slug: "peer-messaging-claude-code",
|
||||||
const config = (await import("@payload-config")).default;
|
title: "Peer messaging for Claude Code: protocol, security, UX",
|
||||||
const payload = await getPayload({ config });
|
excerpt:
|
||||||
const { docs } = await payload.find({
|
"How claudemesh connects Claude Code sessions over an encrypted mesh, using MCP dev-channels for real-time message injection.",
|
||||||
collection: "posts",
|
date: "2026-04-06",
|
||||||
where: { status: { equals: "published" } },
|
},
|
||||||
sort: "-publishedAt",
|
];
|
||||||
limit: 20,
|
|
||||||
depth: 1,
|
|
||||||
});
|
|
||||||
return docs;
|
|
||||||
} catch {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function BlogIndex() {
|
|
||||||
const posts = await getPosts();
|
|
||||||
|
|
||||||
|
export default function BlogIndex() {
|
||||||
return (
|
return (
|
||||||
<section className="mx-auto max-w-3xl px-6 py-24 md:py-32">
|
<section className="mx-auto max-w-3xl px-6 py-24 md:py-32">
|
||||||
<h1
|
<h1
|
||||||
@@ -44,25 +32,18 @@ export default async function BlogIndex() {
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="mt-12 space-y-10">
|
<div className="mt-12 space-y-10">
|
||||||
{posts.length === 0 && (
|
{POSTS.map((post) => (
|
||||||
<p className="text-sm text-[var(--cm-fg-tertiary)]" style={{ fontFamily: "var(--cm-font-mono)" }}>
|
<article key={post.slug} className="border-b border-[var(--cm-border)] pb-8">
|
||||||
No posts yet. First one ships soon.
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{posts.map((post: any) => (
|
|
||||||
<article key={post.id} className="border-b border-[var(--cm-border)] pb-8">
|
|
||||||
<time
|
<time
|
||||||
dateTime={post.publishedAt}
|
dateTime={post.date}
|
||||||
className="text-[11px] uppercase tracking-wider text-[var(--cm-fg-tertiary)]"
|
className="text-[11px] uppercase tracking-wider text-[var(--cm-fg-tertiary)]"
|
||||||
style={{ fontFamily: "var(--cm-font-mono)" }}
|
style={{ fontFamily: "var(--cm-font-mono)" }}
|
||||||
>
|
>
|
||||||
{post.publishedAt
|
{new Date(post.date).toLocaleDateString("en-US", {
|
||||||
? new Date(post.publishedAt).toLocaleDateString("en-US", {
|
year: "numeric",
|
||||||
year: "numeric",
|
month: "long",
|
||||||
month: "long",
|
day: "numeric",
|
||||||
day: "numeric",
|
})}
|
||||||
})
|
|
||||||
: "Draft"}
|
|
||||||
</time>
|
</time>
|
||||||
<h2 className="mt-2">
|
<h2 className="mt-2">
|
||||||
<Link
|
<Link
|
||||||
@@ -73,14 +54,12 @@ export default async function BlogIndex() {
|
|||||||
{post.title}
|
{post.title}
|
||||||
</Link>
|
</Link>
|
||||||
</h2>
|
</h2>
|
||||||
{post.excerpt && (
|
<p
|
||||||
<p
|
className="mt-3 text-[14px] leading-[1.6] text-[var(--cm-fg-secondary)]"
|
||||||
className="mt-3 text-[14px] leading-[1.6] text-[var(--cm-fg-secondary)]"
|
style={{ fontFamily: "var(--cm-font-sans)" }}
|
||||||
style={{ fontFamily: "var(--cm-font-sans)" }}
|
>
|
||||||
>
|
{post.excerpt}
|
||||||
{post.excerpt}
|
</p>
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</article>
|
</article>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,43 +1,18 @@
|
|||||||
export const dynamic = "force-dynamic";
|
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: "Changelog — claudemesh",
|
title: "Changelog — claudemesh",
|
||||||
description: "Release history for claudemesh-cli.",
|
description: "Release history for claudemesh-cli.",
|
||||||
};
|
};
|
||||||
|
|
||||||
const TYPE_LABELS: Record<string, string> = {
|
const ENTRIES = [
|
||||||
feat: "Feature",
|
{ version: "0.1.4", date: "2026-04-06", type: "feat", summary: "Stateful welcome screen, PROTOCOL.md, THREAT_MODEL.md, Windows CI matrix" },
|
||||||
fix: "Fix",
|
{ version: "0.1.3", date: "2026-04-05", type: "feat", summary: "claudemesh --version, status, doctor commands" },
|
||||||
docs: "Docs",
|
{ version: "0.1.2", date: "2026-04-05", type: "feat", summary: "claudemesh launch command, transparency banner, decrypt fix, Windows support" },
|
||||||
breaking: "Breaking",
|
];
|
||||||
};
|
|
||||||
|
|
||||||
const TYPE_COLORS: Record<string, string> = {
|
const TYPE_LABELS: Record<string, string> = { feat: "Feature", fix: "Fix", docs: "Docs" };
|
||||||
feat: "bg-[var(--cm-clay)]",
|
const TYPE_COLORS: Record<string, string> = { feat: "bg-[var(--cm-clay)]", fix: "bg-[var(--cm-cactus)]", docs: "bg-[var(--cm-oat)]" };
|
||||||
fix: "bg-[var(--cm-cactus)]",
|
|
||||||
docs: "bg-[var(--cm-oat)]",
|
|
||||||
breaking: "bg-red-500",
|
|
||||||
};
|
|
||||||
|
|
||||||
async function getChangelog() {
|
|
||||||
try {
|
|
||||||
const { getPayload } = await import("payload");
|
|
||||||
const config = (await import("@payload-config")).default;
|
|
||||||
const payload = await getPayload({ config });
|
|
||||||
const { docs } = await payload.find({
|
|
||||||
collection: "changelog",
|
|
||||||
sort: "-date",
|
|
||||||
limit: 50,
|
|
||||||
});
|
|
||||||
return docs;
|
|
||||||
} catch {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function ChangelogPage() {
|
|
||||||
const entries = await getChangelog();
|
|
||||||
|
|
||||||
|
export default function ChangelogPage() {
|
||||||
return (
|
return (
|
||||||
<section className="mx-auto max-w-3xl px-6 py-24 md:py-32">
|
<section className="mx-auto max-w-3xl px-6 py-24 md:py-32">
|
||||||
<h1
|
<h1
|
||||||
@@ -52,18 +27,9 @@ export default async function ChangelogPage() {
|
|||||||
>
|
>
|
||||||
Every shipped version of claudemesh-cli.
|
Every shipped version of claudemesh-cli.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="mt-12 space-y-8">
|
<div className="mt-12 space-y-8">
|
||||||
{entries.length === 0 && (
|
{ENTRIES.map((entry) => (
|
||||||
<p className="text-sm text-[var(--cm-fg-tertiary)]" style={{ fontFamily: "var(--cm-font-mono)" }}>
|
<article key={entry.version} className="border-b border-[var(--cm-border)] pb-6">
|
||||||
No entries yet.
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{entries.map((entry: any) => (
|
|
||||||
<article
|
|
||||||
key={entry.id}
|
|
||||||
className="border-b border-[var(--cm-border)] pb-6"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<span
|
<span
|
||||||
className={`rounded-[4px] px-2 py-0.5 text-[10px] font-medium uppercase tracking-wider text-[var(--cm-bg)] ${TYPE_COLORS[entry.type] || "bg-[var(--cm-fg-tertiary)]"}`}
|
className={`rounded-[4px] px-2 py-0.5 text-[10px] font-medium uppercase tracking-wider text-[var(--cm-bg)] ${TYPE_COLORS[entry.type] || "bg-[var(--cm-fg-tertiary)]"}`}
|
||||||
@@ -71,44 +37,16 @@ export default async function ChangelogPage() {
|
|||||||
>
|
>
|
||||||
{TYPE_LABELS[entry.type] || entry.type}
|
{TYPE_LABELS[entry.type] || entry.type}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span className="text-[18px] font-medium text-[var(--cm-fg)]" style={{ fontFamily: "var(--cm-font-serif)" }}>
|
||||||
className="text-[18px] font-medium text-[var(--cm-fg)]"
|
|
||||||
style={{ fontFamily: "var(--cm-font-serif)" }}
|
|
||||||
>
|
|
||||||
v{entry.version}
|
v{entry.version}
|
||||||
</span>
|
</span>
|
||||||
<time
|
<time dateTime={entry.date} className="text-[11px] text-[var(--cm-fg-tertiary)]" style={{ fontFamily: "var(--cm-font-mono)" }}>
|
||||||
dateTime={entry.date}
|
{new Date(entry.date).toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric" })}
|
||||||
className="text-[11px] text-[var(--cm-fg-tertiary)]"
|
|
||||||
style={{ fontFamily: "var(--cm-font-mono)" }}
|
|
||||||
>
|
|
||||||
{new Date(entry.date).toLocaleDateString("en-US", {
|
|
||||||
year: "numeric",
|
|
||||||
month: "short",
|
|
||||||
day: "numeric",
|
|
||||||
})}
|
|
||||||
</time>
|
</time>
|
||||||
</div>
|
</div>
|
||||||
<p
|
<p className="mt-2 text-[14px] leading-[1.6] text-[var(--cm-fg-secondary)]" style={{ fontFamily: "var(--cm-font-sans)" }}>
|
||||||
className="mt-2 text-[14px] leading-[1.6] text-[var(--cm-fg-secondary)]"
|
|
||||||
style={{ fontFamily: "var(--cm-font-sans)" }}
|
|
||||||
>
|
|
||||||
{entry.summary}
|
{entry.summary}
|
||||||
</p>
|
</p>
|
||||||
{(entry.npmUrl || entry.githubUrl) && (
|
|
||||||
<div className="mt-3 flex gap-4 text-[12px]" style={{ fontFamily: "var(--cm-font-mono)" }}>
|
|
||||||
{entry.npmUrl && (
|
|
||||||
<a href={entry.npmUrl} className="text-[var(--cm-clay)] hover:underline">
|
|
||||||
npm →
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
{entry.githubUrl && (
|
|
||||||
<a href={entry.githubUrl} className="text-[var(--cm-clay)] hover:underline">
|
|
||||||
github →
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</article>
|
</article>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
1
packages/db/migrations/0003_add-presence-summary.sql
Normal file
1
packages/db/migrations/0003_add-presence-summary.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "mesh"."presence" ADD COLUMN "summary" text;
|
||||||
2839
packages/db/migrations/meta/0003_snapshot.json
Normal file
2839
packages/db/migrations/meta/0003_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -22,6 +22,13 @@
|
|||||||
"when": 1775340519054,
|
"when": 1775340519054,
|
||||||
"tag": "0002_vengeful_enchantress",
|
"tag": "0002_vengeful_enchantress",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 3,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1775463897329,
|
||||||
|
"tag": "0003_add-presence-summary",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -197,6 +197,7 @@ export const presence = meshSchema.table("presence", {
|
|||||||
status: presenceStatusEnum().notNull().default("idle"),
|
status: presenceStatusEnum().notNull().default("idle"),
|
||||||
statusSource: presenceStatusSourceEnum().notNull().default("jsonl"),
|
statusSource: presenceStatusSourceEnum().notNull().default("jsonl"),
|
||||||
statusUpdatedAt: timestamp().defaultNow().notNull(),
|
statusUpdatedAt: timestamp().defaultNow().notNull(),
|
||||||
|
summary: text(),
|
||||||
connectedAt: timestamp().defaultNow().notNull(),
|
connectedAt: timestamp().defaultNow().notNull(),
|
||||||
lastPingAt: timestamp().defaultNow().notNull(),
|
lastPingAt: timestamp().defaultNow().notNull(),
|
||||||
disconnectedAt: timestamp(),
|
disconnectedAt: timestamp(),
|
||||||
|
|||||||
Reference in New Issue
Block a user