Fix usability bugs and USDA importer performance
All checks were successful
parsley/pipeline/head This commit looks good

- Remove duplicate Save buttons in log creator/editor (fired with null data before recipe loaded)
- Redirect to new resource after creating recipe/log instead of dropping back to list
- Fix TheFoodCreator Cancel linking to dead route /food → /foods
- Refactor AppSearchText to use defineModel; fix search box not initializing from URL
- Fix TheCalculator variable shadowing bug (ingredient ref never updated on food select)
- Refactor UsdaImporter to use insert_all! instead of per-record save! (~240k branded foods)
- Fix string-based ndbn min comparison in build_enumerator (fragile on non-padded IDs)
- Add CLAUDE.md with project overview and architecture notes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Dan Elbert 2026-04-20 14:42:22 -05:00
parent df99f800b9
commit 233cea022a
8 changed files with 75 additions and 46 deletions

View File

@ -48,7 +48,7 @@ class LogsController < ApplicationController
@log.source_recipe = @recipe
if @log.save
render json: { success: true }
render json: { id: @log.id }
else
render json: @log.errors, status: :unprocessable_entity
end

View File

@ -48,7 +48,7 @@ class RecipesController < ApplicationController
@recipe.user = current_user
if @recipe.save
render json: { success: true }
render json: { id: @recipe.id }
else
render json: @recipe.errors, status: :unprocessable_entity
end

View File

@ -4,7 +4,7 @@
<food-edit :food="food" :validation-errors="validationErrors" action="Creating"></food-edit>
<button type="button" class="button is-primary" @click="save">Save</button>
<router-link class="button is-secondary" to="/food">Cancel</router-link>
<router-link class="button is-secondary" to="/foods">Cancel</router-link>
</div>
</template>

View File

@ -8,11 +8,6 @@
</div>
</log-edit>
<div class="buttons">
<button type="button" class="button is-primary" @click="save">Save Log</button>
<router-link class="button is-secondary" to="/">Cancel</router-link>
</div>
</div>
</template>
@ -51,7 +46,7 @@
loadResource(
api.postLog(log)
.then(() => router.push('/'))
.then(data => router.push({ name: 'log', params: { id: data.id } }))
.catch(Errors.onlyFor(Errors.ApiValidationError, err => validationErrors.value = err.validationErrors()))
);
}

View File

@ -8,11 +8,6 @@
</div>
</log-edit>
<div class="buttons">
<button type="button" class="button is-primary" @click="save">Save Log</button>
<router-link class="button is-secondary" to="/">Cancel</router-link>
</div>
</div>
</template>
@ -45,7 +40,7 @@
validationErrors.value = {};
loadResource(
api.patchLog(log.value)
.then(() => router.push('/'))
.then(() => router.push({ name: 'log', params: { id: log.value.id } }))
.catch(Errors.onlyFor(Errors.ApiValidationError, err => validationErrors.value = err.validationErrors()))
);
}

View File

@ -42,7 +42,7 @@
validationErrors.value = {};
loadResource(
api.postRecipe(recipe.value)
.then(() => router.push('/'))
.then(data => router.push({ name: 'recipe', params: { id: data.id } }))
.catch(Errors.onlyFor(Errors.ApiValidationError, err => validationErrors.value = err.validationErrors()))
);
}

View File

