diff --git a/caloreazi/CHANGELOG.md b/caloreazi/CHANGELOG.md index 64d2b4a..0b7700a 100644 --- a/caloreazi/CHANGELOG.md +++ b/caloreazi/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 1.23.1 + +- שמונת מקורות הוספת הארוחה מוצגים באריחים גדולים וברורים בפריסה של שתי עמודות וארבע שורות. +- גובה האריחים מותאם אוטומטית למסכים נמוכים כדי לשמור את כל מסך ההוספה ללא גלילה אנכית. + ## 1.23.0 - חלון הוספת הארוחה עוצב מחדש כמסך אחד: חיפוש, שמונה מקורות הוספה בשתי שורות וקרוסלת ארוחות אחרונות או נפוצות. diff --git a/caloreazi/Dockerfile b/caloreazi/Dockerfile index b8f3a0f..36baed0 100644 --- a/caloreazi/Dockerfile +++ b/caloreazi/Dockerfile @@ -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.0" \ + io.hass.version="1.23.1" \ io.hass.type="app" \ io.hass.arch="amd64" diff --git a/caloreazi/README.md b/caloreazi/README.md index 75f95a3..0d066f0 100644 --- a/caloreazi/README.md +++ b/caloreazi/README.md @@ -1,4 +1,4 @@ -# CALOREAZI 1.23.0 +# CALOREAZI 1.23.1 Private Home Assistant nutrition coach with persistent onboarding, daily tracking, global Admin-managed AI, and AI meal-photo analysis. diff --git a/caloreazi/app/api/admin/health/route.ts b/caloreazi/app/api/admin/health/route.ts index 1828439..6a70292 100644 --- a/caloreazi/app/api/admin/health/route.ts +++ b/caloreazi/app/api/admin/health/route.ts @@ -18,5 +18,5 @@ 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 }; - return Response.json({ version: process.env.CALOREAZI_VERSION || "1.23.0", 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 }); + return Response.json({ version: process.env.CALOREAZI_VERSION || "1.23.1", 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 }); } diff --git a/caloreazi/app/globals.css b/caloreazi/app/globals.css index fb457b2..bebb471 100644 --- a/caloreazi/app/globals.css +++ b/caloreazi/app/globals.css @@ -205,6 +205,13 @@ body{touch-action:pan-y}.app-shell{width:100%;max-width:100%;overflow-x:clip} .quick-add-launcher{width:min(720px,calc(100% - 24px))!important;overflow:hidden!important}.quick-add-launcher>.quick-catalog-search{margin:8px 18px 12px}.quick-source-grid{padding:0 18px;display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px}.quick-source{min-width:0;min-height:104px;padding:10px 5px;display:flex;align-items:center;justify-content:center;flex-direction:column;gap:8px;border:1px solid color-mix(in srgb,var(--source,var(--orange)) 32%,var(--line));border-radius:18px;color:var(--ink);background:linear-gradient(150deg,color-mix(in srgb,var(--source,var(--orange)) 12%,var(--surface)),var(--surface));box-shadow:0 8px 22px color-mix(in srgb,var(--source,var(--orange)) 9%,transparent)}.quick-source>span{width:46px;height:46px;display:grid;place-items:center;border-radius:15px;color:#fff;background:linear-gradient(145deg,var(--source,var(--orange)),color-mix(in srgb,var(--source,var(--orange)) 72%,#222));box-shadow:0 7px 16px color-mix(in srgb,var(--source,var(--orange)) 28%,transparent)}.quick-source .app-icon{width:25px;height:25px}.quick-source>strong{max-width:100%;font-size:13px;line-height:1.2;text-align:center}.quick-source.source-2{--source:#ed7445}.quick-source.source-3{--source:#8b72cf}.quick-source.source-4{--source:#557ac7}.quick-source.source-5{--source:#5b9d86}.quick-source.source-6{--source:#e0aa2e}.quick-source.source-7{--source:#56a8d5}.quick-source.source-8{--source:#68a76f}.quick-add-recents{margin:13px 18px 16px;padding-top:11px;border-top:1px solid var(--line)}.quick-add-recents>header{display:flex;align-items:end;justify-content:space-between;gap:8px}.quick-add-recents>header small{color:var(--muted);font-size:11px}.quick-add-recents>div{margin-top:8px;display:flex;gap:8px;overflow-x:auto;overscroll-behavior-x:contain;scroll-snap-type:x proximity;scrollbar-width:none}.quick-add-recents>div::-webkit-scrollbar{display:none}.quick-add-recents button{min-width:174px;min-height:62px;padding:9px;display:grid;grid-template-columns:34px minmax(0,1fr);grid-template-rows:auto auto;align-items:center;gap:2px 8px;scroll-snap-align:start;border:1px solid var(--line);border-radius:14px;color:var(--ink);background:var(--surface);text-align:right}.quick-add-recents button>span{width:34px;height:34px;grid-row:1/3;display:grid;place-items:center;border-radius:11px;color:#fff;background:var(--orange);font-size:21px}.quick-add-recents button strong{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:13px}.quick-add-recents button small{color:var(--muted);font-size:10px}.capture-exit{width:auto!important;min-width:82px!important;height:42px!important;padding:0 12px!important;display:inline-flex!important;align-items:center!important;justify-content:center!important;gap:6px!important;border:1px solid currentColor!important;border-radius:12px!important;font-size:15px!important;font-weight:800!important}.capture-exit span{font-size:14px!important} @media(max-width:760px){.quick-add-launcher{position:fixed!important;inset:0!important;width:100%!important;height:calc(100dvh - 72px - env(safe-area-inset-bottom))!important;max-height:none!important;padding:0!important;border:0!important;border-radius:0!important;overflow:hidden!important}.quick-add-launcher>header{min-height:64px;padding:calc(10px + env(safe-area-inset-top)) 14px 8px!important}.quick-add-launcher>.quick-catalog-search{margin:7px 12px 10px;min-height:46px}.quick-source-grid{padding:0 12px;grid-template-columns:repeat(4,minmax(0,1fr));gap:7px}.quick-source{min-height:92px;padding:7px 3px;border-radius:15px}.quick-source>span{width:42px;height:42px;border-radius:13px}.quick-source .app-icon{width:23px;height:23px}.quick-source>strong{font-size:11px}.quick-add-recents{margin:10px 12px 8px;padding-top:9px}.quick-add-recents>header strong{font-size:13px}.quick-add-recents>header small{font-size:9px}.quick-add-recents>div{margin-top:6px}.quick-add-recents button{min-width:152px;min-height:57px}.meal-camera-modal>header,.barcode-scanner>header{position:sticky;top:0;z-index:5}.capture-exit{min-width:78px!important}} .quick-source-grid{grid-template-columns:repeat(4,minmax(0,1fr))} +.quick-source-grid{grid-template-columns:repeat(2,minmax(0,1fr));grid-template-rows:repeat(4,minmax(82px,1fr))}.quick-source{min-height:92px;display:grid;grid-template-columns:54px minmax(0,1fr);align-content:center;justify-content:stretch;padding:10px 14px;text-align:right}.quick-source>span{width:50px;height:50px}.quick-source>strong{font-size:15px;text-align:right} +@media(max-width:760px){.quick-source-grid{height:min(420px,calc(100dvh - 280px));min-height:330px;grid-template-columns:repeat(2,minmax(0,1fr))!important;grid-template-rows:repeat(4,minmax(0,1fr));gap:8px}.quick-source{min-height:0;padding:8px 10px;grid-template-columns:46px minmax(0,1fr);gap:8px;border-radius:16px}.quick-source>span{width:44px;height:44px}.quick-source>strong{font-size:14px;line-height:1.25}.quick-add-recents{flex:none}} +@media(max-width:760px) and (max-height:700px){.quick-add-launcher>header{min-height:54px!important}.quick-add-launcher>.quick-catalog-search{min-height:42px;margin-block:5px 7px}.quick-source-grid{height:310px;min-height:310px}.quick-source{grid-template-columns:40px minmax(0,1fr);padding:6px 8px}.quick-source>span{width:38px;height:38px}.quick-source .app-icon{width:21px;height:21px}.quick-source>strong{font-size:12px}.quick-add-recents{margin-top:7px}} +@media(max-width:760px){.meal-camera-modal .capture-exit,.barcode-scanner .capture-exit{position:fixed!important;top:calc(10px + env(safe-area-inset-top))!important;left:12px!important;right:auto!important;z-index:120!important;min-width:92px!important;height:46px!important;color:#fff!important;border:1px solid #ffffff80!important;background:#151817e6!important;box-shadow:0 7px 24px #0008!important;backdrop-filter:blur(12px)}.meal-camera-modal>header,.barcode-scanner>header{padding-left:116px!important}.meal-camera-layer,.barcode-scanner-layer{z-index:110!important}} +.quick-add-modal .category-back{position:sticky;top:0;z-index:6;min-height:46px;margin:8px 18px;padding:0 14px;display:inline-flex;align-items:center;gap:7px;border:1px solid color-mix(in srgb,var(--orange) 45%,var(--line));border-radius:13px;color:var(--orange);background:color-mix(in srgb,var(--surface) 94%,transparent);box-shadow:0 7px 20px #00000012;backdrop-filter:blur(12px);font-size:14px;font-weight:900} +@media(max-width:760px){.quick-add-modal .category-back{top:calc(env(safe-area-inset-top) + 4px);margin:7px 12px;min-height:48px}} +.produce-filter{margin:2px 18px 10px;padding:4px;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:4px;border:1px solid var(--line);border-radius:15px;background:var(--canvas)}.produce-filter button{min-height:46px;border:0;border-radius:11px;color:var(--muted);background:transparent;font-size:16px;font-weight:800}.produce-filter button.active{color:#fff;background:linear-gradient(135deg,#5caa72,#76bd67);box-shadow:0 6px 16px #5caa7238}.produce-filter button:focus-visible{outline:3px solid color-mix(in srgb,#5caa72 45%,transparent);outline-offset:2px}@media(max-width:760px){.produce-filter{margin-inline:12px}.produce-filter button{min-height:48px;font-size:17px}} /* Transparent nutrition audit: source, formula and confidence travel with every ingredient. */ .meal-reliability{margin:14px 0;padding:0!important;overflow:hidden;border:1px solid var(--line);border-radius:18px;background:var(--surface)}.meal-reliability.high{border-color:color-mix(in srgb,var(--green) 52%,var(--line))}.meal-reliability.medium{border-color:color-mix(in srgb,var(--carbs) 58%,var(--line))}.meal-reliability.low{border-color:color-mix(in srgb,#e35b55 65%,var(--line))}.meal-reliability>summary{min-height:66px;padding:13px 16px;display:flex;align-items:center;justify-content:space-between;cursor:pointer;list-style:none}.meal-reliability>summary::-webkit-details-marker{display:none}.meal-reliability>summary>span{display:grid;grid-template-columns:34px 1fr;column-gap:10px;align-items:center}.meal-reliability>summary svg{grid-row:1/3;width:24px;height:24px;color:var(--green)}.meal-reliability.medium>summary svg{color:var(--carbs)}.meal-reliability.low>summary svg{color:#e35b55}.meal-reliability>summary b{font-size:17px}.meal-reliability>summary small{color:var(--muted);font-size:13px}.meal-reliability>summary>i{width:10px;height:10px;border-inline-end:2px solid var(--muted);border-bottom:2px solid var(--muted);transform:rotate(45deg);transition:transform .2s}.meal-reliability[open]>summary>i{transform:rotate(225deg)}.meal-reliability>div{padding:0 14px 14px;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:9px}.meal-reliability article{padding:12px;border:1px solid var(--line);border-radius:13px;background:var(--canvas)}.meal-reliability article header{display:flex;justify-content:space-between;gap:10px}.meal-reliability article header b{color:var(--green);font-variant-numeric:tabular-nums}.meal-reliability article p{margin:5px 0;color:var(--muted);font-size:13px}.meal-reliability article small{display:block;color:var(--ink);font-size:13px;direction:rtl}.meal-reliability>ul{margin:0 16px 15px;padding-inline-start:18px;color:#e35b55}.meal-reliability>ul li{margin-top:5px} diff --git a/caloreazi/app/health/route.ts b/caloreazi/app/health/route.ts index d29dfd0..71bce6d 100644 --- a/caloreazi/app/health/route.ts +++ b/caloreazi/app/health/route.ts @@ -1,3 +1,3 @@ export function GET() { - return Response.json({ status: "ok", service: "caloreazi", version: "1.23.0", build: process.env.CALOREAZI_BUILD_COMMIT || "development" }); + return Response.json({ status: "ok", service: "caloreazi", version: "1.23.1", build: process.env.CALOREAZI_BUILD_COMMIT || "development" }); } diff --git a/caloreazi/app/page.tsx b/caloreazi/app/page.tsx index 2232153..573fdf2 100644 --- a/caloreazi/app/page.tsx +++ b/caloreazi/app/page.tsx @@ -657,6 +657,7 @@ export default function Home() { const [forgottenMeals, setForgottenMeals] = useState([]); const [forgottenStatus, setForgottenStatus] = useState(""); const [quickCategory, setQuickCategory] = useState(""); + const [produceFilter, setProduceFilter] = useState<"vegetables" | "fruits">("vegetables"); const [quickSearch, setQuickSearch] = useState(""); const [onlineFoodResults, setOnlineFoodResults] = useState([]); const [onlineFoodStatus, setOnlineFoodStatus] = useState(""); @@ -4351,7 +4352,7 @@ export default function Home() { { label: "הקלט ארוחה", icon: "mic", action: () => { setQuickAddOpen(false); setVoiceOpen(true); } }, { label: "מועדפים", icon: "star", action: () => { setQuickAddOpen(false); setFoodLibraryOpen(true); } }, { label: "משקאות", icon: "drink", action: () => setQuickCategory("drinks") }, - { label: "ירקות ופירות", icon: "produce", action: () => setQuickCategory("produce") }, + { label: "ירקות ופירות", icon: "produce", action: () => { setProduceFilter("vegetables"); setQuickCategory("produce"); } }, ].map((entry: any, index) => )} {quickRepeatMeals.length > 0 &&
ארוחות אחרונות ונפוצותלהוספה חוזרת בלחיצה
{quickRepeatMeals.slice(0, 8).map(({ meal, count }) => )}
} ) : ( @@ -4359,18 +4360,25 @@ export default function Home() { + {quickCategory === "produce" && ( +
+ + +
+ )}
- {(quickCategory === "produce" ? [...quickFoods.vegetables.map((item: any) => ({ ...item, spriteCategory: "vegetables" })), ...quickFoods.fruits.map((item: any) => ({ ...item, spriteCategory: "fruits" }))] : quickFoods[quickCategory]).filter((item) => !quickSearch.trim() || `${item.name} ${item.portion}`.toLocaleLowerCase("he").includes(quickSearch.trim().toLocaleLowerCase("he"))).map((item: any, index: number) => ( + {(quickCategory === "produce" ? quickFoods[produceFilter].map((item: any) => ({ ...item, spriteCategory: produceFilter })) : quickFoods[quickCategory]).filter((item) => !quickSearch.trim() || `${item.name} ${item.portion}`.toLocaleLowerCase("he").includes(quickSearch.trim().toLocaleLowerCase("he"))).map((item: any, index: number) => ( ))} {(state.foods || []) - .filter((food) => (quickCategory === "produce" ? ["vegetables", "fruits"].includes(food.category) : food.category === quickCategory) && (!quickSearch.trim() || `${food.name} ${food.category || ""}`.toLocaleLowerCase("he").includes(quickSearch.trim().toLocaleLowerCase("he")))) + .filter((food) => (quickCategory === "produce" ? food.category === produceFilter : food.category === quickCategory) && (!quickSearch.trim() || `${food.name} ${food.category || ""}`.toLocaleLowerCase("he").includes(quickSearch.trim().toLocaleLowerCase("he")))) .map((food) => (