ROOT CAUSE: each card was wrapped in its own div (min-h-[85vh]), scoping sticky to that wrapper — cards could NEVER overlap. FIX: flatMap returns all sticky divs + h-4 spacers as direct siblings under the same parent (mt-14). Sticky now works correctly — each card overlaps the previous with 20px peek. - Removed all shadows (border-gray-100 only) - z-index: 1-4 (was 10-40, conflicting with nav z-40) - top: 72/92/112/132px (20px stagger) - h-4 spacers between cards (no big white gaps) - Regenerated dinner image: dark navy table, candlelight, £5,000 pledge card — zero white space (was white tablecloth)
805 lines
36 KiB
TypeScript
805 lines
36 KiB
TypeScript
/**
|
|
* Calvana Ship Log Extension — DB-ONLY Architecture
|
|
*
|
|
* ZERO in-memory state. ZERO session reconstruction.
|
|
* Every read hits PostgreSQL. Every write hits PostgreSQL.
|
|
* If DB is unreachable, operations fail loudly — never silently use stale data.
|
|
*
|
|
* Tools (LLM-callable):
|
|
* - calvana_ship: Add/update/complete shipping log entries
|
|
* - calvana_oops: Log mistakes and fixes
|
|
* - calvana_deploy: Push changes to the live site
|
|
*
|
|
* Commands (user):
|
|
* /ships — View current shipping log
|
|
* /ship-deploy — Force deploy to calvana.quikcue.com
|
|
*/
|
|
|
|
import { StringEnum } from "@mariozechner/pi-ai";
|
|
import type { ExtensionAPI, ExtensionContext, Theme } from "@mariozechner/pi-coding-agent";
|
|
import { Text, truncateToWidth, matchesKey } from "@mariozechner/pi-tui";
|
|
import { Type } from "@sinclair/typebox";
|
|
|
|
// ════════════════════════════════════════════════════════════════════
|
|
// CONFIGURATION
|
|
// ════════════════════════════════════════════════════════════════════
|
|
|
|
const DEPLOY_CONFIG = {
|
|
sshHost: "root@159.195.60.33",
|
|
sshPort: "22",
|
|
container: "qc-server-new",
|
|
sitePath: "/opt/calvana/html",
|
|
domain: "calvana.quikcue.com",
|
|
};
|
|
|
|
const SITE_CONFIG = {
|
|
title: "Calvana",
|
|
tagline: "I break rules. Not production.",
|
|
email: "omair@quikcue.com",
|
|
referralLine: "PS — Umar pointed me here. If this turns into a hire, I want him to get paid.",
|
|
};
|
|
|
|
// ════════════════════════════════════════════════════════════════════
|
|
// DB ACCESS — Single source of truth. No caching. No fallbacks.
|
|
// ════════════════════════════════════════════════════════════════════
|
|
|
|
const SSH_BASE = `ssh -o ConnectTimeout=10 -o StrictHostKeyChecking=no -p ${DEPLOY_CONFIG.sshPort} ${DEPLOY_CONFIG.sshHost}`;
|
|
const PG_CONTAINER_CMD = `$(docker ps --format '{{.Names}}' | grep dokploy-postgres)`;
|
|
|
|
/**
|
|
* Run a SQL query against the calvana DB.
|
|
* Uses base64 encoding to bypass the 7-layer quoting hell
|
|
* (local bash → SSH → remote bash → incus → container bash → docker → psql).
|
|
* Returns raw stdout. Throws on failure — callers MUST handle errors.
|
|
* No silent fallbacks. No swallowed exceptions.
|
|
*/
|
|
async function dbQuery(pi: ExtensionAPI, sql: string, timeout = 15000): Promise<string> {
|
|
// Base64-encode the SQL to avoid ALL quoting issues through the SSH/incus/docker chain
|
|
const b64Sql = Buffer.from(sql).toString("base64");
|
|
const result = await pi.exec("bash", ["-c",
|
|
`${SSH_BASE} "incus exec ${DEPLOY_CONFIG.container} -- bash -c 'echo ${b64Sql} | base64 -d | docker exec -i ${PG_CONTAINER_CMD} psql -U dokploy -d calvana -t -A -F \\\"|||\\\"'"`
|
|
], { timeout });
|
|
|
|
if (result.code !== 0) {
|
|
throw new Error(`DB query failed (exit ${result.code}): ${result.stderr?.slice(0, 200)}`);
|
|
}
|
|
return result.stdout?.trim() || "";
|
|
}
|
|
|
|
// ════════════════════════════════════════════════════════════════════
|
|
// DB READ HELPERS — Always fresh from DB. Never cached.
|
|
// ════════════════════════════════════════════════════════════════════
|
|
|
|
interface DbShip {
|
|
id: number;
|
|
title: string;
|
|
status: string;
|
|
metric: string;
|
|
created: string;
|
|
}
|
|
|
|
interface DbOops {
|
|
id: number;
|
|
description: string;
|
|
fixTime: string;
|
|
commitLink: string;
|
|
created: string;
|
|
}
|
|
|
|
async function fetchShips(pi: ExtensionAPI): Promise<DbShip[]> {
|
|
const raw = await dbQuery(pi, "SELECT id, title, status, COALESCE(metric, '-'), created_at::text FROM ships ORDER BY id");
|
|
if (!raw) return [];
|
|
const ships: DbShip[] = [];
|
|
for (const line of raw.split("\n")) {
|
|
if (!line.trim()) continue;
|
|
const p = line.split("|||");
|
|
if (p.length >= 5) {
|
|
ships.push({ id: parseInt(p[0]), title: p[1], status: p[2], metric: p[3], created: p[4] });
|
|
}
|
|
}
|
|
return ships;
|
|
}
|
|
|
|
async function fetchOops(pi: ExtensionAPI): Promise<DbOops[]> {
|
|
const raw = await dbQuery(pi, "SELECT id, description, COALESCE(fix_time, '-'), COALESCE(commit_link, '#commit'), created_at::text FROM oops ORDER BY id");
|
|
if (!raw) return [];
|
|
const oops: DbOops[] = [];
|
|
for (const line of raw.split("\n")) {
|
|
if (!line.trim()) continue;
|
|
const p = line.split("|||");
|
|
if (p.length >= 4) {
|
|
oops.push({ id: parseInt(p[0]), description: p[1], fixTime: p[2], commitLink: p[3], created: p[4] || "" });
|
|
}
|
|
}
|
|
return oops;
|
|
}
|
|
|
|
/** Get summary counts for status bar / system prompt. */
|
|
async function fetchSummary(pi: ExtensionAPI): Promise<{ total: number; shipped: number; oops: number }> {
|
|
try {
|
|
const raw = await dbQuery(pi,
|
|
"SELECT (SELECT count(*) FROM ships), (SELECT count(*) FROM ships WHERE status='shipped'), (SELECT count(*) FROM oops)"
|
|
);
|
|
const p = raw.split("|||");
|
|
return { total: parseInt(p[0]) || 0, shipped: parseInt(p[1]) || 0, oops: parseInt(p[2]) || 0 };
|
|
} catch {
|
|
return { total: -1, shipped: -1, oops: -1 }; // -1 signals DB unreachable
|
|
}
|
|
}
|
|
|
|
// ════════════════════════════════════════════════════════════════════
|
|
// TOOL SCHEMAS
|
|
// ════════════════════════════════════════════════════════════════════
|
|
|
|
const ShipParams = Type.Object({
|
|
action: StringEnum(["add", "update", "list"] as const),
|
|
title: Type.Optional(Type.String({ description: "Ship title (for add)" })),
|
|
id: Type.Optional(Type.Number({ description: "Ship ID (for update)" })),
|
|
status: Type.Optional(StringEnum(["planned", "shipping", "shipped"] as const)),
|
|
metric: Type.Optional(Type.String({ description: "What moved — metric line" })),
|
|
prLink: Type.Optional(Type.String({ description: "PR link" })),
|
|
deployLink: Type.Optional(Type.String({ description: "Deploy link" })),
|
|
loomLink: Type.Optional(Type.String({ description: "Loom clip link" })),
|
|
});
|
|
|
|
const OopsParams = Type.Object({
|
|
action: StringEnum(["add", "list"] as const),
|
|
description: Type.Optional(Type.String({ description: "What broke and how it was fixed" })),
|
|
fixTime: Type.Optional(Type.String({ description: "Time to fix, e.g. '3 min'" })),
|
|
commitLink: Type.Optional(Type.String({ description: "Link to the fix commit" })),
|
|
});
|
|
|
|
const DeployParams = Type.Object({
|
|
dryRun: Type.Optional(Type.Boolean({ description: "If true, generate HTML but don't deploy" })),
|
|
});
|
|
|
|
// ════════════════════════════════════════════════════════════════════
|
|
// EXTENSION
|
|
// ════════════════════════════════════════════════════════════════════
|
|
|
|
export default function (pi: ExtensionAPI) {
|
|
// ── NO in-memory state. Only a cache for the status bar display. ──
|
|
let lastDeployed: string | null = null;
|
|
let statusCache = { total: 0, shipped: 0, oops: 0 };
|
|
|
|
const refreshStatusBar = async (ctx: ExtensionContext) => {
|
|
if (!ctx.hasUI) return;
|
|
const summary = await fetchSummary(pi);
|
|
if (summary.total === -1) {
|
|
ctx.ui.setStatus("calvana", ctx.ui.theme.fg("error", "🚀 DB unreachable"));
|
|
} else {
|
|
statusCache = summary;
|
|
ctx.ui.setStatus("calvana", ctx.ui.theme.fg("dim",
|
|
`🚀 ${summary.shipped}/${summary.total} shipped · ${summary.oops} oops (DB)`
|
|
));
|
|
}
|
|
};
|
|
|
|
// ── Session events: just refresh the status bar. NO state reconstruction. ──
|
|
pi.on("session_start", async (_event, ctx) => await refreshStatusBar(ctx));
|
|
pi.on("session_switch", async (_event, ctx) => await refreshStatusBar(ctx));
|
|
pi.on("session_fork", async (_event, ctx) => await refreshStatusBar(ctx));
|
|
pi.on("session_tree", async (_event, ctx) => await refreshStatusBar(ctx));
|
|
pi.on("turn_end", async (_event, ctx) => await refreshStatusBar(ctx));
|
|
|
|
// ── Inject context so LLM knows about ship tracking ──
|
|
pi.on("before_agent_start", async (event, _ctx) => {
|
|
const s = await fetchSummary(pi);
|
|
const dbStatus = s.total === -1 ? "⚠️ DB UNREACHABLE" : `${s.total} ships (${s.shipped} shipped), ${s.oops} oops`;
|
|
const shipContext = `
|
|
[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.
|
|
|
|
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
|
|
|
|
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: ${dbStatus}
|
|
`;
|
|
return { systemPrompt: event.systemPrompt + shipContext };
|
|
});
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// TOOL: calvana_ship — ALL operations go directly to DB
|
|
// ════════════════════════════════════════════════════════════════
|
|
|
|
pi.registerTool({
|
|
name: "calvana_ship",
|
|
label: "Ship Log",
|
|
description: "Track shipping progress. Actions: add (new entry), update (change status/links), list (show all). Use this whenever you start, progress, or finish a task.",
|
|
parameters: ShipParams,
|
|
|
|
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
switch (params.action) {
|
|
case "add": {
|
|
if (!params.title) {
|
|
return { content: [{ type: "text", text: "Error: title required" }], isError: true };
|
|
}
|
|
const title = params.title.replace(/'/g, "''");
|
|
const status = params.status || "planned";
|
|
const metric = (params.metric || "—").replace(/'/g, "''");
|
|
|
|
try {
|
|
const raw = await dbExec(pi,
|
|
`INSERT INTO ships (title, status, metric) VALUES ('${title}', '${status}', '${metric}') RETURNING id`
|
|
);
|
|
const id = parseInt(raw.trim()) || 0;
|
|
return { content: [{ type: "text", text: `Ship #${id} added: "${params.title}" [${status}]` }] };
|
|
} catch (err: any) {
|
|
return { content: [{ type: "text", text: `DB ERROR adding ship: ${err.message}` }], isError: true };
|
|
}
|
|
}
|
|
|
|
case "update": {
|
|
if (params.id === undefined) {
|
|
return { content: [{ type: "text", text: "Error: id required for update" }], isError: true };
|
|
}
|
|
|
|
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 {
|
|
const raw = await dbExec(pi,
|
|
`UPDATE ships SET ${setClauses.join(", ")} WHERE id=${params.id} RETURNING id, title, status`
|
|
);
|
|
if (!raw.trim()) {
|
|
return { content: [{ type: "text", text: `Ship #${params.id} not found in DB` }], isError: true };
|
|
}
|
|
const p = raw.trim().split("|||");
|
|
return { content: [{ type: "text", text: `Ship #${p[0]} updated: "${p[1]}" [${p[2]}]` }] };
|
|
} catch (err: any) {
|
|
return { content: [{ type: "text", text: `DB ERROR updating ship: ${err.message}` }], isError: true };
|
|
}
|
|
}
|
|
|
|
case "list": {
|
|
try {
|
|
const ships = await fetchShips(pi);
|
|
if (ships.length === 0) {
|
|
return { content: [{ type: "text", text: "No ships in DB." }] };
|
|
}
|
|
const lines = ships.map(s =>
|
|
`#${s.id} [${s.status.toUpperCase()}] ${s.title} — ${s.metric}`
|
|
);
|
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
} catch (err: any) {
|
|
return { content: [{ type: "text", text: `DB ERROR listing ships: ${err.message}` }], isError: true };
|
|
}
|
|
}
|
|
|
|
default:
|
|
return { content: [{ type: "text", text: `Unknown action: ${params.action}` }], isError: true };
|
|
}
|
|
},
|
|
|
|
renderCall(args, theme) {
|
|
let text = theme.fg("toolTitle", theme.bold("🚀 ship "));
|
|
text += theme.fg("muted", args.action || "");
|
|
if (args.title) text += " " + theme.fg("dim", `"${args.title}"`);
|
|
if (args.id !== undefined) text += " " + theme.fg("accent", `#${args.id}`);
|
|
if (args.status) text += " → " + theme.fg("accent", args.status);
|
|
return new Text(text, 0, 0);
|
|
},
|
|
|
|
renderResult(result, { expanded }, theme) {
|
|
const text = result.content[0];
|
|
const msg = text?.type === "text" ? text.text : "";
|
|
if (result.isError) return new Text(theme.fg("error", msg), 0, 0);
|
|
return new Text(theme.fg("success", msg), 0, 0);
|
|
},
|
|
});
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// TOOL: calvana_oops — ALL operations go directly to DB
|
|
// ════════════════════════════════════════════════════════════════
|
|
|
|
pi.registerTool({
|
|
name: "calvana_oops",
|
|
label: "Oops Log",
|
|
description: "Log mistakes and fixes. Actions: add (new oops entry), list (show all). Use when something breaks during a task.",
|
|
parameters: OopsParams,
|
|
|
|
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
switch (params.action) {
|
|
case "add": {
|
|
if (!params.description) {
|
|
return { content: [{ type: "text", text: "Error: description required" }], isError: true };
|
|
}
|
|
const desc = params.description.replace(/'/g, "''");
|
|
const fixTime = (params.fixTime || "—").replace(/'/g, "''");
|
|
const commitLink = (params.commitLink || "#commit").replace(/'/g, "''");
|
|
|
|
try {
|
|
const raw = await dbExec(pi,
|
|
`INSERT INTO oops (description, fix_time, commit_link) VALUES ('${desc}', '${fixTime}', '${commitLink}') RETURNING id`
|
|
);
|
|
const id = parseInt(raw.trim()) || 0;
|
|
return { content: [{ type: "text", text: `Oops #${id}: "${params.description}" (fixed in ${params.fixTime || "—"})` }] };
|
|
} catch (err: any) {
|
|
return { content: [{ type: "text", text: `DB ERROR adding oops: ${err.message}` }], isError: true };
|
|
}
|
|
}
|
|
|
|
case "list": {
|
|
try {
|
|
const oops = await fetchOops(pi);
|
|
if (oops.length === 0) {
|
|
return { content: [{ type: "text", text: "No oops entries. Clean run so far." }] };
|
|
}
|
|
const lines = oops.map(o => `#${o.id} ${o.description} — fixed in ${o.fixTime}`);
|
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
} catch (err: any) {
|
|
return { content: [{ type: "text", text: `DB ERROR listing oops: ${err.message}` }], isError: true };
|
|
}
|
|
}
|
|
|
|
default:
|
|
return { content: [{ type: "text", text: `Unknown action: ${params.action}` }], isError: true };
|
|
}
|
|
},
|
|
|
|
renderCall(args, theme) {
|
|
let text = theme.fg("toolTitle", theme.bold("💥 oops "));
|
|
text += theme.fg("muted", args.action || "");
|
|
if (args.description) text += " " + theme.fg("dim", `"${args.description}"`);
|
|
return new Text(text, 0, 0);
|
|
},
|
|
|
|
renderResult(result, _options, theme) {
|
|
const text = result.content[0];
|
|
const msg = text?.type === "text" ? text.text : "";
|
|
if (result.isError) return new Text(theme.fg("error", msg), 0, 0);
|
|
return new Text(theme.fg("warning", msg), 0, 0);
|
|
},
|
|
});
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// TOOL: calvana_deploy — Reads ONLY from DB, never from memory
|
|
// ════════════════════════════════════════════════════════════════
|
|
|
|
pi.registerTool({
|
|
name: "calvana_deploy",
|
|
label: "Deploy Calvana",
|
|
description: `Regenerate the /live page with current ship log and deploy to https://${DEPLOY_CONFIG.domain}. Call this after adding/updating ships or oops entries to push changes live.`,
|
|
parameters: DeployParams,
|
|
|
|
async execute(_toolCallId, params, signal, onUpdate, _ctx) {
|
|
onUpdate?.({ content: [{ type: "text", text: "Querying database for full ship log..." }] });
|
|
|
|
// NOTE: We intentionally EXCLUDE the 'details' column from ships.
|
|
// It contains multiline HTML that breaks the ||| line-based parser.
|
|
// The metric column is sufficient for the live page cards.
|
|
const HELPER_SCRIPT = `
|
|
PG_CONTAINER=$(incus exec ${DEPLOY_CONFIG.container} -- bash -c "docker ps --format '{{.Names}}' | grep dokploy-postgres")
|
|
if [ -z "$PG_CONTAINER" ]; then echo "ERR:NO_PG_CONTAINER"; exit 1; fi
|
|
SHIPS=$(incus exec ${DEPLOY_CONFIG.container} -- bash -c "docker exec $PG_CONTAINER psql -U dokploy -d calvana -t -A -F '|||' -c \\"SELECT id, title, status, COALESCE(metric,'-'), created_at::text, COALESCE(updated_at::text, created_at::text) FROM ships ORDER BY id\\"")
|
|
OOPS=$(incus exec ${DEPLOY_CONFIG.container} -- bash -c "docker exec $PG_CONTAINER psql -U dokploy -d calvana -t -A -F '|||' -c \\"SELECT id, description, COALESCE(fix_time,'-'), COALESCE(commit_link,'#commit'), created_at::text FROM oops ORDER BY id\\"")
|
|
echo "===SHIPS==="
|
|
echo "$SHIPS"
|
|
echo "===OOPS==="
|
|
echo "$OOPS"
|
|
echo "===END==="
|
|
`.trim();
|
|
|
|
try {
|
|
const dbResult = await pi.exec("bash", ["-c",
|
|
`${SSH_BASE} 'bash -s' << 'DBEOF'\n${HELPER_SCRIPT}\nDBEOF`
|
|
], { signal, timeout: 30000 });
|
|
|
|
if (dbResult.code !== 0) {
|
|
return {
|
|
content: [{ type: "text", text: `DB query failed (code ${dbResult.code}): ${dbResult.stderr}\nstdout: ${dbResult.stdout?.slice(0, 200)}` }],
|
|
isError: true,
|
|
};
|
|
}
|
|
|
|
const output = dbResult.stdout || "";
|
|
if (output.includes("ERR:NO_PG_CONTAINER")) {
|
|
return {
|
|
content: [{ type: "text", text: `ABORT: PostgreSQL container not found.` }],
|
|
isError: true,
|
|
};
|
|
}
|
|
|
|
const shipsSection = output.split("===SHIPS===")[1]?.split("===OOPS===")[0]?.trim() || "";
|
|
const oopsSection = output.split("===OOPS===")[1]?.split("===END===")[0]?.trim() || "";
|
|
|
|
const dbShips: Array<{ id: number; title: string; status: string; metric: string; created: string; updated: string }> = [];
|
|
for (const line of shipsSection.split("\n")) {
|
|
if (!line.trim()) continue;
|
|
const parts = line.split("|||");
|
|
if (parts.length >= 5) {
|
|
dbShips.push({
|
|
id: parseInt(parts[0]), title: parts[1], status: parts[2],
|
|
metric: parts[3], created: parts[4], updated: parts[5] || parts[4],
|
|
});
|
|
}
|
|
}
|
|
|
|
const dbOops: Array<{ id: number; description: string; fixTime: string; commitLink: string; created: string }> = [];
|
|
for (const line of oopsSection.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] || "",
|
|
});
|
|
}
|
|
}
|
|
|
|
if (dbShips.length === 0) {
|
|
return {
|
|
content: [{ type: "text", text: `ABORT: DB returned 0 ships. Refusing to deploy.\nRaw: ${output.slice(0, 500)}` }],
|
|
isError: true,
|
|
};
|
|
}
|
|
|
|
onUpdate?.({ content: [{ type: "text", text: `Found ${dbShips.length} ships + ${dbOops.length} oops. Generating HTML...` }] });
|
|
|
|
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)}...` }],
|
|
};
|
|
}
|
|
|
|
onUpdate?.({ content: [{ type: "text", text: `Deploying ${dbShips.length} ships...` }] });
|
|
|
|
const b64Html = Buffer.from(liveHtml).toString("base64");
|
|
const deployResult = await pi.exec("bash", ["-c",
|
|
`${SSH_BASE} "incus exec ${DEPLOY_CONFIG.container} -- bash -c 'echo ${b64Html} | base64 -d > ${DEPLOY_CONFIG.sitePath}/live/index.html'"`
|
|
], { signal, timeout: 30000 });
|
|
|
|
if (deployResult.code !== 0) {
|
|
return {
|
|
content: [{ type: "text", text: `Deploy write failed: ${deployResult.stderr}` }],
|
|
isError: true,
|
|
};
|
|
}
|
|
|
|
const rebuildResult = await pi.exec("bash", ["-c",
|
|
`${SSH_BASE} "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);
|
|
lastDeployed = now;
|
|
|
|
return {
|
|
content: [{ type: "text", text: `✓ Deployed to https://${DEPLOY_CONFIG.domain}/live\n${dbShips.length} ships + ${dbOops.length} oops from database\n${rebuildResult.stdout}` }],
|
|
};
|
|
} catch (err: any) {
|
|
return {
|
|
content: [{ type: "text", text: `Deploy error: ${err.message}` }],
|
|
isError: true,
|
|
};
|
|
}
|
|
},
|
|
|
|
renderCall(_args, theme) {
|
|
return new Text(theme.fg("toolTitle", theme.bold("🌐 deploy calvana")), 0, 0);
|
|
},
|
|
|
|
renderResult(result, _options, theme) {
|
|
if (result.isError) {
|
|
const text = result.content[0];
|
|
return new Text(theme.fg("error", `✗ ${text?.type === "text" ? text.text : "failed"}`), 0, 0);
|
|
}
|
|
return new Text(theme.fg("success", `✓ Live at https://${DEPLOY_CONFIG.domain}/live`), 0, 0);
|
|
},
|
|
});
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// COMMAND: /ships — reads directly from DB
|
|
// ════════════════════════════════════════════════════════════════
|
|
|
|
pi.registerCommand("ships", {
|
|
description: "View current Calvana shipping log (from DB)",
|
|
handler: async (_args, ctx) => {
|
|
if (!ctx.hasUI) return;
|
|
|
|
let ships: DbShip[] = [];
|
|
let oops: DbOops[] = [];
|
|
let dbError: string | null = null;
|
|
|
|
try {
|
|
[ships, oops] = await Promise.all([fetchShips(pi), fetchOops(pi)]);
|
|
} catch (err: any) {
|
|
dbError = err.message;
|
|
}
|
|
|
|
await ctx.ui.custom<void>((_tui, theme, _kb, done) => {
|
|
return new ShipLogComponent(ships, oops, lastDeployed, dbError, theme, () => done());
|
|
});
|
|
},
|
|
});
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// COMMAND: /ship-deploy
|
|
// ════════════════════════════════════════════════════════════════
|
|
|
|
pi.registerCommand("ship-deploy", {
|
|
description: "Force deploy the Calvana site with current ship log",
|
|
handler: async (_args, ctx) => {
|
|
const ok = await ctx.ui.confirm("Deploy?", `Push ship log to https://${DEPLOY_CONFIG.domain}/live?`);
|
|
if (!ok) return;
|
|
pi.sendUserMessage("Use calvana_deploy to push the current ship log to the live site.", { deliverAs: "followUp" });
|
|
},
|
|
});
|
|
}
|
|
|
|
// ════════════════════════════════════════════════════════════════════
|
|
// UI COMPONENT: /ships viewer
|
|
// ════════════════════════════════════════════════════════════════════
|
|
|
|
class ShipLogComponent {
|
|
private ships: DbShip[];
|
|
private oops: DbOops[];
|
|
private lastDeployed: string | null;
|
|
private dbError: string | null;
|
|
private theme: Theme;
|
|
private onClose: () => void;
|
|
private cachedWidth?: number;
|
|
private cachedLines?: string[];
|
|
|
|
constructor(ships: DbShip[], oops: DbOops[], lastDeployed: string | null, dbError: string | null, theme: Theme, onClose: () => void) {
|
|
this.ships = ships;
|
|
this.oops = oops;
|
|
this.lastDeployed = lastDeployed;
|
|
this.dbError = dbError;
|
|
this.theme = theme;
|
|
this.onClose = onClose;
|
|
}
|
|
|
|
handleInput(data: string): void {
|
|
if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
|
|
this.onClose();
|
|
}
|
|
}
|
|
|
|
render(width: number): string[] {
|
|
if (this.cachedLines && this.cachedWidth === width) return this.cachedLines;
|
|
|
|
const lines: string[] = [];
|
|
const th = this.theme;
|
|
|
|
lines.push("");
|
|
lines.push(truncateToWidth(
|
|
th.fg("borderMuted", "─".repeat(3)) +
|
|
th.fg("accent", " 🚀 Calvana Ship Log (DB) ") +
|
|
th.fg("borderMuted", "─".repeat(Math.max(0, width - 30))),
|
|
width
|
|
));
|
|
lines.push("");
|
|
|
|
if (this.dbError) {
|
|
lines.push(truncateToWidth(` ${th.fg("error", `DB ERROR: ${this.dbError}`)}`, width));
|
|
lines.push("");
|
|
lines.push(truncateToWidth(` ${th.fg("dim", "Press Escape to close")}`, width));
|
|
lines.push("");
|
|
this.cachedWidth = width;
|
|
this.cachedLines = lines;
|
|
return lines;
|
|
}
|
|
|
|
if (this.ships.length === 0) {
|
|
lines.push(truncateToWidth(` ${th.fg("dim", "No ships yet.")}`, width));
|
|
} else {
|
|
const shipped = this.ships.filter(s => s.status === "shipped").length;
|
|
lines.push(truncateToWidth(` ${th.fg("muted", `${shipped}/${this.ships.length} shipped`)}`, width));
|
|
lines.push("");
|
|
for (const s of this.ships) {
|
|
const badge = s.status === "shipped" ? th.fg("success", "✓ SHIPPED ")
|
|
: s.status === "shipping" ? th.fg("warning", "● SHIPPING")
|
|
: th.fg("dim", "○ PLANNED ");
|
|
lines.push(truncateToWidth(` ${badge} ${th.fg("accent", `#${s.id}`)} ${th.fg("text", s.title)}`, width));
|
|
lines.push(truncateToWidth(` ${th.fg("dim", s.created)} · ${th.fg("dim", s.metric)}`, width));
|
|
}
|
|
}
|
|
|
|
if (this.oops.length > 0) {
|
|
lines.push("");
|
|
lines.push(truncateToWidth(` ${th.fg("warning", "💥 Oops Log")}`, width));
|
|
for (const o of this.oops) {
|
|
lines.push(truncateToWidth(` ${th.fg("error", "─")} ${th.fg("muted", o.description)} ${th.fg("dim", `(${o.fixTime})`)}`, width));
|
|
}
|
|
}
|
|
|
|
lines.push("");
|
|
if (this.lastDeployed) {
|
|
lines.push(truncateToWidth(` ${th.fg("dim", `Last deployed: ${this.lastDeployed}`)}`, width));
|
|
}
|
|
lines.push(truncateToWidth(` ${th.fg("dim", "Press Escape to close")}`, width));
|
|
lines.push("");
|
|
|
|
this.cachedWidth = width;
|
|
this.cachedLines = lines;
|
|
return lines;
|
|
}
|
|
|
|
invalidate(): void {
|
|
this.cachedWidth = undefined;
|
|
this.cachedLines = undefined;
|
|
}
|
|
}
|
|
|
|
// Interface re-exports for the component
|
|
interface DbShip { id: number; title: string; status: string; metric: string; created: string; }
|
|
interface DbOops { id: number; description: string; fixTime: string; commitLink: string; created: string; }
|
|
|
|
// ════════════════════════════════════════════════════════════════════
|
|
// HTML GENERATOR
|
|
// ════════════════════════════════════════════════════════════════════
|
|
|
|
function generateLivePageFromDb(
|
|
ships: Array<{ id: number; title: string; status: string; metric: 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 shipsByDate = new Map<string, typeof ships>();
|
|
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);
|
|
}
|
|
|
|
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);
|
|
|
|
return ` <div class="card">
|
|
<div class="card-header">
|
|
<span class="card-id">#${s.id}</span>
|
|
<span class="card-title">${escapeHtml(s.title)}</span>
|
|
<span class="badge ${badgeClass}">${badgeLabel}</span>
|
|
</div>
|
|
<p class="metric">${escapeHtml(s.metric)}</p>
|
|
</div>`;
|
|
}).join("\n");
|
|
|
|
shipSections += `
|
|
<section class="section day-section">
|
|
<h2 class="day-header">${formatDate(date)} <span class="day-count">${dateShips.length} ship${dateShips.length !== 1 ? "s" : ""}</span></h2>
|
|
<div class="card-grid">\n${cards}\n </div>
|
|
</section>`;
|
|
}
|
|
|
|
const oopsEntries = oops.length > 0
|
|
? oops.map(o => ` <div class="oops-entry">
|
|
<span class="oops-id">#${o.id}</span>
|
|
<span>${escapeHtml(o.description)}${o.fixTime !== "—" ? ` <em>Fixed in ${escapeHtml(o.fixTime)}.</em>` : ""}</span>
|
|
${o.commitLink && o.commitLink !== "#commit" ? `<a href="${escapeHtml(o.commitLink)}">→ commit</a>` : ""}
|
|
</div>`).join("\n")
|
|
: ` <div class="oops-entry"><span>Nothing broken yet. Give it time.</span></div>`;
|
|
|
|
return `<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>Calvana — Live Shipping Log</title>
|
|
<meta name="description" content="Intentional chaos. Full receipts. Watch the build happen in real time.">
|
|
<meta property="og:title" content="Calvana — Live Shipping Log">
|
|
<meta property="og:description" content="Intentional chaos. Full receipts.">
|
|
<meta property="og:type" content="website">
|
|
<meta property="og:url" content="https://${DEPLOY_CONFIG.domain}/live">
|
|
<meta name="twitter:card" content="summary">
|
|
<link rel="canonical" href="https://${DEPLOY_CONFIG.domain}/live">
|
|
<link rel="stylesheet" href="/css/style.css">
|
|
<style>
|
|
.stats-bar { display:flex; gap:2rem; margin:1.5rem 0 2rem; flex-wrap:wrap; }
|
|
.stat { text-align:center; }
|
|
.stat-num { font-size:2rem; font-weight:800; line-height:1; }
|
|
.stat-label { font-size:.75rem; text-transform:uppercase; letter-spacing:.08em; opacity:.5; margin-top:.25rem; }
|
|
.stat-num.green { color:#22c55e; }
|
|
.stat-num.amber { color:#f59e0b; }
|
|
.stat-num.red { color:#ef4444; }
|
|
.day-header { font-size:1.1rem; font-weight:700; margin-bottom:.75rem; display:flex; align-items:center; gap:.75rem; }
|
|
.day-count { font-size:.75rem; font-weight:500; opacity:.4; }
|
|
.day-section { margin-bottom:1.5rem; }
|
|
.card-id { font-size:.7rem; font-weight:700; opacity:.3; margin-right:.5rem; }
|
|
.card-details { margin-top:.5rem; font-size:.8rem; opacity:.65; line-height:1.5; }
|
|
.card-details ul { margin:.25rem 0; padding-left:1.25rem; }
|
|
.card-details li { margin-bottom:.15rem; }
|
|
.oops-id { font-size:.7rem; font-weight:700; opacity:.3; margin-right:.5rem; }
|
|
.card { position:relative; }
|
|
.card-header { display:flex; align-items:flex-start; gap:.5rem; flex-wrap:wrap; }
|
|
.card-title { flex:1; min-width:0; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<nav>
|
|
<div class="nav-inner">
|
|
<a href="/" class="logo">calvana<span>.exe</span></a>
|
|
<div class="nav-links">
|
|
<a href="/manifesto">/manifesto</a>
|
|
<a href="/live" class="active">/live</a>
|
|
<a href="/hire">/hire</a>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
<main class="page">
|
|
<h1 class="hero-title">Live Shipping Log</h1>
|
|
<p class="subtitle">Intentional chaos. Full receipts. Every ship ever.</p>
|
|
|
|
<div class="stats-bar">
|
|
<div class="stat"><div class="stat-num green">${shipped}</div><div class="stat-label">Shipped</div></div>
|
|
<div class="stat"><div class="stat-num amber">${shipping}</div><div class="stat-label">In Flight</div></div>
|
|
<div class="stat"><div class="stat-num">${ships.length}</div><div class="stat-label">Total</div></div>
|
|
<div class="stat"><div class="stat-num red">${oops.length}</div><div class="stat-label">Oops</div></div>
|
|
</div>
|
|
${shipSections}
|
|
|
|
<section class="section">
|
|
<div class="two-col">
|
|
<div class="col col-broke">
|
|
<h3>Rules I broke today</h3>
|
|
<ul>
|
|
<li>Didn't ask permission</li>
|
|
<li>Didn't wait for alignment</li>
|
|
<li>Didn't write a PRD</li>
|
|
<li>Didn't submit a normal application</li>
|
|
</ul>
|
|
</div>
|
|
<div class="col col-kept">
|
|
<h3>Rules I refuse to break</h3>
|
|
<ul>
|
|
<li>No silent failures</li>
|
|
<li>No unbounded AI spend</li>
|
|
<li>No hallucinations shipped to users</li>
|
|
<li>No deploy without rollback path</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="section">
|
|
<h2>Oops Log</h2>
|
|
<p class="subtitle" style="margin-bottom:1rem">If it's not here, I haven't broken it yet.</p>
|
|
<div class="oops-log">\n${oopsEntries}\n </div>
|
|
</section>
|
|
|
|
<footer>
|
|
<p class="footer-tagline">${SITE_CONFIG.tagline}</p>
|
|
<p style="margin-top:.4rem">Last updated: ${now} · ${ships.length} ships from PostgreSQL</p>
|
|
</footer>
|
|
</main>
|
|
</body>
|
|
</html>`;
|
|
}
|
|
|
|
function escapeHtml(str: string): string {
|
|
return str
|
|
.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")
|
|
.replace(/"/g, """).replace(/'/g, "'")
|
|
.replace(/\u2014/g, "—").replace(/\u2013/g, "–")
|
|
.replace(/\u2019/g, "’").replace(/\u2018/g, "‘")
|
|
.replace(/\u201c/g, "“").replace(/\u201d/g, "”")
|
|
.replace(/\u2026/g, "…").replace(/\u2192/g, "→")
|
|
.replace(/\u00a3/g, "£").replace(/\u00d7/g, "×");
|
|
}
|