Refactor coach to plain Appwrite storage with integrated overview UI.

Remove client-side encryption, migrate coach_chats schema, fix the Ollama proxy, and embed coach on overview alongside the dedicated tab.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Ned Halksworth
2026-05-23 20:25:21 +01:00
parent d312321ffa
commit dc9fbf496d
11 changed files with 1182 additions and 958 deletions
+90
View File
@@ -0,0 +1,90 @@
import { groupByFlavour } from "./metrics";
type GreetingInput = {
name: string;
todayCans: number;
favouriteFlavour: string;
currentStreak: number;
todayCaffeineMg: number;
allTimeCans: number;
};
type GreetingResult = {
badge: string;
headline: string;
subline: string;
};
export function getBstHour(date = new Date()) {
const hour = new Intl.DateTimeFormat("en-GB", {
timeZone: "Europe/London",
hour: "numeric",
hour12: false,
}).format(date);
return Number.parseInt(hour, 10);
}
export function buildDynamicGreeting(input: GreetingInput): GreetingResult {
const hour = getBstHour();
const timeLabel = timeOfDayLabel(hour);
const cans = input.todayCans;
const favourite =
input.favouriteFlavour === "None yet" ? null : input.favouriteFlavour;
const streak = input.currentStreak;
const badge = cans === 0 ? `${timeLabel} · clear slate` : `${timeLabel} · ${cans} today`;
let headline: string;
if (cans === 0) {
headline =
streak > 0
? `${input.name}, nothing logged yet today — ${streak}-day streak still alive.`
: `${input.name}, no Red Bulls logged yet this ${hour < 12 ? "morning" : hour < 17 ? "afternoon" : "evening"}.`;
} else if (cans === 1) {
headline = `${input.name}, one Red Bull in so far today.`;
} else if (cans <= 3) {
headline = `${input.name}, ${cans} Red Bulls today — steady pace.`;
} else {
headline = `${input.name}, ${cans} Red Bulls today — worth watching the caffeine curve.`;
}
const flavourLine = favourite
? cans > 0
? `Today's top pick looks like ${favourite}.`
: `All-time favourite: ${favourite} (${input.allTimeCans} cans logged).`
: "Your flavour story is just getting started.";
const caffeineLine =
cans > 0 && input.todayCaffeineMg > 0
? `~${Math.round(input.todayCaffeineMg)}mg caffeine so far.`
: hour >= 17 && cans === 0
? "Evening reset — clean slate if you want it."
: hour >= 22
? "Late night — pace yourself if you're still going."
: "Log an intake to unlock today's signals.";
return {
badge,
headline,
subline: [flavourLine, caffeineLine].join(" "),
};
}
export function buildFlavourHistorySummary(entries: Parameters<typeof groupByFlavour>[0]) {
const breakdown = groupByFlavour(entries);
if (!breakdown.length) return "No flavour history yet.";
return breakdown
.map((item, index) => {
const rank = index === 0 ? " (all-time favourite)" : "";
return `- ${item.name}: ${item.value} cans${rank}`;
})
.join("\n");
}
function timeOfDayLabel(hour: number) {
if (hour >= 5 && hour < 12) return "morning";
if (hour >= 12 && hour < 17) return "afternoon";
if (hour >= 17 && hour < 22) return "evening";
return "night";
}