This commit is contained in:
Dominik Klarkowski
2026-06-16 09:43:48 +02:00
parent f0e87d8d11
commit 36407f534b
52 changed files with 3211 additions and 100 deletions

62
lib/models/analysis.ts Normal file
View File

@@ -0,0 +1,62 @@
import { ObjectId } from "mongodb";
import { getDb } from "@/lib/db";
export type AiAnalysisTargetType = "running" | "strength" | "dashboard";
export type AiAnalysisInput = {
targetType: AiAnalysisTargetType;
targetId: ObjectId;
summary: string;
tips: string[];
model: string;
};
export type AiAnalysis = AiAnalysisInput & {
_id: ObjectId;
createdAt: Date;
};
const COLLECTION = "ai_analyses";
async function getCollection() {
const db = await getDb();
return db.collection<AiAnalysis>(COLLECTION);
}
export async function saveAiAnalysis(input: AiAnalysisInput): Promise<AiAnalysis> {
const collection = await getCollection();
const doc = { ...input, _id: new ObjectId(), createdAt: new Date() };
await collection.insertOne(doc);
return doc;
}
export async function getLatestAnalysisForTarget(
targetType: AiAnalysisTargetType,
targetId: ObjectId
): Promise<AiAnalysis | null> {
const collection = await getCollection();
return collection.findOne({ targetType, targetId }, { sort: { createdAt: -1 } });
}
export async function getLatestAnalysis(): Promise<AiAnalysis | null> {
const collection = await getCollection();
return collection.findOne({}, { sort: { createdAt: -1 } });
}
const DASHBOARD_TARGET_ID = new ObjectId("000000000000000000000001");
export async function getDashboardAnalysis(): Promise<AiAnalysis | null> {
const collection = await getCollection();
return collection.findOne(
{ targetType: "dashboard", targetId: DASHBOARD_TARGET_ID },
{ sort: { createdAt: -1 } }
);
}
export async function saveDashboardAnalysis(
summary: string,
tips: string[],
model: string
): Promise<AiAnalysis> {
return saveAiAnalysis({ targetType: "dashboard", targetId: DASHBOARD_TARGET_ID, summary, tips, model });
}

40
lib/models/garmin-auth.ts Normal file
View File

@@ -0,0 +1,40 @@
import type { IOauth1Token } from "garmin-connect/dist/garmin/types";
import { getDb } from "@/lib/db";
import type { GarminPendingMfa } from "@/lib/garmin/sso";
const AUTH_COLLECTION = "garmin_auth";
const PENDING_COLLECTION = "garmin_login_pending";
type GarminAuthDoc = { _id: "tokens"; oauth1Token: IOauth1Token; updatedAt: Date };
type GarminPendingDoc = { _id: "pending"; state: GarminPendingMfa; createdAt: Date };
export async function getSavedOauth1Token(): Promise<IOauth1Token | null> {
const db = await getDb();
const doc = await db.collection<GarminAuthDoc>(AUTH_COLLECTION).findOne({ _id: "tokens" });
return doc?.oauth1Token ?? null;
}
export async function saveOauth1Token(oauth1Token: IOauth1Token): Promise<void> {
const db = await getDb();
await db
.collection<GarminAuthDoc>(AUTH_COLLECTION)
.updateOne({ _id: "tokens" }, { $set: { oauth1Token, updatedAt: new Date() } }, { upsert: true });
}
export async function savePendingMfaState(state: GarminPendingMfa): Promise<void> {
const db = await getDb();
await db
.collection<GarminPendingDoc>(PENDING_COLLECTION)
.updateOne({ _id: "pending" }, { $set: { state, createdAt: new Date() } }, { upsert: true });
}
export async function getPendingMfaState(): Promise<GarminPendingMfa | null> {
const db = await getDb();
const doc = await db.collection<GarminPendingDoc>(PENDING_COLLECTION).findOne({ _id: "pending" });
return doc?.state ?? null;
}
export async function clearPendingMfaState(): Promise<void> {
const db = await getDb();
await db.collection<GarminPendingDoc>(PENDING_COLLECTION).deleteOne({ _id: "pending" });
}

96
lib/models/running.ts Normal file
View File

