persona section: scenario-based personas + cinematic photography + pain stats
PERSONA OVERHAUL: - Personas now defined by WHAT THEY DID, not job titles - 'Charity Manager' -> 'You organized the dinner' - 'Personal Fundraiser' -> 'You shared the link' - 'Volunteer' -> 'You were on the ground' - 'Organisation/Programme Manager' -> 'You claim the Gift Aid' SECTION HEADING: - Brand core insight: 'People don't break promises. Systems do.' - Eyebrow: 'THE PLEDGE GAP' - Sub: 'We built the missing system between I'll donate and the money arriving.' PAIN STATS (visual anchors): - £50,000 pledged / £22,000 collected (the gap) - 23 said I'll donate / 8 actually did - 40 pledges collected / 0 updates received - 200 rows, 47 typos / 6 hours every quarter COPY: Emotionally precise, tells each persona's specific story PHOTOGRAPHY (4 cinematic moment shots): - Dinner aftermath: empty table with lone pledge card, chandeliers - Phone: hands on WhatsApp at kitchen table, warm light - Volunteer: seen from behind, walking between gala tables with cards - Desk still life: laptop spreadsheet, papers, tea, window light - All 2:1 wide aspect, 2.7MB -> 260KB optimized
This commit is contained in:
@@ -502,32 +502,51 @@ Current state from DB: ${state.ships.length} ships (${state.ships.filter(s => s.
|
||||
async execute(_toolCallId, params, signal, onUpdate, _ctx) {
|
||||
onUpdate?.({ content: [{ type: "text", text: "Querying database for full ship log..." }] });
|
||||
|
||||
// ── Build a helper script on the remote server to avoid quoting hell ──
|
||||
// This is the ONLY way to reliably run psql inside docker inside incus inside ssh.
|
||||
// Previous approach with nested escaping silently returned empty results.
|
||||
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}\\\"'"`;
|
||||
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,'-'), COALESCE(details,' '), 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 {
|
||||
// 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 });
|
||||
// 1. Run the helper script via SSH to get ALL ships + oops from DB
|
||||
const dbResult = await pi.exec("bash", ["-c",
|
||||
`${sshBase} 'bash -s' << 'DBEOF'\n${HELPER_SCRIPT}\nDBEOF`
|
||||
], { signal, timeout: 30000 });
|
||||
|
||||
if (shipsResult.code !== 0) {
|
||||
if (dbResult.code !== 0) {
|
||||
return {
|
||||
content: [{ type: "text", text: `DB query failed: ${shipsResult.stderr}` }],
|
||||
details: { state: { ...state }, error: shipsResult.stderr },
|
||||
content: [{ type: "text", text: `DB query failed (code ${dbResult.code}): ${dbResult.stderr}\nstdout: ${dbResult.stdout?.slice(0, 200)}` }],
|
||||
details: { state: { ...state }, error: dbResult.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 });
|
||||
const output = dbResult.stdout || "";
|
||||
if (output.includes("ERR:NO_PG_CONTAINER")) {
|
||||
return {
|
||||
content: [{ type: "text", text: `ABORT: PostgreSQL container not found. Refusing to deploy.` }],
|
||||
details: { state: { ...state }, error: "PG container not found" },
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
// 2. Parse the structured output
|
||||
const shipsSection = output.split("===SHIPS===")[1]?.split("===OOPS===")[0]?.trim() || "";
|
||||
const oopsSection = output.split("===OOPS===")[1]?.split("===END===")[0]?.trim() || "";
|
||||
|
||||
// 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")) {
|
||||
for (const line of shipsSection.split("\n")) {
|
||||
if (!line.trim()) continue;
|
||||
const parts = line.split("|||");
|
||||
if (parts.length >= 6) {
|
||||
@@ -544,7 +563,7 @@ Current state from DB: ${state.ships.length} ships (${state.ships.filter(s => s.
|
||||
}
|
||||
|
||||
const dbOops: Array<{ id: number; description: string; fixTime: string; commitLink: string; created: string }> = [];
|
||||
for (const line of (oopsResult.stdout || "").trim().split("\n")) {
|
||||
for (const line of oopsSection.split("\n")) {
|
||||
if (!line.trim()) continue;
|
||||
const parts = line.split("|||");
|
||||
if (parts.length >= 4) {
|
||||
@@ -558,9 +577,19 @@ Current state from DB: ${state.ships.length} ships (${state.ships.filter(s => s.
|
||||
}
|
||||
}
|
||||
|
||||
onUpdate?.({ content: [{ type: "text", text: `Found ${dbShips.length} ships + ${dbOops.length} oops. Generating HTML...` }] });
|
||||
// ── SAFETY GATE: refuse to deploy if DB returned 0 ships ──
|
||||
// The DB has 48+ entries. If we get 0, the query broke silently.
|
||||
if (dbShips.length === 0) {
|
||||
return {
|
||||
content: [{ type: "text", text: `ABORT: DB query returned 0 ships. This would wipe the live site.\nRaw output (first 500 chars): ${output.slice(0, 500)}\n\nRefusing to deploy. Fix the DB query first.` }],
|
||||
details: { state: { ...state }, error: "0 ships from DB — refusing to deploy" },
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
// 4. Generate HTML from DB data
|
||||
onUpdate?.({ content: [{ type: "text", text: `Found ${dbShips.length} ships + ${dbOops.length} oops from DB. Generating HTML...` }] });
|
||||
|
||||
// 3. Generate HTML from DB data
|
||||
const liveHtml = generateLivePageFromDb(dbShips, dbOops);
|
||||
|
||||
if (params.dryRun) {
|
||||
@@ -570,9 +599,9 @@ Current state from DB: ${state.ships.length} ships (${state.ships.filter(s => s.
|
||||
};
|
||||
}
|
||||
|
||||
onUpdate?.({ content: [{ type: "text", text: "Deploying to server..." }] });
|
||||
onUpdate?.({ content: [{ type: "text", text: `Deploying ${dbShips.length} ships to server...` }] });
|
||||
|
||||
// 5. Deploy via base64
|
||||
// 4. Deploy via base64
|
||||
const b64Html = Buffer.from(liveHtml).toString("base64");
|
||||
const deployResult = await pi.exec("bash", ["-c",
|
||||
`${sshBase} "incus exec ${DEPLOY_CONFIG.container} -- bash -c 'echo ${b64Html} | base64 -d > ${DEPLOY_CONFIG.sitePath}/live/index.html'"`
|
||||
@@ -586,7 +615,7 @@ Current state from DB: ${state.ships.length} ships (${state.ships.filter(s => s.
|
||||
};
|
||||
}
|
||||
|
||||
// 6. Rebuild and update docker service
|
||||
// 5. Rebuild and update docker service
|
||||
const rebuildResult = await pi.exec("bash", ["-c",
|
||||
`${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 });
|
||||
@@ -595,7 +624,7 @@ Current state from DB: ${state.ships.length} ships (${state.ships.filter(s => s.
|
||||
state.lastDeployed = now;
|
||||
|
||||
return {
|
||||
content: [{ type: "text", text: `✓ Deployed to https://${DEPLOY_CONFIG.domain}/live — ${dbShips.length} ships + ${dbOops.length} oops from database\n${rebuildResult.stdout}` }],
|
||||
content: [{ type: "text", text: `✓ Deployed to https://${DEPLOY_CONFIG.domain}/live\n${dbShips.length} ships + ${dbOops.length} oops from database\n${rebuildResult.stdout}` }],
|
||||
details: { state: { ...state, lastDeployed: now } },
|
||||
};
|
||||
} catch (err: any) {
|
||||
|
||||
Reference in New Issue
Block a user