feat: kitchen screen, scan dedup, gemini-3.5-flash-lite, seed grapes/beetroot/pea shoots

This commit is contained in:
Sven (AAMOS AI)
2026-08-16 20:13:49 +07:00
parent d098bf705e
commit c0a15bb1b5
20 changed files with 295 additions and 48 deletions
+8 -3
View File
@@ -48,7 +48,7 @@ const configSchema = z.object({
AAMOS_TIMEOUT_MS: z.coerce.number().int().default(60_000),
GEMINI_API_KEY: z.string().optional(),
GEMINI_MODEL: z.string().default("gemini-2.5-flash"),
GEMINI_MODEL: z.string().default("gemini-3.5-flash-lite"),
GEMINI_TIMEOUT_MS: z.coerce.number().int().default(60_000),
GEMINI_DAILY_BUDGET_USD: z.coerce.number().default(0),
@@ -104,8 +104,13 @@ export function loadConfig(env: NodeJS.ProcessEnv = process.env): AppConfig {
console.error("SÄKERHETSSTOPP: AAMOS_MODE=mock är inte tillåtet i produktion.");
process.exit(1);
}
if (cfg.AAMOS_MODE === "gemini" && !cfg.GEMINI_API_KEY) {
console.error("SÄKERHETSSTOPP: AAMOS_MODE=gemini kräver GEMINI_API_KEY i produktion.");
if (
cfg.AAMOS_MODE === "gemini" &&
(!cfg.GEMINI_API_KEY || cfg.GEMINI_API_KEY.startsWith("ROTATE"))
) {
console.error(
"SÄKERHETSSTOPP: AAMOS_MODE=gemini kräver en riktig GEMINI_API_KEY (hämtas från SSM /cibello/prod/gemini-api-key), inte tom eller placeholder.",
);
process.exit(1);
}
if (cfg.AAMOS_MODE === "http" && (!cfg.AAMOS_API_URL || !cfg.AAMOS_API_KEY)) {
+37
View File
@@ -150,6 +150,43 @@ export async function scanRoutes(app: FastifyInstance) {
if (!locationId)
throw errors.badRequest("storageLocationId saknas och ingen standardplats finns.");
// Dedup (#1): finns varan redan aktiv i hushållet? Uppdatera + hoppa över,
// så överlappande foton / omfotografering inte skapar dubletter.
const dedupCond = item.canonicalIngredientId
? and(
eq(schema.inventoryItems.householdId, householdId),
eq(schema.inventoryItems.canonicalIngredientId, item.canonicalIngredientId),
gt(schema.inventoryItems.quantity, 0),
)
: and(
eq(schema.inventoryItems.householdId, householdId),
isNull(schema.inventoryItems.canonicalIngredientId),
eq(schema.inventoryItems.displayName, item.displayName),
gt(schema.inventoryItems.quantity, 0),
);
const [dupe] = await app.db
.select({ id: schema.inventoryItems.id })
.from(schema.inventoryItems)
.where(dedupCond)
.limit(1);
if (dupe) {
await app.db
.update(schema.inventoryItems)
.set({
quantity: item.quantity,
unit: item.unit,
brand: item.brand ?? null,
bestBeforeDate: item.bestBeforeDate ?? null,
useByDate: item.useByDate ?? null,
lastVerifiedAt: new Date(),
verifiedByUser: true,
updatedAt: new Date(),
})
.where(eq(schema.inventoryItems.id, dupe.id));
created.push(dupe.id);
continue;
}
const [inv] = await app.db
.insert(schema.inventoryItems)
.values({
+1 -1
View File
@@ -109,7 +109,7 @@ describe("scan confirmation → ai_corrections", () => {
},
],
},
modelVersion: "gemini-2.5-flash",
modelVersion: "gemini-3.5-flash-lite",
promptVersion: "gemini-fridge-v1",
})
.returning();