Skip to content

Commit 06aa089

Browse files
committed
release: v1.0.0-rc.54 — Health Connect sync fix, oai-compat LLM proxy, backup fidelity
1 parent 94b0fc2 commit 06aa089

18 files changed

Lines changed: 346 additions & 76 deletions

.env.example

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -95,14 +95,16 @@ JWT_SECRET=change-me-to-a-long-random-secret
9595
# Optional — AI Assistant (shared key for all users)
9696
# If set, AI calls are proxied through the server (key never reaches the browser)
9797
# and the provider/model/key fields are locked in Settings for all users.
98-
# Only the cloud providers below are server-proxied. The "OpenAI Compatible"
99-
# provider (Ollama, LM Studio, DeepSeek, Groq, etc.) runs entirely client-side
100-
# — the browser talks to your endpoint directly — and is configured per-user in
101-
# Settings, not via env vars.
102-
# AI_PROVIDER=claude # claude | openai | gemini
103-
# AI_API_KEY=sk-ant-...
98+
# Supports every provider the app supports — cloud (claude / openai / gemini)
99+
# and OpenAI-compatible local endpoints (Ollama, LM Studio, LocalAI, vLLM,
100+
# DeepSeek, Groq, etc.). Set AI_PROVIDER=oai-compat + AI_BASE_URL + AI_MODEL
101+
# to point at a private-network LLM the browser can't reach directly (e.g.
102+
# a Docker Compose sidecar on an internal network).
103+
# AI_PROVIDER=claude # claude | openai | gemini | oai-compat
104+
# AI_API_KEY=sk-ant-... # required for cloud providers; optional for oai-compat
104105
# AI_API_KEY_FILE=/run/secrets/nutritrace_ai_api_key
105-
# AI_MODEL=claude-haiku-4-5-20251001 # optional, defaults to provider's fast model
106+
# AI_MODEL=claude-haiku-4-5-20251001 # optional for cloud (defaults to provider's fast model); REQUIRED for oai-compat
107+
# AI_BASE_URL=http://ollama:11434 # REQUIRED when AI_PROVIDER=oai-compat; must be reachable from the server container
106108
# AI_ENABLED=true # optional, auto-enables the AI Assistant for all users
107109

108110
# Optional — OIDC Single Sign-On (declared in env instead of the Settings UI)

CHANGELOG.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,54 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
99

1010
---
1111

