feat: remove FPX, add UK charity persona features
- Remove FPX payment rail entirely (Malaysian, not UK) - Add volunteer portal (/v/[code]) with live pledge tracking - Add public event page (/e/[slug]) with progress bar + social proof - Add fundraiser leaderboard (/dashboard/events/[id]/leaderboard) - Add WhatsApp share buttons on confirmation, bank instructions, volunteer view - Enhanced Gift Aid UX with +25% bonus display and HMRC declaration text - Gift Aid report export (HMRC-ready CSV filter) - Volunteer view link + WhatsApp share on QR code cards - Updated home page: 4 personas, 3 UK payment rails, 8 features - Public event API endpoint with privacy-safe donor name truncation - Volunteer API with stats, conversion rate, auto-refresh
This commit is contained in:
719
.pi/extensions/calvana-shiplog.ts
Normal file
719
.pi/extensions/calvana-shiplog.ts
Normal file
@@ -0,0 +1,719 @@
|
||||
/**
|
||||
* Calvana Ship Log Extension
|
||||
*
|
||||
* Automatically tracks what you're shipping and updates the live Calvana site.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* How it works:
|
||||
* 1. When you work on tasks, the LLM uses calvana_ship to track progress
|
||||
* 2. If something breaks, calvana_oops logs it
|
||||
* 3. calvana_deploy rebuilds the /live page HTML and pushes it to the server
|
||||
* 4. The extension auto-injects context so the LLM knows to track ships
|
||||
*
|
||||
* Edit the SSH/deploy config in the DEPLOY_CONFIG section below.
|
||||
*/
|
||||
|
||||
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 — Edit these to change deploy target, copy, links
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
|
||||
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.",
|
||||
};
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
// TYPES
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
|
||||
type ShipStatus = "planned" | "shipping" | "shipped";
|
||||
|
||||
interface ShipEntry {
|
||||
id: number;
|
||||
title: string;
|
||||
status: ShipStatus;
|
||||
timestamp: string;
|
||||
metric: string;
|
||||
prLink: string;
|
||||
deployLink: string;
|
||||
loomLink: string;
|
||||
}
|
||||
|
||||
interface OopsEntry {
|
||||
id: number;
|
||||
description: string;
|
||||
fixTime: string;
|
||||
commitLink: string;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
interface ShipLogState {
|
||||
ships: ShipEntry[];
|
||||
oops: OopsEntry[];
|
||||
nextShipId: number;
|
||||
nextOopsId: number;
|
||||
lastDeployed: string | null;
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
// 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) {
|
||||
// ── State ──
|
||||
let state: ShipLogState = {
|
||||
ships: [],
|
||||
oops: [],
|
||||
nextShipId: 1,
|
||||
nextOopsId: 1,
|
||||
lastDeployed: null,
|
||||
};
|
||||
|
||||
// ── State reconstruction from session ──
|
||||
const reconstructState = (ctx: ExtensionContext) => {
|
||||
state = { ships: [], oops: [], nextShipId: 1, nextOopsId: 1, lastDeployed: null };
|
||||
|
||||
for (const entry of ctx.sessionManager.getBranch()) {
|
||||
if (entry.type !== "message") continue;
|
||||
const msg = entry.message;
|
||||
if (msg.role !== "toolResult") continue;
|
||||
if (msg.toolName === "calvana_ship" || msg.toolName === "calvana_oops" || msg.toolName === "calvana_deploy") {
|
||||
const details = msg.details as { state?: ShipLogState } | undefined;
|
||||
if (details?.state) {
|
||||
state = details.state;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
pi.on("session_start", async (_event, ctx) => {
|
||||
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`));
|
||||
}
|
||||
});
|
||||
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));
|
||||
|
||||
// ── 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
|
||||
|
||||
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.
|
||||
|
||||
Current ships: ${state.ships.length} (${state.ships.filter(s => s.status === "shipped").length} shipped)
|
||||
Current oops: ${state.oops.length}
|
||||
`;
|
||||
return {
|
||||
systemPrompt: event.systemPrompt + shipContext,
|
||||
};
|
||||
});
|
||||
|
||||
// ── Update status bar on turn end ──
|
||||
pi.on("turn_end", async (_event, ctx) => {
|
||||
if (ctx.hasUI) {
|
||||
const theme = ctx.ui.theme;
|
||||
const shipped = state.ships.filter(s => s.status === "shipped").length;
|
||||
const shipping = state.ships.filter(s => s.status === "shipping").length;
|
||||
const total = state.ships.length;
|
||||
let statusText = `🚀 ${shipped}/${total} shipped`;
|
||||
if (shipping > 0) statusText += ` · ${shipping} in flight`;
|
||||
if (state.lastDeployed) statusText += ` · last deploy ${state.lastDeployed}`;
|
||||
ctx.ui.setStatus("calvana", theme.fg("dim", statusText));
|
||||
}
|
||||
});
|
||||
|
||||
// ════════════════════════════════════════════════════════════════
|
||||
// TOOL: calvana_ship
|
||||
// ════════════════════════════════════════════════════════════════
|
||||
|
||||
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) {
|
||||
const now = new Date().toISOString().replace("T", " ").slice(0, 19) + " GMT+8";
|
||||
|
||||
switch (params.action) {
|
||||
case "add": {
|
||||
if (!params.title) {
|
||||
return {
|
||||
content: [{ type: "text", text: "Error: title required" }],
|
||||
details: { state: { ...state }, error: "title required" },
|
||||
};
|
||||
}
|
||||
const entry: ShipEntry = {
|
||||
id: state.nextShipId++,
|
||||
title: params.title,
|
||||
status: (params.status as ShipStatus) || "planned",
|
||||
timestamp: now,
|
||||
metric: params.metric || "—",
|
||||
prLink: params.prLink || "#pr",
|
||||
deployLink: params.deployLink || "#deploy",
|
||||
loomLink: params.loomLink || "#loomclip",
|
||||
};
|
||||
state.ships.push(entry);
|
||||
return {
|
||||
content: [{ type: "text", text: `Ship #${entry.id} added: "${entry.title}" [${entry.status}]` }],
|
||||
details: { state: { ...state, ships: [...state.ships] } },
|
||||
};
|
||||
}
|
||||
|
||||
case "update": {
|
||||
if (params.id === undefined) {
|
||||
return {
|
||||
content: [{ type: "text", text: "Error: id required for update" }],
|
||||
details: { state: { ...state }, error: "id required" },
|
||||
};
|
||||
}
|
||||
const ship = state.ships.find(s => s.id === params.id);
|
||||
if (!ship) {
|
||||
return {
|
||||
content: [{ type: "text", text: `Ship #${params.id} not found` }],
|
||||
details: { state: { ...state }, error: `#${params.id} not found` },
|
||||
};
|
||||
}
|
||||
if (params.status) ship.status = params.status as ShipStatus;
|
||||
if (params.metric) ship.metric = params.metric;
|
||||
if (params.prLink) ship.prLink = params.prLink;
|
||||
if (params.deployLink) ship.deployLink = params.deployLink;
|
||||
if (params.loomLink) ship.loomLink = params.loomLink;
|
||||
ship.timestamp = now;
|
||||
return {
|
||||
content: [{ type: "text", text: `Ship #${ship.id} updated: "${ship.title}" [${ship.status}]` }],
|
||||
details: { state: { ...state, ships: [...state.ships] } },
|
||||
};
|
||||
}
|
||||
|
||||
case "list": {
|
||||
if (state.ships.length === 0) {
|
||||
return {
|
||||
content: [{ type: "text", text: "No ships logged yet." }],
|
||||
details: { state: { ...state } },
|
||||
};
|
||||
}
|
||||
const lines = state.ships.map(s =>
|
||||
`#${s.id} [${s.status.toUpperCase()}] ${s.title} (${s.timestamp}) — ${s.metric}`
|
||||
);
|
||||
return {
|
||||
content: [{ type: "text", text: lines.join("\n") }],
|
||||
details: { state: { ...state } },
|
||||
};
|
||||
}
|
||||
|
||||
default:
|
||||
return {
|
||||
content: [{ type: "text", text: `Unknown action: ${params.action}` }],
|
||||
details: { state: { ...state } },
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
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 details = result.details as { state?: ShipLogState; error?: string } | undefined;
|
||||
if (details?.error) return new Text(theme.fg("error", `Error: ${details.error}`), 0, 0);
|
||||
|
||||
const st = details?.state;
|
||||
if (!st || st.ships.length === 0) return new Text(theme.fg("dim", "No ships"), 0, 0);
|
||||
|
||||
const shipped = st.ships.filter(s => s.status === "shipped").length;
|
||||
const total = st.ships.length;
|
||||
let text = theme.fg("success", `${shipped}/${total} shipped`);
|
||||
|
||||
if (expanded) {
|
||||
for (const s of st.ships) {
|
||||
const badge = s.status === "shipped" ? theme.fg("success", "✓")
|
||||
: s.status === "shipping" ? theme.fg("warning", "●")
|
||||
: theme.fg("dim", "○");
|
||||
text += `\n ${badge} ${theme.fg("accent", `#${s.id}`)} ${theme.fg("muted", s.title)}`;
|
||||
}
|
||||
}
|
||||
return new Text(text, 0, 0);
|
||||
},
|
||||
});
|
||||
|
||||
// ════════════════════════════════════════════════════════════════
|
||||
// TOOL: calvana_oops
|
||||
// ════════════════════════════════════════════════════════════════
|
||||
|
||||
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) {
|
||||
const now = new Date().toISOString().replace("T", " ").slice(0, 19) + " GMT+8";
|
||||
|
||||
switch (params.action) {
|
||||
case "add": {
|
||||
if (!params.description) {
|
||||
return {
|
||||
content: [{ type: "text", text: "Error: description required" }],
|
||||
details: { state: { ...state }, error: "description required" },
|
||||
};
|
||||
}
|
||||
const entry: OopsEntry = {
|
||||
id: state.nextOopsId++,
|
||||
description: params.description,
|
||||
fixTime: params.fixTime || "—",
|
||||
commitLink: params.commitLink || "#commit",
|
||||
timestamp: now,
|
||||
};
|
||||
state.oops.push(entry);
|
||||
return {
|
||||
content: [{ type: "text", text: `Oops #${entry.id}: "${entry.description}" (fixed in ${entry.fixTime})` }],
|
||||
details: { state: { ...state, oops: [...state.oops] } },
|
||||
};
|
||||
}
|
||||
|
||||
case "list": {
|
||||
if (state.oops.length === 0) {
|
||||
return {
|
||||
content: [{ type: "text", text: "No oops entries. Clean run so far." }],
|
||||
details: { state: { ...state } },
|
||||
};
|
||||
}
|
||||
const lines = state.oops.map(o =>
|
||||
`#${o.id} ${o.description} — fixed in ${o.fixTime}`
|
||||
);
|
||||
return {
|
||||
content: [{ type: "text", text: lines.join("\n") }],
|
||||
details: { state: { ...state } },
|
||||
};
|
||||
}
|
||||
|
||||
default:
|
||||
return {
|
||||
content: [{ type: "text", text: `Unknown action: ${params.action}` }],
|
||||
details: { state: { ...state } },
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
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 details = result.details as { state?: ShipLogState; error?: string } | undefined;
|
||||
if (details?.error) return new Text(theme.fg("error", `Error: ${details.error}`), 0, 0);
|
||||
const text = result.content[0];
|
||||
return new Text(theme.fg("warning", text?.type === "text" ? text.text : ""), 0, 0);
|
||||
},
|
||||
});
|
||||
|
||||
// ════════════════════════════════════════════════════════════════
|
||||
// TOOL: calvana_deploy
|
||||
// ════════════════════════════════════════════════════════════════
|
||||
|
||||
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: "Generating HTML..." }] });
|
||||
|
||||
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..." }] });
|
||||
|
||||
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
|
||||
'"`;
|
||||
|
||||
// Use base64 to avoid all escaping nightmares
|
||||
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'"`
|
||||
], { signal, timeout: 30000 });
|
||||
|
||||
if (deployResult.code !== 0) {
|
||||
return {
|
||||
content: [{ type: "text", text: `Deploy failed: ${deployResult.stderr}` }],
|
||||
details: { state: { ...state }, error: deployResult.stderr },
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
// 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'"`
|
||||
], { 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}` }],
|
||||
details: { state: { ...state, lastDeployed: now } },
|
||||
};
|
||||
} catch (err: any) {
|
||||
return {
|
||||
content: [{ type: "text", text: `Deploy error: ${err.message}` }],
|
||||
details: { state: { ...state }, error: err.message },
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
renderCall(_args, theme) {
|
||||
return new Text(theme.fg("toolTitle", theme.bold("🌐 deploy calvana")), 0, 0);
|
||||
},
|
||||
|
||||
renderResult(result, _options, theme) {
|
||||
const details = result.details as { error?: string } | undefined;
|
||||
if (details?.error) return new Text(theme.fg("error", `✗ ${details.error}`), 0, 0);
|
||||
return new Text(theme.fg("success", `✓ Live at https://${DEPLOY_CONFIG.domain}/live`), 0, 0);
|
||||
},
|
||||
});
|
||||
|
||||
// ════════════════════════════════════════════════════════════════
|
||||
// COMMAND: /ships
|
||||
// ════════════════════════════════════════════════════════════════
|
||||
|
||||
pi.registerCommand("ships", {
|
||||
description: "View current Calvana shipping log",
|
||||
handler: async (_args, ctx) => {
|
||||
if (!ctx.hasUI) {
|
||||
ctx.ui.notify("Requires interactive mode", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
await ctx.ui.custom<void>((_tui, theme, _kb, done) => {
|
||||
return new ShipLogComponent(state, 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;
|
||||
|
||||
// Queue a deploy via the LLM
|
||||
pi.sendUserMessage("Use calvana_deploy to push the current ship log to the live site.", { deliverAs: "followUp" });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
// UI COMPONENT: /ships viewer
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
|
||||
class ShipLogComponent {
|
||||
private state: ShipLogState;
|
||||
private theme: Theme;
|
||||
private onClose: () => void;
|
||||
private cachedWidth?: number;
|
||||
private cachedLines?: string[];
|
||||
|
||||
constructor(state: ShipLogState, theme: Theme, onClose: () => void) {
|
||||
this.state = state;
|
||||
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 ") +
|
||||
th.fg("borderMuted", "─".repeat(Math.max(0, width - 26))),
|
||||
width
|
||||
));
|
||||
lines.push("");
|
||||
|
||||
// Ships
|
||||
if (this.state.ships.length === 0) {
|
||||
lines.push(truncateToWidth(` ${th.fg("dim", "No ships yet.")}`, width));
|
||||
} else {
|
||||
const shipped = this.state.ships.filter(s => s.status === "shipped").length;
|
||||
lines.push(truncateToWidth(
|
||||
` ${th.fg("muted", `${shipped}/${this.state.ships.length} shipped`)}`,
|
||||
width
|
||||
));
|
||||
lines.push("");
|
||||
|
||||
for (const s of this.state.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.timestamp)} · ${th.fg("dim", s.metric)}`,
|
||||
width
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Oops
|
||||
if (this.state.oops.length > 0) {
|
||||
lines.push("");
|
||||
lines.push(truncateToWidth(` ${th.fg("warning", "💥 Oops Log")}`, width));
|
||||
for (const o of this.state.oops) {
|
||||
lines.push(truncateToWidth(
|
||||
` ${th.fg("error", "─")} ${th.fg("muted", o.description)} ${th.fg("dim", `(${o.fixTime})`)}`,
|
||||
width
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
lines.push("");
|
||||
if (this.state.lastDeployed) {
|
||||
lines.push(truncateToWidth(` ${th.fg("dim", `Last deployed: ${this.state.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;
|
||||
}
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
// HTML GENERATOR — Builds the /live page from current state
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
|
||||
function generateLivePageHtml(state: ShipLogState): string {
|
||||
const now = new Date().toISOString();
|
||||
|
||||
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" ? " ✓" : "";
|
||||
|
||||
return ` <div class="card">
|
||||
<div class="card-header">
|
||||
<span class="card-title">${escapeHtml(s.title)}${titleSuffix}</span>
|
||||
<span class="badge ${badgeClass}">${badgeLabel}</span>
|
||||
</div>
|
||||
<p class="card-meta">⏱ ${escapeHtml(s.timestamp)}</p>
|
||||
<p class="metric">What moved: ${escapeHtml(s.metric)}</p>
|
||||
<div class="card-links"><a href="${escapeHtml(s.prLink)}">PR</a><a href="${escapeHtml(s.deployLink)}">Deploy</a><a href="${escapeHtml(s.loomLink)}">Loom clip</a></div>
|
||||
</div>`;
|
||||
}).join("\n");
|
||||
|
||||
const oopsEntries = state.oops.map(o => {
|
||||
return ` <div class="oops-entry">
|
||||
<span>${escapeHtml(o.description)}${o.fixTime !== "—" ? ` Fixed in ${escapeHtml(o.fixTime)}.` : ""}</span>
|
||||
<a href="${escapeHtml(o.commitLink)}">→ commit</a>
|
||||
</div>`;
|
||||
}).join("\n");
|
||||
|
||||
// If no ships yet, show placeholder
|
||||
const shipsSection = state.ships.length > 0 ? shipCards : ` <div class="card">
|
||||
<div class="card-header">
|
||||
<span class="card-title">Warming up...</span>
|
||||
<span class="badge badge-planned">Planned</span>
|
||||
</div>
|
||||
<p class="card-meta">⏱ —</p>
|
||||
<p class="metric">What moved: —</p>
|
||||
</div>`;
|
||||
|
||||
const oopsSection = state.oops.length > 0 ? oopsEntries : ` <div class="oops-entry">
|
||||
<span>Nothing broken yet. Give it time.</span>
|
||||
<a href="#commit">→ waiting</a>
|
||||
</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">
|
||||
</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.</p>
|
||||
|
||||
<section class="section">
|
||||
<h2>Today's Ships</h2>
|
||||
<div class="card-grid">
|
||||
${shipsSection}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<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">
|
||||
${oopsSection}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
<p class="footer-tagline">${SITE_CONFIG.tagline}</p>
|
||||
<p style="margin-top:.4rem">Last updated: ${now}</p>
|
||||
</footer>
|
||||
</main>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
function escapeHtml(str: string): string {
|
||||
return str
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
Reference in New Issue
Block a user