From c9301edbe89e954c24a0c5ecf8d6b9380000a666 Mon Sep 17 00:00:00 2001 From: Omair Saleh Date: Tue, 3 Mar 2026 21:17:19 +0800 Subject: [PATCH] hero image fills full text column height on desktop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Grid: items-start → md:items-stretch (both columns same height) - Image: aspect-[4/5] → md:aspect-auto md:h-full (fills column) - Mobile keeps aspect-[3/4] for stacked layout - Bottom of image now lines up with buttons/trust line --- .pi/extensions/calvana-shiplog.ts | 253 +++++++++++++++++++------- pledge-now-pay-later/src/app/page.tsx | 4 +- 2 files changed, 191 insertions(+), 66 deletions(-) diff --git a/.pi/extensions/calvana-shiplog.ts b/.pi/extensions/calvana-shiplog.ts index c2b8cca..d5c5fd2 100644 --- a/.pi/extensions/calvana-shiplog.ts +++ b/.pi/extensions/calvana-shiplog.ts @@ -216,6 +216,19 @@ Current oops: ${state.oops.length} loomLink: params.loomLink || "#loomclip", }; state.ships.push(entry); + + // Persist to PostgreSQL + const addTitle = entry.title.replace(/'/g, "''"); + const addMetric = (entry.metric || "—").replace(/'/g, "''"); + const addStatus = entry.status; + 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 ships (title, status, metric) VALUES (\\x27${addTitle}\\x27, \\x27${addStatus}\\x27, \\x27${addMetric}\\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: `Ship #${entry.id} added: "${entry.title}" [${entry.status}]` }], details: { state: { ...state, ships: [...state.ships] } }, @@ -242,6 +255,18 @@ Current oops: ${state.oops.length} if (params.deployLink) ship.deployLink = params.deployLink; if (params.loomLink) ship.loomLink = params.loomLink; ship.timestamp = now; + + // Persist update to PostgreSQL + const setClauses: string[] = []; + if (params.status) setClauses.push(`status='${params.status}'`); + if (params.metric) setClauses.push(`metric='${(params.metric || "").replace(/'/g, "''")}'`); + setClauses.push("updated_at=now()"); + try { + 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 -c \\\"UPDATE ships SET ${setClauses.join(", ")} WHERE id=${params.id}\\\"'"` + ], { timeout: 15000 }); + } catch { /* DB write failed, local state still updated */ } + return { content: [{ type: "text", text: `Ship #${ship.id} updated: "${ship.title}" [${ship.status}]` }], details: { state: { ...state, ships: [...state.ships] } }, @@ -389,32 +414,82 @@ Current oops: ${state.oops.length} parameters: DeployParams, async execute(_toolCallId, params, signal, onUpdate, _ctx) { - onUpdate?.({ content: [{ type: "text", text: "Generating HTML..." }] }); + onUpdate?.({ content: [{ type: "text", text: "Querying database for full ship log..." }] }); - const liveHtml = generateLivePageHtml(state); - - if (params.dryRun) { - return { - content: [{ type: "text", text: `Dry run — generated ${liveHtml.length} bytes of HTML.\n\n${liveHtml.slice(0, 500)}...` }], - details: { state: { ...state }, dryRun: true }, - }; - } - - onUpdate?.({ content: [{ type: "text", text: "Deploying to server..." }] }); + const sshBase = `ssh -o ConnectTimeout=10 -p ${DEPLOY_CONFIG.sshPort} ${DEPLOY_CONFIG.sshHost}`; + const pgContainer = "$(docker ps --format '{{.Names}}' | grep dokploy-postgres)"; + const psqlCmd = (sql: string) => `${sshBase} "incus exec ${DEPLOY_CONFIG.container} -- bash -c 'docker exec ${pgContainer} psql -U dokploy -d calvana -t -A -F \\\"|||\\\" -c \\\"${sql}\\\"'"`; try { - // Write HTML to server via SSH + incus exec - const escapedHtml = liveHtml.replace(/'/g, "'\\''"); - const sshCmd = `ssh -o ConnectTimeout=10 -p ${DEPLOY_CONFIG.sshPort} ${DEPLOY_CONFIG.sshHost}`; - const writeCmd = `${sshCmd} "incus exec ${DEPLOY_CONFIG.container} -- bash -c 'cat > ${DEPLOY_CONFIG.sitePath}/live/index.html << '\\''HTMLEOF'\\'' -${liveHtml} -HTMLEOF -'"`; + // 1. Query all ships from database + const shipsResult = await pi.exec("bash", ["-c", + psqlCmd("SELECT id, title, status, COALESCE(metric,'—'), COALESCE(details,''), created_at::text, COALESCE(updated_at::text, created_at::text) FROM ships ORDER BY id") + ], { signal, timeout: 20000 }); - // Use base64 to avoid all escaping nightmares + if (shipsResult.code !== 0) { + return { + content: [{ type: "text", text: `DB query failed: ${shipsResult.stderr}` }], + details: { state: { ...state }, error: shipsResult.stderr }, + isError: true, + }; + } + + // 2. Query all oops from database + const oopsResult = await pi.exec("bash", ["-c", + psqlCmd("SELECT id, description, COALESCE(fix_time,'—'), COALESCE(commit_link,'#commit'), created_at::text FROM oops ORDER BY id") + ], { signal, timeout: 20000 }); + + // 3. Parse DB results into ship/oops arrays + const dbShips: Array<{ id: number; title: string; status: string; metric: string; details: string; created: string; updated: string }> = []; + for (const line of shipsResult.stdout.trim().split("\n")) { + if (!line.trim()) continue; + const parts = line.split("|||"); + if (parts.length >= 6) { + dbShips.push({ + id: parseInt(parts[0]), + title: parts[1], + status: parts[2], + metric: parts[3], + details: parts[4], + created: parts[5], + updated: parts[6] || parts[5], + }); + } + } + + const dbOops: Array<{ id: number; description: string; fixTime: string; commitLink: string; created: string }> = []; + for (const line of (oopsResult.stdout || "").trim().split("\n")) { + if (!line.trim()) continue; + const parts = line.split("|||"); + if (parts.length >= 4) { + dbOops.push({ + id: parseInt(parts[0]), + description: parts[1], + fixTime: parts[2], + commitLink: parts[3], + created: parts[4] || "", + }); + } + } + + onUpdate?.({ content: [{ type: "text", text: `Found ${dbShips.length} ships + ${dbOops.length} oops. Generating HTML...` }] }); + + // 4. Generate HTML from DB data + const liveHtml = generateLivePageFromDb(dbShips, dbOops); + + if (params.dryRun) { + return { + content: [{ type: "text", text: `Dry run — ${dbShips.length} ships, ${dbOops.length} oops, ${liveHtml.length} bytes HTML.\n\n${liveHtml.slice(0, 500)}...` }], + details: { state: { ...state }, dryRun: true }, + }; + } + + onUpdate?.({ content: [{ type: "text", text: "Deploying to server..." }] }); + + // 5. Deploy via base64 const b64Html = Buffer.from(liveHtml).toString("base64"); const deployResult = await pi.exec("bash", ["-c", - `ssh -o ConnectTimeout=10 -p ${DEPLOY_CONFIG.sshPort} ${DEPLOY_CONFIG.sshHost} "incus exec ${DEPLOY_CONFIG.container} -- bash -c 'echo ${b64Html} | base64 -d > ${DEPLOY_CONFIG.sitePath}/live/index.html'"` + `${sshBase} "incus exec ${DEPLOY_CONFIG.container} -- bash -c 'echo ${b64Html} | base64 -d > ${DEPLOY_CONFIG.sitePath}/live/index.html'"` ], { signal, timeout: 30000 }); if (deployResult.code !== 0) { @@ -425,16 +500,16 @@ HTMLEOF }; } - // Rebuild and update docker service + // 6. Rebuild and update docker service const rebuildResult = await pi.exec("bash", ["-c", - `ssh -o ConnectTimeout=10 -p ${DEPLOY_CONFIG.sshPort} ${DEPLOY_CONFIG.sshHost} "incus exec ${DEPLOY_CONFIG.container} -- bash -c 'cd /opt/calvana && docker build -t calvana:latest . 2>&1 | tail -2 && docker service update --force calvana 2>&1 | tail -2'"` + `${sshBase} "incus exec ${DEPLOY_CONFIG.container} -- bash -c 'cd /opt/calvana && docker build -t calvana:latest . 2>&1 | tail -2 && docker service update --force calvana 2>&1 | tail -2'"` ], { signal, timeout: 60000 }); const now = new Date().toISOString().replace("T", " ").slice(0, 19); state.lastDeployed = now; return { - content: [{ type: "text", text: `✓ Deployed to https://${DEPLOY_CONFIG.domain}/live\n${rebuildResult.stdout}` }], + content: [{ type: "text", text: `✓ Deployed to https://${DEPLOY_CONFIG.domain}/live — ${dbShips.length} ships + ${dbOops.length} oops from database\n${rebuildResult.stdout}` }], details: { state: { ...state, lastDeployed: now } }, }; } catch (err: any) { @@ -589,48 +664,77 @@ class ShipLogComponent { // HTML GENERATOR — Builds the /live page from current state // ════════════════════════════════════════════════════════════════════ +// Keep the old function signature for backward compat but it's no longer called by deploy function generateLivePageHtml(state: ShipLogState): string { + return generateLivePageFromDb( + state.ships.map(s => ({ id: s.id, title: s.title, status: s.status, metric: s.metric, details: "", created: s.timestamp, updated: s.timestamp })), + state.oops.map(o => ({ id: o.id, description: o.description, fixTime: o.fixTime, commitLink: o.commitLink, created: o.timestamp })) + ); +} + +function generateLivePageFromDb( + ships: Array<{ id: number; title: string; status: string; metric: string; details: string; created: string; updated: string }>, + oops: Array<{ id: number; description: string; fixTime: string; commitLink: string; created: string }> +): string { const now = new Date().toISOString(); + const shipped = ships.filter(s => s.status === "shipped").length; + const shipping = ships.filter(s => s.status === "shipping").length; - const shipCards = state.ships.map(s => { - const badgeClass = s.status === "shipped" ? "badge-shipped" - : s.status === "shipping" ? "badge-shipping" - : "badge-planned"; - const badgeLabel = s.status.charAt(0).toUpperCase() + s.status.slice(1); - const titleSuffix = s.status === "shipped" ? " ✓" : ""; + // Group ships by date (newest first) + const shipsByDate = new Map(); + for (const s of [...ships].reverse()) { + const date = s.created.split(" ")[0] || s.created.split("T")[0] || "Unknown"; + if (!shipsByDate.has(date)) shipsByDate.set(date, []); + shipsByDate.get(date)!.push(s); + } - return `
+ const formatDate = (dateStr: string) => { + try { + const d = new Date(dateStr); + return d.toLocaleDateString("en-GB", { weekday: "long", day: "numeric", month: "long", year: "numeric" }); + } catch { return dateStr; } + }; + + let shipSections = ""; + for (const [date, dateShips] of shipsByDate) { + const cards = dateShips.map(s => { + const badgeClass = s.status === "shipped" ? "badge-shipped" + : s.status === "shipping" ? "badge-shipping" + : "badge-planned"; + const badgeLabel = s.status.charAt(0).toUpperCase() + s.status.slice(1); + + // If details has HTML (from DB), use it; otherwise use metric + const hasDetails = s.details && s.details.trim().length > 10 && s.details.includes("<"); + const detailsBlock = hasDetails + ? `\n
${s.details}
` + : ""; + + return `
- ${escapeHtml(s.title)}${titleSuffix} + #${s.id} + ${escapeHtml(s.title)} ${badgeLabel}
-

⏱ ${escapeHtml(s.timestamp)}

-

What moved: ${escapeHtml(s.metric)}

- +

${escapeHtml(s.metric)}

${detailsBlock}
`; - }).join("\n"); + }).join("\n"); - const oopsEntries = state.oops.map(o => { - return `
- ${escapeHtml(o.description)}${o.fixTime !== "—" ? ` Fixed in ${escapeHtml(o.fixTime)}.` : ""} - → commit -
`; - }).join("\n"); + shipSections += ` +
+

${formatDate(date)} ${dateShips.length} ship${dateShips.length !== 1 ? "s" : ""}

+
+${cards} +
+
`; + } - // If no ships yet, show placeholder - const shipsSection = state.ships.length > 0 ? shipCards : `
-
- Warming up... - Planned -
-

⏱ —

-

What moved: —

-
`; - - const oopsSection = state.oops.length > 0 ? oopsEntries : `
- Nothing broken yet. Give it time. - → waiting -
`; + const oopsEntries = oops.length > 0 + ? oops.map(o => `
+ #${o.id} + ${escapeHtml(o.description)}${o.fixTime !== "—" ? ` Fixed in ${escapeHtml(o.fixTime)}.` : ""} + ${o.commitLink && o.commitLink !== "#commit" ? `→ commit` : ""} +
`).join("\n") + : `
Nothing broken yet. Give it time.
`; return ` @@ -646,6 +750,26 @@ function generateLivePageHtml(state: ShipLogState): string { +

Live Shipping Log

-

Intentional chaos. Full receipts.

+

Intentional chaos. Full receipts. Every ship ever.

-
-

Today's Ships

-
-${shipsSection} -
-
+
+
${shipped}
Shipped
+
${shipping}
In Flight
+
${ships.length}
Total
+
${oops.length}
Oops
+
+${shipSections}
@@ -696,13 +821,13 @@ ${shipsSection}

Oops Log

If it's not here, I haven't broken it yet.

-${oopsSection} +${oopsEntries}
-

Last updated: ${now}

+

Last updated: ${now} · ${ships.length} ships from PostgreSQL

diff --git a/pledge-now-pay-later/src/app/page.tsx b/pledge-now-pay-later/src/app/page.tsx index 1b847ce..ee29df6 100644 --- a/pledge-now-pay-later/src/app/page.tsx +++ b/pledge-now-pay-later/src/app/page.tsx @@ -50,7 +50,7 @@ export default function HomePage() { {/* ━━ HERO (dark) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */}
-
+
{/* ── Text column ── */}
@@ -99,7 +99,7 @@ export default function HomePage() { {/* ── Image column ── */}
-
+