Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions caloreazi/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Changelog

## 1.24.0

- צילום, ארוחה ידנית ומועדפים זמינים מיד; שאר האפשרויות תחת פעולות נוספות.
- תוקנו בדיקת חיבורים שבוטלו, אימות מזהי ניתוח והפרדת תור Offline לפי משתמש.
- צילומים מנותחים נשמרים כטיוטות עד אישור או מחיקה, עם טיפול בהמתנה ובכשלי שמירה.
- תוקנו הוספת שתייה למשתמש חדש, בקשות מקבילות, שינוי תאריך ארוחה וסכומי תזונה בשינוי כמות.
- שמירת כל רכיבי הארוחה במסד, כתיבת יומן ממוקדת ושיפורי נגישות והסברי איכות מידע.
- הורחבו בדיקות האינטגרציה ובדיקות התנהגות הסנכרון.

## 1.23.3

- כפתור הוספת הארוחה בסרגל התחתון סוגר באופן גורף כל חלון או תהליך פתוח ומחזיר למסך המקורות הראשי.
Expand Down
2 changes: 1 addition & 1 deletion caloreazi/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ RUN chmod +x /etc/services.d/app/run /etc/services.d/nginx/run /etc/services.d/p
&& rm -f /etc/nginx/http.d/default.conf

LABEL \
io.hass.version="1.23.3" \
io.hass.version="1.24.0" \
io.hass.type="app" \
io.hass.arch="amd64"

Expand Down
2 changes: 1 addition & 1 deletion caloreazi/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# CALOREAZI 1.23.3
# CALOREAZI 1.24.0

Private Home Assistant nutrition coach with persistent onboarding, daily tracking, global Admin-managed AI, and AI meal-photo analysis.

Expand Down
2 changes: 1 addition & 1 deletion caloreazi/app/api/activity/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export async function POST(request: Request) {
if (mutationKey && (initial.systemSettings?.mutationReceipts || []).includes(receipt)) return Response.json(userView(initial, session.userId, session.role === "admin"));
const body = await request.json(); const type = String(body.type || "").trim(); const minutes = Math.max(0, Number(body.minutes) || 0); const steps = Math.max(0, Math.round(Number(body.steps) || 0)); const recordedAt = Number.isFinite(Date.parse(String(body.recordedAt || ""))) ? new Date(body.recordedAt).toISOString() : new Date().toISOString();
if (!type || (!minutes && !steps)) return Response.json({ error: "יש להזין סוג פעילות וזמן או צעדים" }, { status: 400 });
const state = await updateState((latest) => { const data = ensureUserData(latest, session.userId); const localDate = /^\d{4}-\d{2}-\d{2}$/.test(String(body.localDate || "")) ? String(body.localDate) : entryDateFor(data, recordedAt); data.activity.push({ id: crypto.randomUUID(), date: localDate, time: recordedAt, type: type.slice(0, 60), description: String(body.description || "").slice(0, 700), intensity: ["low","medium","high"].includes(body.intensity) ? body.intensity : "medium", minutes, steps, distanceKm: Math.max(0, Number(body.distanceKm) || 0), activeCalories: Math.max(0, Math.round(Number(body.activeCalories) || 0)) }); data.activity = data.activity.slice(-730); if (mutationKey) latest.systemSettings.mutationReceipts = [...(latest.systemSettings.mutationReceipts || []), receipt].slice(-2000); addAudit(latest, { userId: session.userId, action: "activity.created", target: type }); return latest; });
const state = await updateState((latest) => { if (mutationKey && (latest.systemSettings?.mutationReceipts || []).includes(receipt)) return latest; const data = ensureUserData(latest, session.userId); const localDate = /^\d{4}-\d{2}-\d{2}$/.test(String(body.localDate || "")) ? String(body.localDate) : entryDateFor(data, recordedAt); data.activity.push({ id: crypto.randomUUID(), date: localDate, time: recordedAt, type: type.slice(0, 60), description: String(body.description || "").slice(0, 700), intensity: ["low","medium","high"].includes(body.intensity) ? body.intensity : "medium", minutes, steps, distanceKm: Math.max(0, Number(body.distanceKm) || 0), activeCalories: Math.max(0, Math.round(Number(body.activeCalories) || 0)) }); data.activity = data.activity.slice(-730); if (mutationKey) latest.systemSettings.mutationReceipts = [...(latest.systemSettings.mutationReceipts || []), receipt].slice(-2000); addAudit(latest, { userId: session.userId, action: "activity.created", target: type }); return latest; });
return Response.json(userView(state, session.userId, session.role === "admin"));
}

