fix headline rhythm + wider hero image + 10x faster deploys

HEADLINE:
- 3 balanced lines: 'Turn I'll donate' / 'into money' / 'in the bank.'
- Removed   that orphaned 'money' on its own line
- <br className='hidden lg:block'> controls breaks on desktop only

IMAGE:
- Hero container: max-w-5xl -> max-w-7xl (image 25% wider)
- Stat strip widened to match
- Much more of the gala scene visible, phone prominent

DEPLOY SPEED (deploy.sh):
- Persistent /opt/pnpl/ build dir (no temp dir creation/deletion)
- BuildKit with cache mounts (npm + .next/cache)
- No more docker builder prune / docker rmi (preserves cache!)
- Installed docker-buildx v0.31.1 on server
- Before: ~245s (4+ min)  After: ~29s (cached) / ~136s (first)
- Use: cd pledge-now-pay-later && bash deploy.sh
This commit is contained in:
2026-03-03 21:37:34 +08:00
parent c9301edbe8
commit f0b1cb2f3a
5 changed files with 173 additions and 27 deletions

View File

@@ -118,10 +118,79 @@ export default function (pi: ExtensionAPI) {
lastDeployed: null,
};
// ── State reconstruction from session ──
const reconstructState = (ctx: ExtensionContext) => {
// ── State reconstruction: DB first, session fallback ──
const reconstructFromDb = async () => {
try {
const sshBase = `ssh -o ConnectTimeout=5 -p ${DEPLOY_CONFIG.sshPort} ${DEPLOY_CONFIG.sshHost}`;
const pgContainer = "$(docker ps --format '{{.Names}}' | grep dokploy-postgres)";
const shipsResult = await pi.exec("bash", ["-c",
`${sshBase} "incus exec ${DEPLOY_CONFIG.container} -- bash -c 'docker exec ${pgContainer} psql -U dokploy -d calvana -t -A -F \\\"|||\\\" -c \\\"SELECT id, title, status, COALESCE(metric, chr(45)), created_at::text FROM ships ORDER BY id\\\"'" 2>/dev/null`
], { timeout: 15000 });
if (shipsResult.code === 0 && shipsResult.stdout.trim()) {
state.ships = [];
let maxId = 0;
for (const line of shipsResult.stdout.trim().split("\n")) {
if (!line.trim()) continue;
const parts = line.split("|||");
if (parts.length >= 5) {
const id = parseInt(parts[0]);
if (id > maxId) maxId = id;
state.ships.push({
id,
title: parts[1],
status: parts[2] as ShipStatus,
timestamp: parts[4],
metric: parts[3],
prLink: "#pr",
deployLink: "#deploy",
loomLink: "#loomclip",
});
}
}
state.nextShipId = maxId + 1;
}
const oopsResult = await pi.exec("bash", ["-c",
`${sshBase} "incus exec ${DEPLOY_CONFIG.container} -- bash -c 'docker exec ${pgContainer} psql -U dokploy -d calvana -t -A -F \\\"|||\\\" -c \\\"SELECT id, description, COALESCE(fix_time, chr(45)), COALESCE(commit_link, chr(35)), created_at::text FROM oops ORDER BY id\\\"'" 2>/dev/null`
], { timeout: 15000 });
if (oopsResult.code === 0 && oopsResult.stdout.trim()) {
state.oops = [];
let maxOopsId = 0;
for (const line of oopsResult.stdout.trim().split("\n")) {
if (!line.trim()) continue;
const parts = line.split("|||");
if (parts.length >= 4) {
const id = parseInt(parts[0]);
if (id > maxOopsId) maxOopsId = id;
state.oops.push({
id,
description: parts[1],
fixTime: parts[2],
commitLink: parts[3],
timestamp: parts[4] || "",
});
}
}
state.nextOopsId = maxOopsId + 1;
}
} catch {
// DB unavailable — fall through to session reconstruction
}
};
const reconstructState = async (ctx: ExtensionContext) => {
state = { ships: [], oops: [], nextShipId: 1, nextOopsId: 1, lastDeployed: null };
// Always try DB first — this is the source of truth
await reconstructFromDb();
// If DB returned data, we're done
if (state.ships.length > 0) return;
// Fallback: reconstruct from session history (when DB is unreachable)
for (const entry of ctx.sessionManager.getBranch()) {
if (entry.type !== "message") continue;
const msg = entry.message;
@@ -136,34 +205,38 @@ export default function (pi: ExtensionAPI) {
};
pi.on("session_start", async (_event, ctx) => {
reconstructState(ctx);
await reconstructState(ctx);
if (ctx.hasUI) {
const theme = ctx.ui.theme;
const shipCount = state.ships.length;
const shipped = state.ships.filter(s => s.status === "shipped").length;
ctx.ui.setStatus("calvana", theme.fg("dim", `🚀 ${shipped}/${shipCount} shipped`));
ctx.ui.setStatus("calvana", theme.fg("dim", `🚀 ${shipped}/${shipCount} shipped (DB)`));
}
});
pi.on("session_switch", async (_event, ctx) => reconstructState(ctx));
pi.on("session_fork", async (_event, ctx) => reconstructState(ctx));
pi.on("session_tree", async (_event, ctx) => reconstructState(ctx));
pi.on("session_switch", async (_event, ctx) => await reconstructState(ctx));
pi.on("session_fork", async (_event, ctx) => await reconstructState(ctx));
pi.on("session_tree", async (_event, ctx) => await reconstructState(ctx));
// ── Inject context so LLM knows about ship tracking ──
pi.on("before_agent_start", async (event, _ctx) => {
const shipContext = `
[Calvana Ship Log Extension Active]
You have access to these tools for tracking work:
- calvana_ship: Track shipping progress (add/update/list entries)
- calvana_oops: Log mistakes and fixes
- calvana_deploy: Push updates to the live site at https://${DEPLOY_CONFIG.domain}/live
[Calvana Ship Log Extension Active — DB-backed]
Ship log is persisted in PostgreSQL (calvana DB on dokploy-postgres).
State survives across sessions — the DB is the source of truth, not session history.
When you START working on a task, use calvana_ship to add or update it to "shipping".
When you COMPLETE a task, update it to "shipped" with a metric.
If something BREAKS, log it with calvana_oops.
After significant changes, use calvana_deploy to push updates live.
Tools:
- calvana_ship: Track shipping progress (add/update/list). Writes to DB.
- calvana_oops: Log mistakes and fixes. Writes to DB.
- calvana_deploy: Queries DB for ALL historical entries, generates HTML, deploys to https://${DEPLOY_CONFIG.domain}/live
Current ships: ${state.ships.length} (${state.ships.filter(s => s.status === "shipped").length} shipped)
Current oops: ${state.oops.length}
Rules:
- When you START working on a task, use calvana_ship to add or update it to "shipping".
- When you COMPLETE a task, update it to "shipped" with a metric.
- If something BREAKS, log it with calvana_oops.
- After significant changes, use calvana_deploy to push updates live.
- calvana_deploy reads from the DATABASE — it shows ALL ships ever, not just this session.
Current state from DB: ${state.ships.length} ships (${state.ships.filter(s => s.status === "shipped").length} shipped), ${state.oops.length} oops
`;
return {
systemPrompt: event.systemPrompt + shipContext,
@@ -358,6 +431,19 @@ Current oops: ${state.oops.length}
timestamp: now,
};
state.oops.push(entry);
// Persist to PostgreSQL
const oopsDesc = entry.description.replace(/'/g, "''");
const oopsTime = (entry.fixTime || "-").replace(/'/g, "''");
const oopsCommit = (entry.commitLink || "#commit").replace(/'/g, "''");
try {
const dbResult = await pi.exec("bash", ["-c",
`ssh -o ConnectTimeout=10 -p ${DEPLOY_CONFIG.sshPort} ${DEPLOY_CONFIG.sshHost} "incus exec ${DEPLOY_CONFIG.container} -- bash -c 'docker exec \$(docker ps --format {{.Names}} | grep dokploy-postgres) psql -U dokploy -d calvana -t -c \\\"INSERT INTO oops (description, fix_time, commit_link) VALUES (\\x27${oopsDesc}\\x27, \\x27${oopsTime}\\x27, \\x27${oopsCommit}\\x27) RETURNING id\\\"'"`
], { timeout: 15000 });
const dbId = parseInt((dbResult.stdout || "").trim());
if (dbId > 0) entry.id = dbId;
} catch { /* DB write failed, local state still updated */ }
return {
content: [{ type: "text", text: `Oops #${entry.id}: "${entry.description}" (fixed in ${entry.fixTime})` }],
details: { state: { ...state, oops: [...state.oops] } },
@@ -840,5 +926,15 @@ function escapeHtml(str: string): string {
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
.replace(/'/g, "&#39;")
.replace(/\u2014/g, "&mdash;") // em dash
.replace(/\u2013/g, "&ndash;") // en dash
.replace(/\u2019/g, "&rsquo;") // right single quote
.replace(/\u2018/g, "&lsquo;") // left single quote
.replace(/\u201c/g, "&ldquo;") // left double quote
.replace(/\u201d/g, "&rdquo;") // right double quote
.replace(/\u2026/g, "&hellip;") // ellipsis
.replace(/\u2192/g, "&rarr;") // right arrow
.replace(/\u00a3/g, "&pound;") // pound sign
.replace(/\u00d7/g, "&times;"); // multiplication sign
}