@@ -0,0 +1,96 @@
import { ObjectId } from "mongodb";
import { z } from "zod";
import { getDb } from "@/lib/db";
export const runningActivitySchema = z.object({
garminActivityId: z.number().int(),
name: z.string().min(1),
startTime: z.date(),
durationSec: z.number().positive(),
distanceM: z.number().nonnegative(),
avgPaceSecPerKm: z.number().nonnegative(),
avgHr: z.number().positive().optional(),
maxHr: z.number().positive().optional(),
calories: z.number().nonnegative().optional(),
elevationGainM: z.number().nonnegative().optional(),
avgCadence: z.number().nonnegative().optional(),
avgVerticalOscillationCm: z.number().nonnegative().optional(),
avgGroundContactTimeMs: z.number().nonnegative().optional(),
avgStrideLengthCm: z.number().nonnegative().optional(),
avgGroundContactBalanceLeftPct: z.number().nonnegative().optional(),
avgVerticalRatioPct: z.number().nonnegative().optional(),
vo2Max: z.number().nonnegative().optional(),
aerobicTrainingEffect: z.number().nonnegative().optional(),
anaerobicTrainingEffect: z.number().nonnegative().optional(),
trainingEffectLabel: z.string().optional(),
avgPowerW: z.number().nonnegative().optional(),
maxPowerW: z.number().nonnegative().optional(),
normPowerW: z.number().nonnegative().optional(),
avgRespirationRate: z.number().nonnegative().optional(),
hasRoute: z.boolean().optional(),
});
export type RunningActivityInput = z.infer<typeof runningActivitySchema>;
export type RoutePoint = [number, number];
export type RunningActivity = RunningActivityInput & {
_id: ObjectId;
createdAt: Date;
routePoints?: RoutePoint[];
};
const COLLECTION = "running_activities";
const SYNC_STATE_COLLECTION = "sync_state";
async function getCollection() {
const db = await getDb();
const collection = db.collection<RunningActivity>(COLLECTION);
await collection.createIndex({ garminActivityId: 1 }, { unique: true });
return collection;
}
export async function upsertRunningActivity(activity: RunningActivityInput): Promise<void> {
const collection = await getCollection();
await collection.updateOne(
{ garminActivityId: activity.garminActivityId },
{
$set: activity,
$setOnInsert: { createdAt: new Date() },
},
{ upsert: true }
);
}
export async function listRunningActivities(): Promise<RunningActivity[]> {
const collection = await getCollection();
return collection.find().sort({ startTime: -1 }).toArray();
}
export async function getRunningActivity(id: string): Promise<RunningActivity | null> {
const collection = await getCollection();
return collection.findOne({ _id: new ObjectId(id) });
}
export async function setRunningActivityRoutePoints(
garminActivityId: number,
points: RoutePoint[]
): Promise<void> {
const collection = await getCollection();
await collection.updateOne({ garminActivityId }, { $set: { routePoints: points } });
}
type SyncState = { _id: "garmin"; lastSyncAt: Date };
export async function getLastSyncAt(): Promise<Date | null> {
const db = await getDb();
const state = await db.collection<SyncState>(SYNC_STATE_COLLECTION).findOne({ _id: "garmin" });
return state?.lastSyncAt ?? null;
}
export async function setLastSyncAt(date: Date): Promise<void> {
const db = await getDb();
await db
.collection<SyncState>(SYNC_STATE_COLLECTION)
.updateOne({ _id: "garmin" }, { $set: { lastSyncAt: date } }, { upsert: true });
}

66
lib/models/strength.ts Normal file
View File

@@ -0,0 +1,66 @@
import { ObjectId } from "mongodb";
import { z } from "zod";
import { getDb } from "@/lib/db";
export const strengthSetSchema = z.object({
order: z.number().int().positive(),
weightKg: z.number().positive().optional(),
reps: z.number().int().positive().optional(),
});
export const strengthExerciseSchema = z.object({
name: z.string().min(1),
notes: z.string().optional(),
sets: z.array(strengthSetSchema),
});
export const strengthWorkoutSchema = z.object({
date: z.date(),
name: z.string().min(1),
notes: z.string().optional(),
exercises: z.array(strengthExerciseSchema),
sourceUrl: z.string().optional(),
sourceKey: z.string().min(1),
});
export type StrengthSet = z.infer<typeof strengthSetSchema>;
export type StrengthExercise = z.infer<typeof strengthExerciseSchema>;
export type StrengthWorkoutInput = z.infer<typeof strengthWorkoutSchema>;
export type StrengthWorkout = StrengthWorkoutInput & {
_id: ObjectId;
createdAt: Date;
};
const COLLECTION = "strength_workouts";
async function getCollection() {
const db = await getDb();
const collection = db.collection<StrengthWorkout>(COLLECTION);
await collection.createIndex({ sourceKey: 1 }, { unique: true });
return collection;
}
export async function upsertStrengthWorkout(
workout: StrengthWorkoutInput
): Promise<void> {
const collection = await getCollection();
await collection.updateOne(
{ sourceKey: workout.sourceKey },
{
$set: workout,
$setOnInsert: { createdAt: new Date() },
},
{ upsert: true }
);
}
export async function listStrengthWorkouts(): Promise<StrengthWorkout[]> {
const collection = await getCollection();
return collection.find().sort({ date: -1 }).toArray();
}
export async function getStrengthWorkout(id: string): Promise<StrengthWorkout | null> {
const collection = await getCollection();
return collection.findOne({ _id: new ObjectId(id) });
}