本文へ移動
cccskills
無料GitHub で公開

openfoodjournal

Living project knowledge for OpenFoodJournal — an iOS 26 food journaling app with AI-powered nutrition scanning. Consult this skill FIRST when starting any work on this project. Contains architecture decisions, data models, service contracts, current state, known issues, and conventions. Update this skill whenever requirements, architecture, or project state changes so it stays accurate across sessions.

インストール方法を見る

含まれるファイル(4)

  • SKILL.md29.8 KB
  • references/models.md2.5 KB
  • references/services.md2.7 KB
  • references/views.md3.5 KB

SKILL.md(原文)

インストールする前に、エージェントに与えられる指示の中身を確認できます。

OpenFoodJournal — Project Knowledge

This is the single source of truth for any LLM agent working on this project. Read this before writing code. Update it when things change.

Quick Facts

KeyValue
PlatformiOS 26.2+ (iPhone)
UI FrameworkSwiftUI + Liquid Glass (no #available gating needed)
Data LayerSwiftData (@Model) + CloudKit Private Database. Release: iCloud.k3vnc.OpenFoodJournal; Debug: iCloud.k3vnc.OpenFoodJournal.dev (separate containers, not the same container in two environments)
State Pattern@Observable + @Environment injection (no singletons)
Bundle IDsRelease: k3vnc.OpenFoodJournal; Debug: k3vnc.OpenFoodJournal.dev (OFJ Dev on device)
Build SystemXcode (xcodebuild), no SPM dependencies
Build Verify CommandNever build locally. gh workflow run cloud-ci.yml --ref <branch> — ask the user first. See .claude/rules/ios-builds.md.
Build NotesNo simulator devices exist, so Xcode's run destination falls through to the connected iPhone carrying real data. Local xcodebuild also evicts signing artifacts from shared DerivedData. Cloud CI isolates DerivedData per job and is the only sanctioned way to find out whether something compiles.
AI BackendDirect Gemini REST API (BYOK — user provides own API key, stored in Keychain)
App EntryMacrosApp in OpenFoodJournalApp.swift

Architecture Overview

MacrosApp (creates ModelContainer w/ CloudKit + 4 @Observable services)
  └─ ContentView (4-tab TabView)
       ├─ Journal tab → DailyLogView (WeeklyCalendarStrip, macro summary, meal sections, RadialMenuButton)
       ├─ Food Bank tab → FoodBankView (searchable, sortable saved food list, swipe-to-edit, "+" menu: Scan/Manual/Search OFF)
       ├─ History tab → HistoryView (CalendarGridView with progress rings, MacroChartView, macro cards → NutritionDetailView)
       └─ Settings tab → SettingsView (goals, health, data export)

Radial FAB: DailyLogView uses RadialMenuButton — a "+" icon at bottom center that fans out Scan / Manual / Containers / Food Bank in an upper semicircle (210°–330°). Supports tap-to-toggle and drag-to-action. Containers are accessed from here instead of a separate tab.

Service injection: All services (NutritionStore, ScanService, HealthKitService, UserGoals, OpenFoodFactsService) are created in MacrosApp.init() and passed via .environment(). Views consume them with @Environment(ServiceType.self). SyncService was removed — CloudKit handles sync natively via ModelConfiguration(cloudKitDatabase:).

Sheet management: DailyLogView uses a single DailyLogSheet enum with .sheet(item:) — never multiple booleans.

Data Models

See references/models.md for full property lists.

  • DailyLog — @Model, keyed by date normalized to midnight (no @Attribute(.unique) — CloudKit can't enforce uniqueness; app-level dedup in fetchOrCreateLog(for:)). Owns [NutritionEntry]? (optional for CloudKit) via cascade delete. Uses safeEntries computed property for reads.
  • NutritionEntry — @Model, stores core macros (cal/protein/carbs/fat) + dynamic micronutrients: [String: MicronutrientValue] + brand/serving/servingCount/servingQuantity/servingUnit/servingMappings. @Attribute(.externalStorage) on sourceImage.
  • SavedFood — @Model, reusable food template in Food Bank. Same fields as NutritionEntry minus meal/log context. Includes lastUsedAt: Date (defaults to createdAt) for "Last Used" sorting. Created from entries, manual input, or directly from ManualEntryView's "Add to Journal & Food Bank" action.
  • TrackedContainer — @Model, weight-based container tracking. Snapshots food nutrition at creation time. Start weight → final weight → derived consumption via consumedServings math.
  • UserGoals — @Observable @MainActor, uses @ObservationIgnored @AppStorage for each goal property to avoid property-wrapper conflicts.
  • Preferences — @Model, singleton row for UI customization. Stores ringSlot1..5 (nutrient IDs for MacroSummaryBar configurable rings). Preferences.current(in:) static factory fetches-or-creates the singleton. Synced to Turso via GET/PUT /api/preferences. Added to ModelContainer in app init. Sheets use @Bindable var prefs: Preferences for direct binding.
  • MealType — enum: .breakfast, .lunch, .dinner, .snack
  • ScanMode — enum: .label, .foodPhoto, .barcode, .manual
  • ServingSize — enum: .mass(grams:), .volume(ml:), .both(grams:ml:). Stores canonical SI values. Has availableUnits: [String] (dimension-appropriate unit list), convert(_:from:to:) -> Double? (handles same-dimension and cross-dimension via density for .both). Static tables: massConversions (g/oz/kg/lb), volumeConversions (mL/cup/tbsp/tsp/fl oz/L). type: String returns "mass"/"volume"/"both" for JSON serialization.
  • ServingMapping — Codable struct with from: ServingAmount / to: ServingAmount for per-food unit conversions (e.g. 1 cup = 244g). Legacy field kept alongside ServingSize.
  • MicronutrientValue — Codable struct with value: Double / unit: String for dynamic micronutrient storage

Services

See references/services.md for full API contracts.

  • NutritionStore — SwiftData CRUD. log(), fetchLog(), fetchLogs(), delete(), saveEntry(), exportCSV(). Pure local operations — CloudKit sync is handled automatically by SwiftData's ModelConfiguration(cloudKitDatabase:). No sync service reference, no fire-and-forget Tasks.
  • ScanService — Resizes images to max 2000px (UIGraphicsImageRenderer) then JPEG 0.90 before direct Gemini REST API call (generativelanguage.googleapis.com) → NutritionEntry (not yet inserted). Uses GeminiModelConfig static configs: .labelScan (gemini-3.1-flash-lite-preview, MINIMAL thinking) and .foodPhotoScan (gemini-3.1-pro-preview, HIGH thinking). Includes automatic fallback to gemini-2.5-flash/pro on 500/503 errors. Loads API key from KeychainService. User reviews in ScanResultCard before committing. Logs scan duration via ContinuousClock and stores it on NutritionEntry.scanDurationMs.
  • KeychainService — Static helper for secure Keychain storage (Security framework). Stores Gemini API key under service k3vnc.OpenFoodJournal, account gemini-api-key. Methods: save(_:for:), load(for:), delete(for:), hasGeminiAPIKey, geminiAPIKey.
  • ServingConverter — Pure-value struct encapsulating all serving-unit conversion math. 4-strategy factorFor(_:) (ServingSize tables → direct mapping → chain → SI bridge), availableUnits, and scaledCalories/Protein/Carbs/Fat. Used by both EditEntryView and LogFoodSheet to eliminate duplicate conversion logic.
  • HealthKitService — Opt-in Apple Health writes (one HKQuantitySample per macro). Reads activeEnergyBurned.- OpenFoodFactsService — Text search and barcode lookup against the Open Food Facts REST API. Search uses search.openfoodfacts.org (Elasticsearch-backed, the v1 CGI endpoint returns 503). Barcode lookup uses world.openfoodfacts.org/api/v2/product/{code}. Batch-fetches full product details after search using withTaskGroup. OFFProduct model stores full nutrition. lookupBarcode() manages UI state (isLoading/errorMessage); internal fetchProductByBarcode() is stateless for batch use. Used by both OpenFoodFactsSearchView (text search) and ScanCaptureView (barcode camera scan).- UserGoals — Daily targets for cal/protein/carbs/fat, persisted in UserDefaults.

View Map

See references/views.md for detailed view hierarchy and notable patterns.

Scan Flow (Core Value Prop)

User taps Scan → CameraController (AVCaptureSession) → JPEG
  → Prompt overlay: food photo shows text input, label scan skips prompt
  → ScanService.scan(image, mode) → loads API key from KeychainService
  → Builds JSON request with base64-encoded image + prompt
  → POST to https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent?key={key}
  → ContinuousClock measures full round-trip duration
  → Label mode: gemini-3.1-flash-lite-preview (fast, MINIMAL thinking)
  → Food photo mode: gemini-3.1-pro-preview (HIGH thinking for estimation)
  → Fallback on 500/503: gemini-2.5-flash (labels) / gemini-2.5-pro (food photos)
  → Parses last text part (skips thinking parts) → JSON → GeminiNutritionResponse
  → GeminiNutritionResponse → NutritionEntry (NOT inserted yet)
  → Entry gets scanDurationMs set from ContinuousClock measurement
  → ScanResultCard (editable, shows duration badge) → User taps "Add to Journal"
  → NutritionStore.log(entry, to: date) → SwiftData insert (CloudKit syncs automatically)
  → Auto-creates SavedFood in Food Bank
  → HealthKitService.write(entry) if enabled

Barcode Scan Flow

User taps "Scan Barcode" card → CameraController (AVCaptureSession) → JPEG
  → promptOverlay: "Look Up" button (no text prompt for barcode mode)
  → Vision VNDetectBarcodesRequest detects barcode from photo
  → OpenFoodFactsService.lookupBarcode(barcodeValue)
  → GET https://world.openfoodfacts.org/api/v2/product/{code}?fields=...
  → OFFProduct → ManualEntryView(defaultDate:, prefillProduct:)
  → User reviews/edits pre-filled nutrition data → saves to journal

Open Food Facts Search Flow

User taps "Search Open Food Facts" in Food Bank "+" menu
  → OpenFoodFactsSearchView (searchable, Enter-only via .onSubmit)
  → GET https://search.openfoodfacts.org/search?q={query}&fields=...&page_size=25
  → Returns barcodes → batch-fetch full products via withTaskGroup
  → OFFProductRow (styled like SavedFoodRowView — macros, calories, serving)
  → Tap row → ManualEntryView(defaultDate:, prefillProduct: product)
  → User reviews/edits pre-filled data → saves to journal

CloudKit Sync Architecture (app-store branch)

iOS App (SwiftData + CloudKit)
  ←→ iCloud Private Database (automatic, free, multi-device)

  → ScanService → Direct Gemini REST API (BYOK, zero server dependency)
  → API key stored in iOS Keychain via KeychainService

Strategy: SwiftData's ModelConfiguration(cloudKitDatabase: .private("iCloud.k3vnc.OpenFoodJournal")) handles all sync automatically. No sync code in the iOS app. Zero server maintenance for data operations.

CloudKit requirements enforced on all models:

  • All stored properties have defaults (including fully qualified enum defaults: MealType.snack not .snack)
  • No @Attribute(.unique) (CloudKit can't enforce uniqueness)
  • Relationships are optional (var entries: [NutritionEntry]? = [])
  • No .deny delete rules

Entitlements: OpenFoodJournal.entitlements includes iCloud (CloudKit), Push Notifications (aps-environment), background mode (remote-notification).

Data migration: TursoMigrationView was deleted — CloudKit replaced Turso and the migration tool is dead code. All data now syncs via CloudKit Private Database automatically.

Server: Express.js on Render — exists in repo for main branch usage but is NOT used by the app-store branch at all. The iOS app calls Gemini REST API directly (BYOK). No server dependency.

Turso migration pattern (server/db.js): Use PRAGMA table_info(table_name) to check existing columns before running ALTER TABLE ... ADD COLUMN. Idempotent on re-deploy. Example:

const info = await db.execute("PRAGMA table_info(nutrition_entries)");
const cols = info.rows.map(r => r.name);
if (!cols.includes("serving_type")) {
  await db.execute("ALTER TABLE nutrition_entries ADD COLUMN serving_type TEXT");
  // ...
}

Server tables: daily_logs, nutrition_entries, saved_foods, tracked_containers, user_goals — schema in server/db.js.

API endpoints (mounted at /api):

  • GET /api/sync?since=ISO_TIMESTAMP — Full or incremental data pull
  • POST/GET/PUT/DELETE /api/entries, /api/foods, /api/containers
  • GET/PUT /api/goals
  • Entries auto-create their parent daily_log on POST

Sync integration points — CloudKit sync is fully automatic. No per-view sync calls needed. All SwiftData mutations are automatically pushed to CloudKit.

Environment vars (Render): TURSO_DATABASE_URL, TURSO_AUTH_TOKEN (falls back to file:local.db for dev)

Known Gotchas

  1. CLI builds are stricter than the Xcode IDE — missing import SwiftData may compile in previews but fail in CLI builds. Verify on Cloud CI, never with a local xcodebuild.
  2. .easeInOut is a static property — don't write .easeInOut(value:). The value: belongs to .animation(_:value:).
  3. Ternary type mismatch — .primary is HierarchicalShapeStyle, .orange is Color. Use Color.primary to unify.
  4. @ObservationIgnored on @AppStorage — required in UserGoals to avoid double-wrapper conflict. Every new @AppStorage property needs it.
  5. private enum across structs — private scopes to the type, not the file. Use fileprivate for shared-file enums.
  6. ModelContainer(for:configurations:) — not schema:. The Schema wrapper is only for migrations.
  7. Swift named parameters must be in declaration order — adding a new init parameter doesn't let you put it anywhere in call sites. Reorder both the declaration and all call sites to match. Build will catch mismatches.
  8. PRAGMA table_info() not IF NOT EXISTS for ALTER TABLE — SQLite doesn't support ALTER TABLE ADD COLUMN IF NOT EXISTS. Use PRAGMA to check first.
  9. servingQuantity/servingUnit must be set after scaling in LogFoodSheet — toNutritionEntry() copies the food template's serving values; after scaling macros in logButton, write entry.servingQuantity = quantity and entry.servingUnit = selectedUnit so EditEntryView opens with the correct baseline.
  10. .swipeActions is silently ignored outside a List — it does nothing in LazyVStack, ScrollView, or VStack. If swipe actions don't fire, check that the ForEach is inside a List. The modifier compiles without error regardless of container.
  11. Smooth tappable List rows with swipeActions: use Button + .buttonStyle(.plain) — Default Button style causes ~150ms tap disambiguation delay. onTapGesture + contentShape(Rectangle()) eliminates tap delay but makes swipes choppy (TapGesture blocks swipe tracking). The correct pattern is Button { } label: { YourRowView() }.buttonStyle(.plain) — UIKit-optimized for List swipe coexistence, smooth on both taps and swipes.
  12. ZStack gesture priority — in a ZStack, the last (top) view receives gestures first. Put a Color.clear.contentShape(Rectangle()).onTapGesture { dismiss() } layer before the overlay content to create an outside-tap dismiss. Views on top intercept taps; the clear layer catches everything that falls through.
  13. Color.clear needs .contentShape(Rectangle()) — transparent views have no hit-test area by default. Without contentShape, taps pass through as if the view doesn't exist.
  14. @Model enum defaults must be fully qualified — var mealType: MealType = .snack fails during macro expansion. Use MealType.snack. The error message is unhelpful (just says macro expansion failed).
  15. CloudKit optional relationships need safeEntries pattern — var entries: [NutritionEntry]? = [] requires unwrapping everywhere. Add var safeEntries: [NutritionEntry] { entries ?? [] } and use that for reads. Use log.entries?.append(entry) for writes.
  16. KnownMicronutrient.Category cases are .vitamin/.mineral — not .vitamins/.minerals. The enum raw values are plural ("Vitamins"/"Minerals") but the Swift case names are singular.
  17. Debug must remain a separate installed app and data domain — Debug uses bundle ID k3vnc.OpenFoodJournal.dev, display name OFJ Dev, and CloudKit container iCloud.k3vnc.OpenFoodJournal.dev; Release uses k3vnc.OpenFoodJournal and iCloud.k3vnc.OpenFoodJournal. Do not collapse either the bundle IDs or container identifiers.
  18. Gemini image thinking levels are a model-specific enum — gemini-3.1-flash-lite-image officially supports exactly minimal and high: https://ai.google.dev/gemini-api/docs/models/gemini-3.1-flash-lite-image. Production uses the typed GeminiFlashLiteImageThinkingLevel.high. Never send low to this image model or reuse a text-model thinking string. The exact production request builder is covered by deterministic tests and a protected live TestFlight API contract.
  19. AVCaptureSession.startRunning() / stopRunning() block — they take hundreds of milliseconds to a second. CameraController runs them on k3vnc.openfoodjournal.camera.session, not the main actor. Calling them from @MainActor setup() freezes the scan sheet for ~1s.

What's New Sheet Pattern

WhatsNewSheet is version-gated via @AppStorage("lastSeenVersion") in ContentView. On appear, if lastSeenVersion != currentVersion (from CFBundleShortVersionString), the sheet is presented. On dismiss, lastSeenVersion is updated. To add features for a new version, edit WhatsNewSheet.swift and bump the version header text. Each feature is a FeatureRow(icon:, color:, title:, description:).

v1.1 features: Open Food Facts search, barcode scanning, faster food photo scans (lite model default), reorderable nutrient rings, nutrition history navigation (day/week/month with date picker).

Entitlements (app-store branch)

Already configured in OpenFoodJournal.entitlements:

  • com.apple.developer.icloud-services: CloudKit
  • com.apple.developer.icloud-container-identifiers: iCloud.k3vnc.OpenFoodJournal
  • aps-environment: development (auto-switches to production on Archive)
  • UIBackgroundModes: remote-notification

Still needed (must add to entitlements):

  • com.apple.developer.healthkit (HealthKit usage compiles but entitlement missing)

Still needed (provisioning profile):

  • Enable iCloud + Push Notifications capabilities in Apple Developer portal
  • Regenerate provisioning profile

Branches

  • main — Original Turso sync architecture. SyncService.swift present, all views have fire-and-forget sync Tasks. Used for developer's personal Turso instance.
  • app-store — CloudKit sync, no Turso dependency. SyncService.swift deleted, all sync Tasks removed. TursoMigrationView for one-time data import. This is the App Store submission branch.

Conventions

  • Comments: Explain why, not what. Entry-level devs should understand each function's purpose.
  • File creation: Build large files in small chunks to avoid network errors.
  • Retrospectives: Live in docs/. Update when later fixes change the story.
  • Skills: This file is the project skill. Update it when architecture or requirements change.
  • Commits: Descriptive messages. Push after every significant change.

DailyLogView container: Uses a List (not ScrollView+LazyVStack) with .listStyle(.plain) + .scrollContentBackground(.hidden). WeeklyCalendarStrip and MacroSummaryBar are plain List rows with listRowBackground(Color.clear) + listRowSeparator(.hidden). Meal sections use MealSectionView which returns a Section{} that becomes a proper sticky List section header in a List context. All swipe actions are on the MealSectionView Button wrapper (not on EntryRowView).

RadialMenuButton: Option bubbles support direct .onTapGesture (as well as drag-to-select). A Color.clear.contentShape(Rectangle()).ignoresSafeArea() layer behind GlassEffectContainer dismisses the menu when tapping outside. The layer is only inserted into the ZStack when isOpen == true. Option label text has a subtle drop shadow for legibility over light/glass backgrounds.

Swipe mappings:

  • FoodBankView row: trailing (swipe left) = Edit (blue) + Delete (red, no full-swipe); leading (swipe right) = Add to journal (green)
  • MealSectionView row: trailing (swipe left) = Delete; leading (swipe right) = Edit — both on the outer Button wrapper (never on EntryRowView to avoid double-registration gesture lag)

FoodNutrientBreakdownView: Inverse of NutrientBreakdownView — shows all nutrients (macros + micros) for a specific food across the selected period. Accessed by tapping a food row in NutrientBreakdownView's "By Food" section.

EditEntryView: Has full serving-mappings section (same as LogFoodSheet). Uses shared AddServingMappingSheet (defined in LogFoodSheet.swift, internal not private). addMapping() calls nutritionStore.saveEntry(entry).

Current State (Last Updated: 2026-07-31)

  • Branch: app-store — CloudKit migration complete
  • Turso is live, not removed. TursoMirrorService is constructed in OpenFoodJournalApp.init and is a push-only, generation-pruned mirror. It is hard-disabled in Debug builds — isEnabled returns false and loadCredentials() throws — so no network path reaches Turso from a developer build. Do not re-enable it.
  • Debug builds install as OFJ Dev (k3vnc.OpenFoodJournal.dev) alongside the TestFlight app, with a separate local sandbox
  • App structure complete: all models, services, and views implemented
  • 4-tab layout: Journal, Food Bank, History, Settings (Containers accessed via RadialMenuButton)
  • Compile verification runs on Cloud CI (.github/workflows/cloud-ci.yml), never locally
  • SwiftData + CloudKit Private Database for data persistence and sync
  • Render proxy deployed at openfoodjournal.onrender.com (Gemini scan proxy only)
  • Food Bank: save foods from scan/manual entry, browse/search/sort, log to journal
  • Open Food Facts integration: search 4M+ products, add to journal/food bank, debounced search, per-serving nutrition
  • Food Bank "+" toolbar menu: Search Open Food Facts, Manual Entry (replaces empty-state-only guidance)
  • Settings: OFF contribute toggle (off.contributeEnabled, default off) in Integrations section
  • Container Tracking: create from Food Bank food, enter start weight, complete with final weight
  • Serving Mappings: per-food unit conversions, editable in EditEntryView
  • NutritionDetailView: ScrollView on JournalCalorieBackground (same page treatment as History/calendar). Macro summary and vitamin/mineral sections use NutritionSurface glass cards. Avoid insetGrouped List here — glass on grouped-list chrome creates a double-card halo.
  • External TestFlight: ci/release-config.json carries externalTestFlightGroupID (empty by default). When set, asc publish testflight distributes to it alongside the internal group and --submit --confirm asks for Apple's Beta App Review. asc skips the submission when no external group was added, so an empty value keeps internal-only behaviour.
  • App Store product-page copy: app-store.yml resolves description, keywords and promotionalText before the protected approval gate, in order: committed metadata/releases/<version>/<locale>/store.json, the Claude Code agentic harness, the Bifrost router, then keeping the live text. Every tier must pass .github/scripts/validate-store-metadata.sh. The proposed copy appears in the approval summary, and generated fields are excluded from --copy-metadata-from so they are not overwritten.
  • History micronutrient pins: long-press chip → context menu Pin/Unpin. Persisted by PinnedMicronutrientSettings (history.pinnedMicronutrientIDs) and included in AppSettingsRecord backups.
  • FoodNutrientBreakdownView: inverse of NutrientBreakdownView — food → all nutrients. NavigationLink from NutrientBreakdownView "By Food" rows
  • LogFoodSheet editable micronutrients: collapsible DisclosureGroup with editable text fields for each micronutrient, values applied at log time
  • Radial menu text shadow: option labels have .shadow() for legibility over glass
  • Swipe gesture lag fix: swipe actions consolidated on MealSectionView Button wrapper (removed from EntryRowView), SavedFoodRowView has .contentShape(Rectangle()), EntryRowView uses static DateFormatter
  • WeeklyCalendarStrip: horizontally scrollable week strip with momentum snapping
  • Comprehensive micronutrient tracking: 30 FDA nutrients with daily values
  • Entitlements configured: iCloud (CloudKit), Push Notifications, Camera, HealthKit descriptions
  • BYOK Gemini integration: direct REST API calls, no server proxy needed
  • KeychainService for secure API key storage
  • Onboarding flow: 6 pages (Welcome → API Key → Goals → Camera → Radial Menu Tutorial → HealthKit)
  • RadialMenuDemo: pure SwiftUI phase-based animation teaching press-and-drag gesture (in onboarding page 4)
  • Settings: API key management section (save/delete/change key), "Show Onboarding" replay button, "Sources & Disclaimers" link
  • App Store Guideline 1.4.1 compliance: HealthDisclaimerView with FDA citations (21 CFR §101.9), AI estimation disclaimers, Atwater system citation, and general medical disclaimer. Inline citations on NutritionDetailView, ScanResultCard, and GoalsEditorView.
  • App Store audit complete: HealthKit entitlement, Privacy Policy, PrivacyInfo.xcprivacy, AGPL→MIT licensing
  • sourceImage removed from NutritionEntry and SavedFood (was stored but never displayed)
  • TursoMigrationView deleted — CloudKit replaced Turso, migration tool is dead code
  • No unit tests beyond Xcode template stubs

App Store Submission Notes

First submission rejected (Guideline 1.4.1 — Physical Harm): App provided health/nutrition data without citations. Fixed by adding HealthDisclaimerView (Settings → Sources & Disclaimers) with FDA Daily Values links, Atwater system reference, AI estimation disclaimer, and general medical disclaimer. Inline citations added to NutritionDetailView, ScanResultCard, and GoalsEditorView.

Other audit findings to address before next submission:

  • README.md describes main branch architecture (Turso/Express), not app-store branch. Update README or add branch-specific note.
  • Reviewer notes previously contained false HealthKit claim ("data is never read") — corrected to list all read/write types.
  • server/ directory and render.yaml are dead code on app-store branch — reviewers clicking the GitHub link may be confused.
  • AGPL-3.0 license has App Store exception in LICENSE_NOTICE.md — Apple may or may not flag this.
  • Privacy policy is web-only (GitHub link) — fails offline. Consider embedding a copy in-app.

Planned Work (Backlog)

UX / Feature Improvements

  • Weekly/monthly view should display daily averages — In NutritionDetailView, when selectedPeriod is .weekly or .monthly, macroTotals currently accumulates totals. For multi-day periods, display the daily average (total ÷ days in period) so "14,000 kcal this week" becomes "2,000 kcal/day avg". The period picker label should reflect this ("Weekly Avg", "Monthly Avg"). Same applies to the comparisonCard values in HistoryView.weekComparisonSection (already divides by 7 — verify .monthly path does the same).

Open Food Facts Integration

  • Open Food Facts (OFF) is a free, open database of 4M+ food products. Integrated into the Food Bank for searching and adding foods without scanning.
  • Service: OpenFoodFactsService — @Observable @MainActor service for OFF REST API calls (text search + barcode lookup). Injected via .environment() from MacrosApp.
  • API endpoints used:
    • Text search: GET https://search.openfoodfacts.org/search?q={query}&fields=product_name,brands,code&page_size=25 — dedicated Elasticsearch-backed search service (the legacy v1 CGI endpoint returns 503)
    • Barcode/product lookup (v2): GET https://world.openfoodfacts.org/api/v2/product/{barcode}?fields=... — full nutrition data
    • Two-step flow: Search returns lightweight hits (name, brand, code via OFFSearchHit); tapping a result fetches full nutrition via barcode lookup → OFFProduct
    • Fields requested (for product detail): product_name,brands,nutriments,serving_size,serving_quantity,code
    • Nutriments mapping: energy-kcal_100g → calories, proteins_100g → protein, carbohydrates_100g → carbs, fat_100g → fat, plus micronutrients (fiber, sugar, sodium, etc.)
    • User-Agent: OpenFoodJournal/1.0 (openfoodjournal@example.com) — required by OFF API policy
    • Rate limits: 10 req/min for search, 100 req/min for product reads
    • No auth required for reads; writes (contribute) require OFF account credentials
  • UI access points:
    • FoodBankView toolbar: "+" button (leading of sort button) → Menu with 3 options: Scan, Manual Entry, Search Open Food Facts
    • Scan and Manual Entry reuse existing sheets (ScanView via DailyLogSheet, ManualEntryView)
    • Search Open Food Facts opens OpenFoodFactsSearchView — debounced text input, paginated results, tap to review details
    • Result action: Same pattern as ManualEntryView — "Add to Journal" or "Add to Journal & Food Bank" via toolbar Menu
  • Settings: @AppStorage("off.contributeEnabled") toggle (default: false) in SettingsView. When enabled, scanned nutrition data is submitted back to OFF after the user logs it. Write API uses POST /cgi/product_jqm2.pl with user credentials.
  • Data flow: OFF search result → OFFProduct struct (Codable) → converted to NutritionEntry / SavedFood with scanMode: .manual and brand preserved. Nutrition values from OFF are per-100g; conversion to serving-based values uses serving_size field when available.
  • No SPM dependency: Direct URLSession calls, no OFF Swift SDK imported (keeps zero-dependency architecture).

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

同じリポジトリのスキル

概要と使いどころ

Evaluates code against Apple's App Store Review Guidelines. Use this skill when reviewing iOS, macOS, tvOS, watchOS, or visionOS app code (Swift, Objective-C, React Native, or Expo) to identify potential App Store rejection issues before submission. Triggers on tasks involving app review preparation, compliance checking, or App Store submission readiness.

日本語の概要は準備中です。原文の説明を表示しています。

kvn8888/OpenFoodJournal52026年10月10日 更新

Create a new App Store Connect app record via browser automation. Use when there is no public API for app creation and you need an agent to drive the New App form.

日本語の概要は準備中です。原文の説明を表示しています。

kvn8888/OpenFoodJournal52026年10月10日 更新

Run an offline ASO audit on canonical App Store metadata under `./metadata` and surface keyword gaps using Astro MCP. Use after pulling metadata with `asc metadata pull`.

日本語の概要は準備中です。原文の説明を表示しています。

kvn8888/OpenFoodJournal52026年10月10日 更新

Track build processing, find latest builds, and clean up old builds with asc. Use when managing build retention or waiting on processing.

日本語の概要は準備中です。原文の説明を表示しています。

kvn8888/OpenFoodJournal52026年10月10日 更新

Guidance for using asc cli in this repo (flags, output formats, pagination, auth, and discovery). Use when asked to run or design asc commands or interact with App Store Connect via the CLI.

日本語の概要は準備中です。原文の説明を表示しています。

kvn8888/OpenFoodJournal52026年10月10日 更新

Triage TestFlight crashes, beta feedback, and performance diagnostics using asc. Use when the user asks about TF crashes, TestFlight crash reports, beta tester feedback, app hangs, disk writes, launch diagnostics, or wants a crash summary for a build or app.

日本語の概要は準備中です。原文の説明を表示しています。

kvn8888/OpenFoodJournal52026年10月10日 更新

kvn8888 のスキルをすべて見る

このスキルの問題を報告する