Skip to content

Commit 3bcb42b

Browse files
committed
chore: sync from nutritrace-dev (v1.0.0-rc.39)
1 parent 3adbb98 commit 3bcb42b

18 files changed

Lines changed: 741 additions & 130 deletions

File tree

.env.example

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,16 +39,22 @@ JWT_SECRET=change-me-to-a-long-random-secret
3939
# INSECURE_COOKIES=1
4040

4141
# ─── Local Open Food Facts mirror (Issue #22) ──────────────────────────────
42-
# When OFF_LOCAL_DB is set, barcode and food-name lookups try a local DuckDB
42+
# When OFF_LOCAL_DB is set, barcode and food-name lookups try a local OFF
4343
# mirror before reaching the public api.openfoodfacts.org. Useful for
4444
# air-gapped self-hosters, strict-egress environments, and resilience when
4545
# the OFF API is down.
4646
#
47+
# File formats supported (auto-detected by extension):
48+
# .parquet — Hugging Face Parquet dump (default since rc.39, the
49+
# maintained option after OFF retired their native DuckDB
50+
# snapshot). DuckDB queries it via read_parquet().
51+
# .duckdb — Legacy native DuckDB file (pre-rc.39 setups still work).
52+
#
4753
# Setup:
4854
# 1. Uncomment the OFF mirror volume mount in docker-compose.yml
49-
# 2. Set OFF_LOCAL_DB below to the in-container path (default /data/off.duckdb).
50-
# NutriTrace downloads the initial snapshot (~4 GB) automatically on first
51-
# boot if the file is missing — no manual wget required.
55+
# 2. Set OFF_LOCAL_DB below to the in-container path (default /data/off.parquet).
56+
# NutriTrace downloads the initial snapshot (~7-8 GB) automatically on
57+
# first boot if the file is missing — no manual wget required.
5258
# 3. (Optional) Set OFF_LOCAL_ONLY=1 to refuse any fallback to the remote
5359
# OFF API. True air-gap mode: lookups for products not in the mirror
5460
# return "not found" instead of querying the public API.
@@ -60,10 +66,12 @@ JWT_SECRET=change-me-to-a-long-random-secret
6066
# atomic swap; no container restart needed. See DEPLOY.md → "Local Open Food
6167
# Facts mirror" for the full guide.
6268
#
63-
# OFF_LOCAL_DB=/data/off.duckdb
69+
# OFF_LOCAL_DB=/data/off.parquet
6470
# OFF_LOCAL_ONLY=1
65-
# Optional — pin the download source. Defaults to the official OFF nightly build.
66-
# OFF_LOCAL_URL=https://challenges.openfoodfacts.org/products.duckdb
71+
# Optional — pin the download source. Defaults to the maintained Hugging Face
72+
# Parquet dump. To use a legacy / self-hosted DuckDB file instead, set this to
73+
# a URL ending in .duckdb and use a .duckdb extension on OFF_LOCAL_DB.
74+
# OFF_LOCAL_URL=https://huggingface.co/datasets/openfoodfacts/product-database/resolve/main/food.parquet?download=true
6775

6876
# Optional — Log level (error | warn | info | debug)
6977
# Default: info — logs requests, warnings, and errors

.gitignore

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,3 @@ data/
3434
*.swp
3535
*.swo
3636

37-
# Android — build artifacts and generated files (keep source, ignore generated)
38-
android/.gradle/
39-
android/app/build/
40-
android/build/
41-
android/local.properties
42-
android/capacitor-cordova-android-plugins/
43-
android/.idea/
44-
*.apk
45-
*.aab
46-
*.keystore
47-
android/keystore.properties

CHANGELOG.md

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

1010
---
1111