Expand Down
2 changes: 1 addition & 1 deletion caloreazi/app/api/admin/health/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,6 @@ export async function GET(request: Request) {
const saveAudits = (state.auditLog || []).filter((item: any) => item.action === "meal.created").slice(-500).map((item: any) => { try { return JSON.parse(item.details || "{}"); } catch { return {}; } });
const timedSaveAudits = saveAudits.filter((item: any) => Number(item.interactionDurationMs || item.durationMs || 0) > 0);
const quality = { averageAddSeconds: timedSaveAudits.length ? Math.round(timedSaveAudits.reduce((sum: number, item: any) => sum + Number(item.interactionDurationMs || item.durationMs), 0) / timedSaveAudits.length / 100) / 10 : null, recognitionApprovalRate: recognized.length ? Math.round(recognized.filter((meal: any) => !meal.updatedAt).length / recognized.length * 100) : null, correctionRate: recognized.length ? Math.round(recognized.filter((meal: any) => meal.updatedAt).length / recognized.length * 100) : null, highReliabilityRate: allMeals.length ? Math.round(allMeals.filter((meal: any) => Number(meal.nutritionReliability?.score || 0) >= 88).length / allMeals.length * 100) : null, duplicateBlocks: (state.auditLog || []).filter((item: any) => item.action === "meal.duplicate_blocked").length, failedAnalyses: (state.analysisJobs || []).filter((item: any) => item.status === "failed").length, aiCostPerMeal: monthlyMeals.length ? monthlyAiCost / monthlyMeals.length : 0, aiCostPerActiveUser: activeUserIds.size ? monthlyAiCost / activeUserIds.size : 0 };
const releaseVersion = "1.23.3";
const releaseVersion = "1.24.0";
return Response.json({ version: process.env.CALOREAZI_VERSION || releaseVersion, build: process.env.CALOREAZI_BUILD_COMMIT || "development", application: "ok", database, storage: "ok", ai: state.ai.encryptedKey ? "configured" : "attention", users: state.users.length, activeUsers: state.users.filter((user) => !user.disabled).length, activeSessions: (state.sessions || []).filter((item) => !item.revokedAt && new Date(item.expiresAt).getTime() > Date.now()).length, analysisJobs: { pending: (state.analysisJobs || []).filter((item) => ["pending", "processing"].includes(item.status)).length, failed: quality.failedAnalyses }, meals, quality, trashItems: state.trash.length, auditEvents: state.auditLog.length, aiRequests: usage.length, estimatedAiCost: usage.reduce((sum, item) => sum + Number(item.cost || 0), 0), byFeature });
}
6 changes: 3 additions & 3 deletions caloreazi/app/api/auth/session/route.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { clearSessionCookie, createSessionCookie, currentSession, verifyPassword } from "@/server/auth.js";
import { clearSessionCookie, createSessionCookie, currentSession, requireUser, verifyPassword } from "@/server/auth.js";
import { addAudit, readState, updateState } from "@/server/store.js";
import { checkRateLimit, clientAddress, requireSameOrigin } from "@/server/security.js";
export const runtime = "nodejs";
Expand All @@ -21,6 +21,6 @@ export async function POST(request: Request) {
return Response.json({ ok: true, rememberedDays: 30 }, { headers: { "Set-Cookie": createSessionCookie(request, user, sessionId), "Cache-Control": "no-store" } });
}

