ci: trigga på master + formatfix inför Gitea Actions
CI / Typecheck, test & build (push) Failing after 2s

This commit is contained in:
Sven (AAMOS AI)
2026-08-13 17:25:14 +07:00
parent 61d60931ad
commit c32a7e33c7
141 changed files with 40555 additions and 45210 deletions
+17 -4
View File
@@ -21,7 +21,9 @@ export async function adminAnalyticsRoutes(app: FastifyInstance) {
/** Compute a funnel between two UTC dates (YYYY-MM-DD). */
app.get("/admin/v1/analytics/funnels/:name", admin, async (req) => {
const params = z.object({ name: z.enum(Object.keys(FUNNELS) as [string, ...string[]]) }).parse(req.params);
const params = z
.object({ name: z.enum(Object.keys(FUNNELS) as [string, ...string[]]) })
.parse(req.params);
const query = z
.object({
startDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
@@ -69,7 +71,10 @@ export async function adminAnalyticsRoutes(app: FastifyInstance) {
const querySql = `WITH ${ctes.join(", ")} SELECT ${selects.join(", ")}`;
const result = await app.db.execute(sql.raw(querySql));
const row = (Array.isArray(result) ? result[0] : (result.rows[0] ?? {})) as Record<string, number>;
const row = (Array.isArray(result) ? result[0] : (result.rows[0] ?? {})) as Record<
string,
number
>;
const stepCounts = steps.map((event, i) => ({
step: i + 1,
@@ -97,7 +102,10 @@ export async function adminAnalyticsRoutes(app: FastifyInstance) {
const query = z
.object({
startDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
endDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
endDate: z
.string()
.regex(/^\d{4}-\d{2}-\d{2}$/)
.optional(),
})
.parse(req.query);
@@ -147,7 +155,12 @@ export async function adminAnalyticsRoutes(app: FastifyInstance) {
string,
{ cohortSize: number; retention: Array<{ day: number; active: number; rate: number }> }
> = {};
for (const r of rows as Array<{ cohort_date: string; day: number; cohort_size: number; active_users: number }>) {
for (const r of rows as Array<{
cohort_date: string;
day: number;
cohort_size: number;
active_users: number;
}>) {
const key = String(r.cohort_date).slice(0, 10);
if (!cohorts[key]) {
cohorts[key] = { cohortSize: Number(r.cohort_size), retention: [] };
+10 -2
View File
@@ -1,6 +1,11 @@
import type { FastifyInstance } from "fastify";
import { eq } from "drizzle-orm";
import { schema, evaluateReleaseGates, seedReleaseGates, summarizeReleaseGates } from "@app/database";
import {
schema,
evaluateReleaseGates,
seedReleaseGates,
summarizeReleaseGates,
} from "@app/database";
import { releaseGateUpdateSchema } from "@app/validation";
import { z } from "zod";
import { errors, parse } from "../lib/errors.js";
@@ -40,7 +45,10 @@ export async function adminReleaseGateRoutes(app: FastifyInstance) {
/** List current gate definitions without re-evaluating. */
app.get("/admin/v1/release-gates", admin, async () => {
const gates = await app.db.select().from(schema.releaseGates).orderBy(schema.releaseGates.category, schema.releaseGates.gateKey);
const gates = await app.db
.select()
.from(schema.releaseGates)
.orderBy(schema.releaseGates.category, schema.releaseGates.gateKey);
const summary = summarizeReleaseGates(
gates.map((g) => ({
gateKey: g.gateKey,
+6 -2
View File
@@ -64,7 +64,9 @@ export async function adminWorkersRoutes(app: FastifyInstance) {
const { id } = req.params as { id: string };
const job = await Job.fromId(dlq, id);
if (!job) {
return reply.status(404).send({ error: { code: "NOT_FOUND", message: "Jobbet finns inte i dead letter queue." } });
return reply
.status(404)
.send({ error: { code: "NOT_FOUND", message: "Jobbet finns inte i dead letter queue." } });
}
const mainQueue = new Queue(JOB_QUEUE_NAME, { connection: app.redis });
@@ -82,7 +84,9 @@ export async function adminWorkersRoutes(app: FastifyInstance) {
const { id } = req.params as { id: string };
const job = await Job.fromId(dlq, id);
if (!job) {
return reply.status(404).send({ error: { code: "NOT_FOUND", message: "Jobbet finns inte i dead letter queue." } });
return reply
.status(404)
.send({ error: { code: "NOT_FOUND", message: "Jobbet finns inte i dead letter queue." } });
}
await job.remove();
return reply.send({ ok: true, message: "Jobbet har tagits bort från dead letter queue." });
+2 -1
View File
@@ -24,7 +24,8 @@ export async function analyticsRoutes(app: FastifyInstance) {
rateLimit: {
max: ANALYTICS_RATE_LIMIT_MAX,
timeWindow: "1 minute",
keyGenerator: (req) => `analytics:${req.ip}:${(req as unknown as { userId?: string }).userId ?? "anon"}`,
keyGenerator: (req) =>
`analytics:${req.ip}:${(req as unknown as { userId?: string }).userId ?? "anon"}`,
},
},
},
+5 -1
View File
@@ -103,7 +103,11 @@ export async function cookingSessionRoutes(app: FastifyInstance) {
const params = z.object({ id: z.uuid() }).parse(req.params);
const body = z.object({ reason: z.string().max(200).optional() }).parse(req.body ?? {});
const session = await getOwnedSession(app, params.id, req.userId);
if (session.status === "completed" || session.status === "cancelled" || session.status === "undone") {
if (
session.status === "completed" ||
session.status === "cancelled" ||
session.status === "undone"
) {
throw errors.conflict("Sessionen är redan avslutad.");
}
+1 -1
View File
@@ -461,7 +461,7 @@ export async function inventoryRoutes(app: FastifyInstance) {
}
const trustState = computeTrust({
confidence: updates.confidence as number | undefined ?? item.confidence,
confidence: (updates.confidence as number | undefined) ?? item.confidence,
verifiedByUser: (updates.verifiedByUser as boolean | undefined) ?? item.verifiedByUser,
lastVerifiedAt: (updates.lastVerifiedAt as Date | null | undefined) ?? item.lastVerifiedAt,
quantity: (updates.quantity as number | undefined) ?? item.quantity,
+3 -1
View File
@@ -173,7 +173,9 @@ export async function meRoutes(app: FastifyInstance) {
const [row] = await app.db
.insert(schema.userTermsConsents)
.values({ userId: req.userId, termsVersion: TERMS_VERSION, acceptedAt: now })
.onConflictDoNothing({ target: [schema.userTermsConsents.userId, schema.userTermsConsents.termsVersion] })
.onConflictDoNothing({
target: [schema.userTermsConsents.userId, schema.userTermsConsents.termsVersion],
})
.returning();
return {
accepted: true,
+3 -14
View File
@@ -4,11 +4,7 @@ import { z } from "zod";
import { schema } from "@app/database";
import { quickStartInputSchema, onboardingInputSchema } from "@app/validation";
import { errors, parse } from "../lib/errors.js";
import {
audit,
createHouseholdWithDefaults,
getActiveHouseholdId,
} from "../lib/helpers.js";
import { audit, createHouseholdWithDefaults, getActiveHouseholdId } from "../lib/helpers.js";
import { t } from "../lib/i18n.js";
import { KNOWN_FLAGS } from "@app/feature-flags";
@@ -70,11 +66,7 @@ export async function onboardingRoutes(app: FastifyInstance) {
const input = parse(quickStartInputSchema, req.body);
// Normalize goals: multi-select UI sends `goals`; legacy clients send `primaryGoal`.
const goals = input.goals?.length
? input.goals
: input.primaryGoal
? [input.primaryGoal]
: [];
const goals = input.goals?.length ? input.goals : input.primaryGoal ? [input.primaryGoal] : [];
const primaryGoal = goals[0];
// Upsert minimal preferences
@@ -262,10 +254,7 @@ export async function onboardingRoutes(app: FastifyInstance) {
/** POST /v1/onboarding/skip skip remaining steps (GDPR-friendly, user's choice). */
app.post("/v1/onboarding/skip", auth, async (req) => {
const body = parse(
z.object({ step: z.enum(["b", "c"]).optional() }),
req.body,
);
const body = parse(z.object({ step: z.enum(["b", "c"]).optional() }), req.body);
const [user] = await app.db
.update(schema.users)
+10 -3
View File
@@ -433,9 +433,16 @@ export async function recipeRoutes(app: FastifyInstance) {
})
.returning();
const result = await completeCookingSession(app, session!, req.userId, input, req.correlationId, {
emitStartedEvent: true,
});
const result = await completeCookingSession(
app,
session!,
req.userId,
input,
req.correlationId,
{
emitStartedEvent: true,
},
);
return { sessionId: session!.id, ...result };
});
+18 -7
View File
@@ -4,11 +4,13 @@ import { schema } from "@app/database";
import { z } from "zod";
import { buildReconciliationCandidates, classifyExpiry, computeTrust } from "@app/inventory-engine";
import { errors, parse } from "../lib/errors.js";
import { getActiveDecayProfile, requireActiveHousehold, requireMembership, trackProductAnalytics } from "../lib/helpers.js";
import {
reconciliationResolveInputSchema,
reconciliationStartInputSchema,
} from "@app/validation";
getActiveDecayProfile,
requireActiveHousehold,
requireMembership,
trackProductAnalytics,
} from "../lib/helpers.js";
import { reconciliationResolveInputSchema, reconciliationStartInputSchema } from "@app/validation";
import { inventoryReconciliationCompleted, inventoryReconciliationStarted } from "@app/analytics";
/** Quick Reconciliation (Fas 2 §5.4) */
@@ -63,10 +65,15 @@ export async function reconciliationRoutes(app: FastifyInstance) {
);
if (upcomingEntries.length > 0) {
const recipeIds = [...new Set(upcomingEntries.map((m) => m.recipeId).filter(Boolean))] as string[];
const recipeIds = [
...new Set(upcomingEntries.map((m) => m.recipeId).filter(Boolean)),
] as string[];
if (recipeIds.length > 0) {
const ingredients = await app.db
.select({ recipeId: schema.recipeIngredients.recipeId, canonicalId: schema.recipeIngredients.canonicalIngredientId })
.select({
recipeId: schema.recipeIngredients.recipeId,
canonicalId: schema.recipeIngredients.canonicalIngredientId,
})
.from(schema.recipeIngredients)
.where(inArray(schema.recipeIngredients.recipeId, recipeIds));
for (const ing of ingredients) {
@@ -255,7 +262,11 @@ export async function reconciliationRoutes(app: FastifyInstance) {
req.userId,
inventoryReconciliationCompleted({
householdId,
properties: { itemId: params.itemId, action: input.action, hadAdjustment: quantityChange !== 0 },
properties: {
itemId: params.itemId,
action: input.action,
hadAdjustment: quantityChange !== 0,
},
}),
);
+55 -12
View File
@@ -5,7 +5,11 @@ import { diffScans, type ScanDiffRow, computeBalance } from "@app/inventory-engi
import { z } from "zod";
import type { Unit } from "@app/shared-types";
import { errors, parse } from "../lib/errors.js";
import { requireActiveHousehold, requireMembership, trackProductAnalytics } from "../lib/helpers.js";
import {
requireActiveHousehold,
requireMembership,
trackProductAnalytics,
} from "../lib/helpers.js";
import { scanDiffApplyInputSchema } from "@app/validation";
import { inventoryConflictCreated, inventoryConflictResolved } from "@app/analytics";
@@ -25,7 +29,11 @@ export async function scanDiffRoutes(app: FastifyInstance) {
const householdId = await requireActiveHousehold(app.db, req.userId);
await requireMembership(app.db, householdId, req.userId);
const [job] = await app.db.select().from(schema.scanJobs).where(eq(schema.scanJobs.id, params.id)).limit(1);
const [job] = await app.db
.select()
.from(schema.scanJobs)
.where(eq(schema.scanJobs.id, params.id))
.limit(1);
if (!job || job.userId !== req.userId) throw errors.notFound("Skanningen finns inte.");
if (!job.result) throw errors.badRequest("Skanningen har inget resultat än.");
@@ -57,7 +65,10 @@ export async function scanDiffRoutes(app: FastifyInstance) {
kind: row.kind,
displayName: row.displayName,
previousItemId: row.previousItemId,
proposedResolution: row.kind === "vanished" ? "depleted" : { quantity: row.newQuantity, locationId: row.newLocationId },
proposedResolution:
row.kind === "vanished"
? "depleted"
: { quantity: row.newQuantity, locationId: row.newLocationId },
}));
return {
@@ -96,14 +107,19 @@ export async function scanDiffRoutes(app: FastifyInstance) {
}
}
const [job] = await app.db.select().from(schema.scanJobs).where(eq(schema.scanJobs.id, params.id)).limit(1);
const [job] = await app.db
.select()
.from(schema.scanJobs)
.where(eq(schema.scanJobs.id, params.id))
.limit(1);
if (!job || job.userId !== req.userId) throw errors.notFound("Skanningen finns inte.");
const appliedItemIds: string[] = [];
for (const row of input.rows) {
if (row.kind === "new_item") {
const locationId = row.newLocationId ?? (await defaultLocation(app, householdId, job.scanType));
const locationId =
row.newLocationId ?? (await defaultLocation(app, householdId, job.scanType));
if (!locationId) continue;
const [inv] = await app.db
.insert(schema.inventoryItems)
@@ -219,7 +235,11 @@ export async function scanDiffRoutes(app: FastifyInstance) {
.where(eq(schema.inventoryItems.id, itemId))
.limit(1);
const txs = await app.db
.select({ type: schema.inventoryTransactions.type, quantityDelta: schema.inventoryTransactions.quantityDelta, unit: schema.inventoryTransactions.unit })
.select({
type: schema.inventoryTransactions.type,
quantityDelta: schema.inventoryTransactions.quantityDelta,
unit: schema.inventoryTransactions.unit,
})
.from(schema.inventoryTransactions)
.where(eq(schema.inventoryTransactions.inventoryItemId, itemId));
const balance = computeBalance(txs);
@@ -253,7 +273,12 @@ export async function scanDiffRoutes(app: FastifyInstance) {
const conflicts = await app.db
.select()
.from(schema.inventoryConflicts)
.where(and(eq(schema.inventoryConflicts.householdId, householdId), eq(schema.inventoryConflicts.status, "open")))
.where(
and(
eq(schema.inventoryConflicts.householdId, householdId),
eq(schema.inventoryConflicts.status, "open"),
),
)
.orderBy(schema.inventoryConflicts.createdAt);
return { conflicts };
});
@@ -278,7 +303,8 @@ export async function scanDiffRoutes(app: FastifyInstance) {
.from(schema.inventoryConflicts)
.where(eq(schema.inventoryConflicts.id, params.id))
.limit(1);
if (!conflict || conflict.householdId !== householdId) throw errors.notFound("Konflikten finns inte.");
if (!conflict || conflict.householdId !== householdId)
throw errors.notFound("Konflikten finns inte.");
if (conflict.status !== "open") throw errors.badRequest("Konflikten är redan löst.");
const resolved = resolveConflictPayload(conflict, body);
@@ -349,7 +375,11 @@ function extractObservations(result: Record<string, unknown> | unknown[]): Array
useByDate?: string | null;
observationConfidence: number;
}> {
const raw = Array.isArray(result) ? result : Array.isArray(result.items) ? (result.items as unknown[]) : [];
const raw = Array.isArray(result)
? result
: Array.isArray(result.items)
? (result.items as unknown[])
: [];
return raw
.map((r) => {
const item = r as Record<string, unknown>;
@@ -382,18 +412,31 @@ function scanSource(scanType: string) {
async function defaultLocation(app: FastifyInstance, householdId: string, scanType: string) {
const wanted =
scanType === "freezer" ? "freezer" : scanType === "pantry" || scanType === "ingredients" ? "pantry" : "fridge";
scanType === "freezer"
? "freezer"
: scanType === "pantry" || scanType === "ingredients"
? "pantry"
: "fridge";
const [loc] = await app.db
.select({ id: schema.storageLocations.id })
.from(schema.storageLocations)
.where(and(eq(schema.storageLocations.householdId, householdId), eq(schema.storageLocations.type, wanted)))
.where(
and(
eq(schema.storageLocations.householdId, householdId),
eq(schema.storageLocations.type, wanted),
),
)
.limit(1);
return loc?.id ?? null;
}
function resolveConflictPayload(
conflict: typeof schema.inventoryConflicts.$inferSelect,
body: { resolution: "source_a" | "source_b" | "manual"; manualQuantity?: number; manualLocationId?: string },
body: {
resolution: "source_a" | "source_b" | "manual";
manualQuantity?: number;
manualLocationId?: string;
},
): { quantity?: number; locationId?: string; status?: string } {
const a = (conflict.payloadA ?? {}) as Record<string, unknown>;
const b = (conflict.payloadB ?? {}) as Record<string, unknown>;