12+
## [1.0.0-rc.39] — 2026-05-27
13+
14+
### Added
15+
16+
- **OIDC RP-initiated logout (PWA + Android).** Signing out now also ends the
17+
session at your identity provider, so the next sign-in isn't silently
18+
completed by a still-alive IdP session. Requires registering two Post Logout
19+
Redirect URIs at your IdP (HTTPS root for PWA, `nutritrace://oidc-callback`
20+
for Android). Thanks to @jimmielightner. (Issue #48, PR #49)
21+
- **Edit any Quick Calories entry.** Tap a Quick Calories row in the diary to
22+
change the kcal, name, or any of the three optional macros.
23+
24+
### Changed
25+
26+
- **OFF local mirror now uses the Hugging Face Parquet snapshot** since OFF
27+
retired the previous DuckDB source. Existing setups keep working without
28+
renaming; format is auto-detected. (Issue #22 followup)
29+
- **Quick Calories diary row uses your custom name** when a meal has just one
30+
Quick Calories entry. Multi-entry meals still collapse to "Quick Calories × N".
31+
- **Diary edit sheet macro pills get the four-color treatment** (yellow / purple
32+
/ green / orange) to match the Quick Calories card and Foods quantity sheet.
33+
- **Number-input spinner arrows removed app-wide** across food, meal, recipe,
34+
and diary edits.
35+
36+
### Fixed
37+
38+
- **Docker image now actually loads DuckDB.** Base swapped from Alpine to
39+
Debian-slim so the OFF mirror feature works end to end. Image grows ~30 MB.
40+
(Reported by @duplaja)
41+
- **Goals: body-stat fallback now appears on the initial "Your Goals" tab**
42+
instead of only after switching tabs. (Issue #46 followup from @duplaja)
43+
- **Quick Calories: macro input width grows with the typed value** so the "g"
44+
stays flush against the digits regardless of length.
45+
46+
---
47+
1248
## [1.0.0-rc.38] — 2026-05-25
1349

1450
### Added

DEPLOY.md

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -145,23 +145,25 @@ Self-hosters on air-gapped networks, in strict-egress environments, or just want
145145

146146
1. Uncomment the OFF mirror volume mount in `docker-compose.yml`:
147147
```yaml
148-
- ${OFF_LOCAL_DB_HOST_PATH:-./off.duckdb}:/data/off.duckdb
148+
- ${OFF_LOCAL_DB_HOST_PATH:-./off.parquet}:/data/off.parquet
149149
```
150150
Note: the mount is read-write so NutriTrace can perform in-place refresh via atomic swap. Earlier docs suggested `:ro`; if you previously set that, change it to writable so scheduled and manual refreshes work.
151151

152152
2. Set the env vars in `.env`:
153153
```bash
154-
OFF_LOCAL_DB_HOST_PATH=/path/on/host/off.duckdb # the host path (will be created if missing)
155-
OFF_LOCAL_DB=/data/off.duckdb # the in-container path
156-
# Optional — pin the download URL (defaults to the official OFF nightly build).
157-
# OFF_LOCAL_URL=https://challenges.openfoodfacts.org/products.duckdb
154+
OFF_LOCAL_DB_HOST_PATH=/path/on/host/off.parquet # the host path (will be created if missing)
155+
OFF_LOCAL_DB=/data/off.parquet # the in-container path
156+
# Optional — pin the download URL (defaults to the maintained Hugging Face Parquet dump).
157+
# OFF_LOCAL_URL=https://huggingface.co/datasets/openfoodfacts/product-database/resolve/main/food.parquet?download=true
158158
# Optional — full air-gap mode (never call api.openfoodfacts.org)
159159
# OFF_LOCAL_ONLY=1
160160
```
161161

162+
Note on file formats: since rc.39, NutriTrace defaults to the OFF Parquet dump on Hugging Face (~7-8 GB) because OFF retired their pre-built native DuckDB snapshot. The lookup code transparently opens the Parquet via DuckDB's `read_parquet()` so per-barcode queries are still fast (DuckDB row-group prunes). If you still have a legacy `.duckdb` file at `OFF_LOCAL_DB`, that path is auto-detected by file extension and continues to work unchanged.
163+
162164
3. `docker compose up -d` to recreate the container.
163165

164-
4. **NutriTrace handles the initial download automatically.** On startup, if `OFF_LOCAL_DB` is set and the file is missing, it pulls the full snapshot (~4 GB) from `OFF_LOCAL_URL` in the background. Lookups during the initial pull fall through to the public OFF API (or, in air-gap mode, return "not found") until the download completes. Watch the container logs for `[off-local] refresh complete` to confirm readiness, or open Settings → Connected Services → Open Food Facts; the banner shows live download progress.
166+
4. **NutriTrace handles the initial download automatically.** On startup, if `OFF_LOCAL_DB` is set and the file is missing, it pulls the full snapshot (~7-8 GB) from `OFF_LOCAL_URL` in the background. Lookups during the initial pull fall through to the public OFF API (or, in air-gap mode, return "not found") until the download completes. Watch the container logs for `[off-local] refresh complete` to confirm readiness, or open Settings → Connected Services → Open Food Facts; the banner shows live download progress.
165167

166168
### Refreshing the mirror
167169

@@ -174,8 +176,8 @@ You can also click **Refresh Now** in the same panel to force an immediate refre
174176
Prefer the command line? You can still drop a fresh file in place manually:
175177

176178
```bash
177-
wget https://challenges.openfoodfacts.org/products.duckdb -O /path/on/host/off.duckdb.new
178-
mv /path/on/host/off.duckdb.new /path/on/host/off.duckdb
179+
wget 'https://huggingface.co/datasets/openfoodfacts/product-database/resolve/main/food.parquet?download=true' -O /path/on/host/off.parquet.new
180+
mv /path/on/host/off.parquet.new /path/on/host/off.parquet
179181
# Optional — trigger reopen without waiting for the next lookup:
180182
docker compose restart
181183
```
@@ -198,7 +200,7 @@ The native Android client normally calls `api.openfoodfacts.org` directly (bypas
198200

199201
The mirror file is **deliberately excluded from NutriTrace's full-backup ZIPs**. It's a ~4 GB reproducible snapshot of public OFF data; bundling it would balloon backups for no real benefit. Backups continue to contain only your SQLite database (foods, meals, diary, settings, etc.) and your uploaded photos. Your chosen Auto-Refresh interval is preserved (it's a tiny `app_config` row), so after restoring on a fresh install the schedule comes back automatically; the next refresh cycle (or a manual Refresh Now) re-populates the mirror file itself.
200202

201-
If you want a mirror snapshot in your own off-site storage, copy the `off.duckdb` host file directly with whatever tool you already use (rsync, restic, borg, etc.) — separate from NutriTrace's backup flow.
203+
If you want a mirror snapshot in your own off-site storage, copy the `off.parquet` (or legacy `off.duckdb`) host file directly with whatever tool you already use (rsync, restic, borg, etc.) — separate from NutriTrace's backup flow.
202204

203205
### Caveats
204206

Dockerfile

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,16 @@ COPY . .
1111
RUN npm run build
1212

1313
# ── Stage 2: Express server + static frontend ────────────────────────────────
14-
FROM node:20-alpine
15-
RUN apk add --no-cache python3 make g++
14+
# Debian-slim base (not Alpine) — the @duckdb/node-bindings-linux-x64 native
15+
# library used by the OFF mirror feature is glibc-linked and won't load on
16+
# musl-based images. On Alpine, DuckDB fails with "Error loading shared
17+
# library ld-linux-x86-64.so.2: No such file or directory" because the
18+
# glibc dynamic linker isn't present. DuckDB does not ship a musl variant
19+
# of those node bindings, so a glibc base is required.
20+
FROM node:20-slim
21+
RUN apt-get update \
22+
&& apt-get install -y --no-install-recommends python3 build-essential \
23+
&& rm -rf /var/lib/apt/lists/*
1624
WORKDIR /app
1725
COPY server/package*.json ./
1826
RUN npm install --omit=dev

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,13 @@ Optional. Connect any OpenID Connect 1.0 compliant identity provider — **Authe
413413

414414
**Mobile**: Android in server-connected mode supports SSO too. The app opens the IdP authorize URL in an in-app browser (Chrome Custom Tabs); the IdP redirects back via `nutritrace://oidc-callback/` deep link, the app intercepts it and signs you in — no manual paste, no token wrangling.
415415

416+
**Logout (RP-initiated)**: when an SSO user signs out, NutriTrace also ends the session at the IdP via the standard OIDC end-session endpoint (using `id_token_hint`), so the next sign-in isn't silently completed by a still-alive IdP session. For this to work, register **two Post Logout Redirect URIs** at your IdP:
417+
418+
- `https://your-nutritrace-host/` (or your `BASE_URL` root) for PWA logouts.
419+
- `nutritrace://oidc-callback` for Android logouts.
420+
421+
If the IdP doesn't expose an `end_session_endpoint` (some don't), logout falls back to a local-only clear and the next login prompts again only if the IdP's own session has expired.
422+
416423
**Security**: client secrets are encrypted at rest using the same key derivation as wearable OAuth tokens. Email-based auto-linking only fires when the IdP explicitly flags the email verified, AND the provider's `auto-register` is enabled — both gates have to be on, since email-based auto-link is the main account-takeover vector if the IdP is dishonest about verification.
417424

418425
---

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 107
23-
versionName "1.0.0-rc.38"
22+
versionCode 108
23+
versionName "1.0.0-rc.39"
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: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@ services:
99
- ${DATA_UPLOADS_PATH}:/data/uploads
1010
# Optional — local Open Food Facts mirror (for air-gapped or
1111
# offline-resilient barcode + name lookups). Uncomment the mount + set
12-
# OFF_LOCAL_DB in .env. NutriTrace downloads the initial ~4 GB snapshot
13-
# automatically on first boot if the file is missing; subsequent
14-
# refreshes happen on a schedule from the Settings UI. Mount is
15-
# read-write so atomic swap during refresh succeeds.
16-
# See DEPLOY.md → "Local Open Food Facts mirror" for the full recipe.
17-
# - ${OFF_LOCAL_DB_HOST_PATH:-./off.duckdb}:/data/off.duckdb
12+
# OFF_LOCAL_DB in .env. NutriTrace downloads the initial ~7-8 GB
13+
# Parquet snapshot from Hugging Face automatically on first boot if
14+
# the file is missing; subsequent refreshes happen on a schedule from
15+
# the Settings UI. Mount is read-write so atomic swap during refresh
16+
# succeeds. File extension is auto-detected — Parquet (default) or
17+
# legacy .duckdb both supported. See DEPLOY.md → "Local Open Food
18+
# Facts mirror" for the full recipe.
19+
# - ${OFF_LOCAL_DB_HOST_PATH:-./off.parquet}:/data/off.parquet
1820
# Forward every variable from .env into the container. Without this, only
1921
# variables explicitly named in the `environment:` block below reach the
2022
# app, so .env settings like INSECURE_COOKIES were silently ignored (#41).
@@ -77,12 +79,14 @@ services:
7779
# Env-defined providers show with a lock badge in Settings → Authentication
7880
# and are read-only there — edit via .env / docker-compose / k8s secrets.
7981
# Optional — Local Open Food Facts mirror (Issue #22). When set,
80-
# barcode + name lookups try the local DuckDB before reaching out
82+
# barcode + name lookups try the local mirror before reaching out
8183
# to api.openfoodfacts.org. Path is the in-container path matching
82-
# the volume mount above. See DEPLOY.md for download / refresh.
83-
# - OFF_LOCAL_DB=${OFF_LOCAL_DB:-} # e.g. /data/off.duckdb
84+
# the volume mount above. File extension is auto-detected — Parquet
85+
# (default since rc.39) or legacy .duckdb both supported. See
86+
# DEPLOY.md for download / refresh.
87+
# - OFF_LOCAL_DB=${OFF_LOCAL_DB:-} # e.g. /data/off.parquet
8488
# - OFF_LOCAL_ONLY=${OFF_LOCAL_ONLY:-} # 1 = air-gap mode, never call remote OFF
85-
# - OFF_LOCAL_URL=${OFF_LOCAL_URL:-} # override download source (defaults to OFF nightly build)
89+
# - OFF_LOCAL_URL=${OFF_LOCAL_URL:-} # override download source (defaults to HF Parquet)
8690
# Any server env var may also be supplied via <NAME>_FILE, for example:
8791
# - SMTP_PASS_FILE=/run/secrets/nutritrace_smtp_pass
8892
# - AI_API_KEY_FILE=/run/secrets/nutritrace_ai_api_key

package.json

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,12 @@
11
{
22
"name": "nutritrace",
3-
"version": "1.0.0-rc.38",
3+
"version": "1.0.0-rc.39",
44
"private": true,
55
"type": "module",
66
"scripts": {
77
"dev": "vite",
88
"build": "vite build",
99
"preview": "vite preview",
10-
"android": "npm run build && npx cap sync android && npx cap open android",
11-
"android:build": "npm run build && npx cap sync android",
12-
"android:run": "npx cap run android",
13-
"cap:sync": "npm run build && npx cap sync",
14-
"cap:add:android": "npx cap add android",
15-
"i18n:check": "node scripts/i18n-check.cjs",
1610
"postinstall": "node scripts/postinstall.cjs"
1711
},
1812
"dependencies": {

0 commit comments

Comments
 (0)