export async function GET(request: Request) { const state = await readState(); const session = currentSession(request); const user = state.users.find((item) => item.id === session?.userId); if (!user) return Response.json({ error: "יש להתחבר" }, { status: 401 }); return Response.json({ sessions: (state.sessions || []).filter((item) => item.userId === user.id && !item.revokedAt && new Date(item.expiresAt).getTime() > Date.now()).map((item) => ({ ...item, current: item.id === session.sid })) }); }
export async function PATCH(request: Request) { const denied = requireSameOrigin(request); if (denied) return denied; const state = await readState(); const session = currentSession(request); const user = state.users.find((item) => item.id === session?.userId); if (!user) return Response.json({ error: "יש להתחבר" }, { status: 401 }); const body = await request.json(); await updateState((latest) => { latest.sessions = (latest.sessions || []).map((item) => item.userId === user.id && (body.all === true || item.id === body.id) ? { ...item, revokedAt: new Date().toISOString() } : item); addAudit(latest, { userId: user.id, action: body.all === true ? "auth.sessions_revoked_all" : "auth.session_revoked", target: body.id || "all" }); return latest; }); const currentRevoked = body.all === true || body.id === session.sid; return Response.json({ ok: true }, { headers: currentRevoked ? { "Set-Cookie": clearSessionCookie(request) } : undefined }); }
export async function GET(request: Request) { const state = await readState(); const session = requireUser(state, request); const user = state.users.find((item) => item.id === session?.userId); if (!user) return Response.json({ error: "יש להתחבר" }, { status: 401 }); return Response.json({ sessions: (state.sessions || []).filter((item) => item.userId === user.id && !item.revokedAt && new Date(item.expiresAt).getTime() > Date.now()).map((item) => ({ ...item, current: item.id === session.sid })) }); }
export async function PATCH(request: Request) { const denied = requireSameOrigin(request); if (denied) return denied; const state = await readState(); const session = requireUser(state, request); const user = state.users.find((item) => item.id === session?.userId); if (!user) return Response.json({ error: "יש להתחבר" }, { status: 401 }); const body = await request.json(); await updateState((latest) => { latest.sessions = (latest.sessions || []).map((item) => item.userId === user.id && (body.all === true || item.id === body.id) ? { ...item, revokedAt: new Date().toISOString() } : item); addAudit(latest, { userId: user.id, action: body.all === true ? "auth.sessions_revoked_all" : "auth.session_revoked", target: body.id || "all" }); return latest; }); const currentRevoked = body.all === true || body.id === session.sid; return Response.json({ ok: true }, { headers: currentRevoked ? { "Set-Cookie": clearSessionCookie(request) } : undefined }); }
export async function DELETE(request: Request) { const session = currentSession(request); if (session?.sid) await updateState((latest) => { const item = latest.sessions?.find((entry) => entry.id === session.sid && entry.userId === session.userId); if (item) item.revokedAt = new Date().toISOString(); return latest; }); return Response.json({ ok: true }, { headers: { "Set-Cookie": clearSessionCookie(request) } }); }
10 changes: 6 additions & 4 deletions caloreazi/app/api/meals/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ export async function POST(request: Request) {
const startedAt = Date.now();
const initial = await readState(); const session = requireUser(initial, request);
if (!session) return Response.json({ error: "יש להתחבר" }, { status: 401 });
const body = await request.json(); const name = String(body.name || "").trim();
const body = await request.json();
if (body.analysisJobId && !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(String(body.analysisJobId))) return Response.json({ error: "מזהה ניתוח אינו תקין" }, { status: 400 });
const name = String(body.name || "").trim();
const clientRequestId = String(body.clientRequestId || request.headers.get("idempotency-key") || "").trim().slice(0, 120);
const items = Array.isArray(body.items) ? body.items.slice(0, 30) : [];
const calculated = items.length ? calculateMealFromItems(items) : body;
Expand Down Expand Up @@ -105,7 +107,7 @@ export async function PATCH(request: Request) {
if (body.baseUpdatedAt !== undefined && String(meal.updatedAt || "") !== String(body.baseUpdatedAt || "")) { editConflict = true; return latest; }
if (body.scale !== undefined) {
const scale = Math.max(.25, Math.min(4, Number(body.scale) || 1));
["protein", "carbs", "fat"].forEach((field) => { meal[field] = Math.round(Number(meal[field] || 0) * scale * 10) / 10; });
["protein", "carbs", "fat", "sugar", "fiber", "sodiumMg", "saturatedFat", "addedSugar"].forEach((field) => { meal[field] = Math.round(Number(meal[field] || 0) * scale * 10) / 10; });
meal.kcal = roundCalories(Number(meal.kcal || 0) * scale);
meal.items = (meal.items || []).map((item) => ({ ...item, quantity: Math.round(Number(item.quantity || 1) * scale * 100) / 100 }));
} else {
Expand All @@ -115,12 +117,12 @@ export async function PATCH(request: Request) {
meal.period = ["breakfast", "lunch", "dinner", "snack"].includes(body.period) ? body.period : meal.period;
meal.kcal = roundCalories(calculated.kcal); meal.protein = Math.max(0, Number(calculated.protein) || 0); meal.carbs = Math.max(0, Number(calculated.carbs) || 0); meal.fat = Math.max(0, Number(calculated.fat) || 0); meal.sugar = Math.max(0, Number(calculated.sugar) || 0); meal.sugarTrackedItems = Math.max(0, Number(calculated.sugarTrackedItems) || 0); meal.items = items;
meal.nutritionReliability = assessMealReliability({ ...body, ...calculated, items });
const requested = new Date(body.occurredAt || meal.time); if (Number.isFinite(requested.getTime()) && requested.getTime() <= Date.now()) meal.time = requested.toISOString();
const requested = new Date(body.occurredAt || meal.time); if (Number.isFinite(requested.getTime()) && requested.getTime() <= Date.now() && requested.toISOString() !== meal.time) { meal.time = requested.toISOString(); meal.logicalDate = localDateAt(meal.time, userTimeZone(ensureUserData(latest, session.userId))); }
}
if (body.scale === undefined && editedItems.length) { const data = ensureUserData(latest, session.userId); data.foodCalibration.push(...buildFoodCorrections(previousItems, editedItems, "meal_edit")); data.foodCalibration = data.foodCalibration.slice(-100); }
meal.score = calculateMealScore(meal); meal.updatedAt = new Date().toISOString();
const previousDay = found.day; previousDay.meals = previousDay.meals.filter((item) => item.id !== id); restoreOwnedMeal(latest, session.userId, meal);
savedLocalDate = localDateAt(meal.time, userTimeZone(ensureUserData(latest, session.userId)));
savedLocalDate = meal.logicalDate || localDateAt(meal.time, userTimeZone(ensureUserData(latest, session.userId)));
addAudit(latest, { userId: session.userId, action: "meal.updated", target: id }); return latest;
});
if (!foundMeal) return Response.json({ error: "הארוחה לא נמצאה" }, { status: 404 });
Expand Down
1 change: 1 addition & 0 deletions caloreazi/app/api/measurements/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export const runtime = "nodejs";
export async function POST(request: Request) {
const initial = await readState(); const session = requireUser(initial, request);
if (!session) return Response.json({ error: "יש להתחבר" }, { status: 401 });
if (!ensureUserData(initial, session.userId).profile) return Response.json({ error: "יש להשלים פרופיל לפני הוספת מדידה" }, { status: 409 });
const body = await request.json(); const weight = Number(body.weight);
if (!(weight >= 25 && weight <= 350)) return Response.json({ error: "יש להזין משקל תקין" }, { status: 400 });
const date = String(body.date || new Date().toISOString().slice(0, 10));
Expand Down
Loading
Loading