@ -378,49 +378,62 @@ class UsdaImporter
end
build_enumerator(opened_files).each_slice(500) do |slice|
UsdaFood.transaction do
slice.each do |data|
now = Time.current
food_attrs = []
weight_groups = []
food = UsdaFood.new
slice.each do |data|
food = UsdaFood.new
weight_hashes = []
data.each do |name, rows|
file_info = FILES[name]
obj = food
rows.each do |row|
if file_info[:map_into]
obj = food.send(file_info[:map_into]).build
end
if file_info[:static]
file_info[:static].each do |k, v|
obj.send("#{k}=", v)
end
end
data.each do |name, rows|
file_info = FILES[name]
rows.each do |row|
if file_info[:map_into]
w = {}
file_info[:static]&.each { |k, v| w[k.to_s] = v }
file_info[:map].each { |db, col| w[db.to_s] = row[col] }
weight_hashes << w
else
file_info[:static]&.each { |k, v| food.send("#{k}=", v) }
if file_info[:map_function]
file_info[:map_function].call(obj, row)
file_info[:map_function].call(food, row)
else
file_info[:map].each do |db, col|
obj.send("#{db}=", row[col])
end
file_info[:map].each { |db, col| food.send("#{db}=", row[col]) }
end
end
end
end
food.save!
attrs = food.attributes.except('id')
attrs['created_at'] = now
attrs['updated_at'] = now
food_attrs << attrs
weight_groups << weight_hashes
end
result = UsdaFood.insert_all!(food_attrs, returning: %w[id ndbn])
id_idx = result.columns.index('id')
ndbn_idx = result.columns.index('ndbn')
ndbn_to_id = result.rows.each_with_object({}) { |row, h| h[row[ndbn_idx]] = row[id_idx] }
all_weights = []
food_attrs.each_with_index do |fa, i|
food_id = ndbn_to_id[fa['ndbn']]
weight_groups[i].each do |w|
all_weights << w.merge('usda_food_id' => food_id, 'created_at' => now, 'updated_at' => now)
end
end
UsdaFoodWeight.insert_all!(all_weights) if all_weights.any?
end
ensure
opened_files.each { |k, v| v.close }
sorted_files.each { |k, v| `rm #{v}` }
end
Food.where('ndbn != ?', '').where('ndbn IS NOT NULL').each do |i|
Food.where('ndbn != ?', '').where('ndbn IS NOT NULL').find_each do |i|
i.set_usda_food(i.usda_food)
i.save!
end
@ -444,7 +457,7 @@ class UsdaImporter
loop do
break if enumerate_data.values.all? { |d| d[:done] }
current_ndbn = enumerate_data.select { |_, d| !d[:done] }.values.map { |d| d[:next_ndbn] }.min
current_ndbn = enumerate_data.select { |_, d| !d[:done] }.values.min_by { |d| d[:next_ndbn].to_i }[:next_ndbn]
results = Hash.new { |hash, key| hash[key] = [] }
enumerate_data.each do |name, data|

View File

@ -3,19 +3,45 @@ require 'usda_importer'
RSpec.describe UsdaImporter do
it 'imports' do
i = UsdaImporter.new(Rails.root.join('spec', 'test_data'))
i.import
subject(:import) { UsdaImporter.new(Rails.root.join('spec', 'test_data')).import }
it 'imports the correct number of foods with weights' do
import
expect(UsdaFood.count).to eq 5
butter = UsdaFood.where(ndbn: '01001').first
butter = UsdaFood.find_by(ndbn: '01001')
expect(butter).not_to be_nil
expect(butter.usda_food_weights.count).to eq 4
clif_bar = UsdaFood.where(ndbn: '45042066').first
clif_bar = UsdaFood.find_by(ndbn: '45042066')
expect(clif_bar).not_to be_nil
expect(clif_bar.usda_food_weights.count).to eq 1
end
it 'imports SR28 nutrition fields correctly' do
import
butter = UsdaFood.find_by(ndbn: '01001')
expect(butter.kcal).to eq 717
expect(butter.protein).to eq 0.85
expect(butter.source).to eq 'sr'
end
it 'imports branded nutrition fields via map_function' do
import
clif_bar = UsdaFood.find_by(ndbn: '45042066')
expect(clif_bar.kcal).to eq 368
expect(clif_bar.protein).to eq 13.24
expect(clif_bar.source).to eq 'bf'
end
it 'updates linked Food records with usda nutrition data' do
food = create(:food, ndbn: '01001')
import
food.reload
expect(food.kcal).to eq 717
end
end