12+
## [1.0.0-rc.54] - 2026-07-10
13+
14+
### Fixed
15+
16+
- **Health Connect Users' Android Sync Unstuck.**
17+
Pending wellness rows on the phone were tripping a SQL bug on
18+
push, which stopped every subsequent pull. Symptom: items added
19+
on the browser weren't reaching the phone. Every user with
20+
Health Connect enabled on Android was silently affected. Fixed.
21+
(#89, reported by duplaja)
22+
23+
- **More Accurate Time to Sound Sleep.**
24+
Server-side ttss derivation now credits the initial 5-10 min
25+
settling-in awake segment, matching the Fitbit app. Closes the
26+
consistent -5 to -7 min under-count on nights with a short
27+
initial wake segment. Applies to Health Connect and Google
28+
Health syncs.
29+
30+
### Added
31+
32+
- **Server-Side Proxy for Local LLM Endpoints.**
33+
Env-locked AI now supports `AI_PROVIDER=oai-compat` alongside
34+
claude / openai / gemini. Point `AI_BASE_URL` at a private
35+
network Ollama, LM Studio, LocalAI, vLLM, or similar and the
36+
server calls it directly, so the browser never has to see the
37+
endpoint. Solves the mixed-content + Docker-internal-DNS gap
38+
for self-hosters running a local LLM sidecar. Same capability
39+
added to LiftTrace and CookTrace. (#90, reported by
40+
gentlecolts)
41+
42+
### Improved
43+
44+
- **Backup + Restore Fidelity Pass.**
45+
Eight silent data-loss scenarios closed across the full backup,
46+
JSON export/import, and Android local backup flows. Highlights:
47+
OFF unit metadata (nutrition_basis, alt_units, density_g_ml)
48+
from rc.50 now survives a restore, federation API tokens
49+
survive backups, JSON settings import correctly pushes to the
50+
server on PWA, and native restore no longer double-inserts on
51+
a second pass.
52+
53+
- **"Copy to Another Date" Defaults to Today.**
54+
Copying a meal or item from a past date now pre-fills today in
55+
the picker instead of the source date. One less tap for the
56+
common "same lunch again today" case.
57+
58+
---
59+
1260
## [1.0.0-rc.53] - 2026-07-05
1361

1462
### Added

DEPLOY.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,10 @@ services:
8989
| `SMTP_USER` | No | — | SMTP username |
9090
| `SMTP_PASS` | No | — | SMTP password |
9191
| `SMTP_FROM` | No | — | From address, e.g. `"NutriTrace" <noreply@example.com>` |
92-
| `AI_PROVIDER` | No | — | `claude` \| `openai` \| `gemini`. If set, AI calls are proxied server-side and the provider/model/key fields are locked in Settings for all users. |
93-
| `AI_API_KEY` | No | — | API key for the chosen provider. Server-only, never reaches the browser. |
94-
| `AI_MODEL` | No | provider default | Optional model override (e.g. `claude-haiku-4-5-20251001`). |
92+
| `AI_PROVIDER` | No | — | `claude` \| `openai` \| `gemini` \| `oai-compat`. If set, AI calls are proxied server-side and the provider/model/key fields are locked in Settings for all users. |
93+
| `AI_API_KEY` | No | — | API key for the chosen provider. Server-only, never reaches the browser. Optional when `AI_PROVIDER=oai-compat`. |
94+
| `AI_MODEL` | No | provider default | Optional model override (e.g. `claude-haiku-4-5-20251001`, `llama3.1:8b`). Required when `AI_PROVIDER=oai-compat`. |
95+
| `AI_BASE_URL` | No | — | Required when `AI_PROVIDER=oai-compat`. Base URL of your OpenAI-compatible endpoint, e.g. `http://ollama:11434`. Reached from the server container, not the browser — Docker Compose sidecars on internal networks work. |
9596
| `AI_ENABLED` | No | — | If `true`, auto-enables the AI Assistant for all users. |
9697

9798
> **Note:** SMTP and AI settings can also be configured in **Settings → Email** / **Settings → AI Assistant** (admin only). Environment variables take priority over the UI and lock the corresponding fields when set.

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,8 @@ On first launch, a setup wizard walks you through enabling user management and c
289289
| `SMTP_FROM` | No | — | From address, e.g. `NutriTrace <noreply@example.com>` |
290290
| `AI_PROVIDER` | No | — | Lock Trace to a specific provider for all users: `claude` \| `openai` \| `gemini` \| `oai-compat` |
291291
| `AI_API_KEY` | No | — | Shared AI API key. Key is server-side only — never sent to the browser. Optional when `AI_PROVIDER=oai-compat` and pointing at a local endpoint that doesn't require auth. |
292-
| `AI_MODEL` | No | provider default | Override the AI model (e.g. `claude-haiku-4-5-20251001`, `llama3.1:8b`) |
292+
| `AI_MODEL` | No | provider default | Override the AI model (e.g. `claude-haiku-4-5-20251001`, `llama3.1:8b`). Required when `AI_PROVIDER=oai-compat` |
293+
| `AI_BASE_URL` | No | — | Required when `AI_PROVIDER=oai-compat`. Base URL of your OpenAI-compatible endpoint, e.g. `http://ollama:11434`. Must be reachable from the server container (a Docker Compose sidecar on an internal network is the intended use — the browser never touches it, so mixed-content and internal DNS aren't blockers). |
293294
| `AI_ENABLED` | No | — | Set to `true` to auto-enable Trace for all users |
294295

295296
SMTP and AI settings can also be configured in the Settings UI. Environment variables take priority over UI values and lock those fields for all users.

android/app/build.gradle

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ android {
1919
applicationId "com.nutritrace.app"
2020
minSdkVersion rootProject.ext.minSdkVersion
2121
targetSdkVersion rootProject.ext.targetSdkVersion
22-
versionCode 122
23-
versionName "1.0.0-rc.53"
22+
versionCode 123
23+
versionName "1.0.0-rc.54"
2424
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
2525
aaptOptions {
2626
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.

docker-compose.yml

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -67,12 +67,15 @@ services:
6767
# Optional — AI Assistant (shared key for all users). When set, AI calls
6868
# are proxied through the server (key never reaches the browser) and the
6969
# provider/model/key fields are locked in Settings for all users.
70-
# NOTE: only the cloud providers below are server-proxied. The "OpenAI
71-
# Compatible" provider (Ollama, LM Studio, DeepSeek, Groq, etc.) runs
72-
# entirely client-side and is configured per-user in Settings, not here.
73-
# - AI_PROVIDER=${AI_PROVIDER:-} # claude | openai | gemini
74-
# - AI_API_KEY=${AI_API_KEY:-}
75-
# - AI_MODEL=${AI_MODEL:-}
70+
# Supports every provider the app supports — cloud (claude / openai /
71+
# gemini) and OpenAI-compatible local endpoints (Ollama, LM Studio,
72+
# LocalAI, vLLM, DeepSeek, Groq, etc.). Set AI_PROVIDER=oai-compat +
73+
# AI_BASE_URL + AI_MODEL to point at a private-network LLM the browser
74+
# can't reach directly (e.g. an Ollama sidecar on this compose network).
75+
# - AI_PROVIDER=${AI_PROVIDER:-} # claude | openai | gemini | oai-compat
76+
# - AI_API_KEY=${AI_API_KEY:-} # required for cloud; optional for oai-compat
77+
# - AI_MODEL=${AI_MODEL:-} # optional for cloud; REQUIRED for oai-compat
78+
# - AI_BASE_URL=${AI_BASE_URL:-} # REQUIRED for oai-compat, e.g. http://ollama:11434
7679
# - AI_ENABLED=${AI_ENABLED:-}
7780
# Optional — OIDC Single Sign-On declared in env instead of the Settings UI.
7881
# Single-provider shorthand (`OIDC_*` is an alias for `OIDC_PROVIDER_1_*`):

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "nutritrace",
3-
"version": "1.0.0-rc.53",
3+
"version": "1.0.0-rc.54",
44
"private": true,
55
"type": "module",
66
"scripts": {

server/ai.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@ export function seedAiFromEnv() {
77
AI_PROVIDER: 'ai_provider',
88
AI_API_KEY: 'ai_api_key',
99
AI_MODEL: 'ai_model',
10+
// AI_BASE_URL enables server-side proxying for the `oai-compat`
11+
// provider (Ollama, LM Studio, LocalAI, vLLM, etc.). Required when
12+
// AI_PROVIDER=oai-compat; ignored otherwise. Lets self-hosters put
13+
// their LLM on a private Docker network — the browser never talks
14+
// to it directly, so mixed-content and internal-DNS resolution
15+
// stop being blockers (#90 gentlecolts).
16+
AI_BASE_URL: 'ai_base_url',
1017
};
1118
const upsert = db.prepare(
1219
'INSERT INTO app_config (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value'

server/lib/google-health.js

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -643,6 +643,7 @@ export async function fetchSleepForDate(accessToken, dateStr) {
643643
}
644644

645645
const BRIEF_LIGHT_MAX = 5; // LIGHT segments < 5 min count toward Sound Sleep
646+
const TTSS_SETTLING_CAP = 10; // initial AWAKE up to N min counts toward TTSS
646647
let interruptionsMin = 0;
647648
let fullAwakenings = 0;
648649
let restlessnessMin = 0;
@@ -658,10 +659,23 @@ export async function fetchSleepForDate(accessToken, dateStr) {
658659
const durMin = (new Date(e) - new Date(s)) / 60000;
659660
if (!Number.isFinite(durMin)) continue;
660661
if (seg.type === 'AWAKE') {
661-
// First AWAKE >= 5 min: settling-in period, credit to Time to Sound Sleep.
662+
// First AWAKE >= 5 min: settling-in period. Fitbit's app measures TTSS
663+
// from when the wearable detected the start of the sleep session
664+
// (including a short initial settling-in AWAKE) to the first DEEP
665+
// block — BUT only for reasonably short initial AWAKE. For long initial
666+
// AWAKE (Jun 30 2026: 15m), Fitbit apparently assumes the user was
667+
// awake before going to bed and starts TTSS from the first sleep stage
668+
// instead. Calibration data: 3.5m/5m/5.5m/7m AWAKE all include-in-ttss,
669+
// 15m does not — threshold sits somewhere between. TTSS_SETTLING_CAP
670+
// set at 10 min conservatively; revisit if a 10-15m point lands.
662671
if (!firstAwakeSeen) {
663672
firstAwakeSeen = true;
664-
if (durMin >= FULL_AWAKE_MIN) continue;
673+
if (durMin >= FULL_AWAKE_MIN) {
674+
if (durMin <= TTSS_SETTLING_CAP && firstAsleepStart == null) {
675+
firstAsleepStart = new Date(s).getTime();
676+
}
677+
continue;
678+
}
665679
}
666680
// Last AWAKE >= 5 min: morning wake-up event, exclude.
667681
if (i === lastAwakeBigIdx) continue;

server/routes/ai.js

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -134,18 +134,29 @@ router.post('/chat', requireAuth, aiChatLimit, wrap(async (req, res) => {
134134
}
135135

136136
const cfg = getAiConfig();
137-
if (!cfg.ai_api_key) return res.status(503).json({ error: 'AI not configured on server. Set AI_API_KEY in environment.' });
138-
139137
const provider = cfg.ai_provider || 'claude';
140138
const model = cfg.ai_model || AI_DEFAULT_MODELS[provider] || '';
141139
const apiKey = cfg.ai_api_key;
140+
const baseUrl = cfg.ai_base_url;
142141
const toolsArr = Array.isArray(tools) ? tools : [];
143142

143+
// API key is required for cloud providers. `oai-compat` local endpoints
144+
// (Ollama, LM Studio, etc.) often don't need one — mirror the client-side
145+
// callAI() behaviour at aiChat.js:247-249.
146+
if (!apiKey && provider !== 'oai-compat') {
147+
return res.status(503).json({ error: 'AI not configured on server. Set AI_API_KEY in environment.' });
148+
}
149+
if (provider === 'oai-compat') {
150+
if (!baseUrl) return res.status(503).json({ error: 'AI_PROVIDER=oai-compat requires AI_BASE_URL in environment.' });
151+
if (!model) return res.status(503).json({ error: 'AI_PROVIDER=oai-compat requires AI_MODEL in environment.' });
152+
}
153+
144154
let result;
145155
switch (provider) {
146-
case 'claude': result = await _callClaude(apiKey, model, messages, systemPrompt, toolsArr); break;
147-
case 'openai': result = await _callOpenAI(apiKey, model, messages, systemPrompt, toolsArr); break;
148-
case 'gemini': result = await _callGemini(apiKey, model, messages, systemPrompt, toolsArr); break;
156+
case 'claude': result = await _callClaude(apiKey, model, messages, systemPrompt, toolsArr); break;
157+
case 'openai': result = await _callOpenAI(apiKey, model, messages, systemPrompt, toolsArr, 'https://api.openai.com'); break;
158+
case 'gemini': result = await _callGemini(apiKey, model, messages, systemPrompt, toolsArr); break;
159+
case 'oai-compat': result = await _callOpenAI(apiKey || 'no-key', model, messages, systemPrompt, toolsArr, baseUrl.replace(/\/+$/, '')); break;
149160
default: return res.status(400).json({ error: `Unknown provider: ${provider}` });
150161
}
151162
res.json(result);
@@ -214,7 +225,7 @@ async function _callClaude(apiKey, model, messages, systemPrompt, tools) {
214225
return { assistantMessage, toolCalls };
215226
}
216227

217-
async function _callOpenAI(apiKey, model, messages, systemPrompt, tools) {
228+
async function _callOpenAI(apiKey, model, messages, systemPrompt, tools, baseUrl = 'https://api.openai.com') {
218229
const openaiTools = (tools || []).map(t => ({
219230
type: 'function',
220231
function: { name: t.name, description: t.description, parameters: t.parameters },
@@ -227,7 +238,7 @@ async function _callOpenAI(apiKey, model, messages, systemPrompt, tools) {
227238
};
228239
if (openaiTools.length) body.tools = openaiTools;
229240

230-
const res = await fetch('https://api.openai.com/v1/chat/completions', {
241+
const res = await fetch(`${baseUrl}/v1/chat/completions`, {
231242
method: 'POST',
232243
headers: {
233244
'Content-Type': 'application/json',

0 commit comments

Comments
 (0)