Commit Graph
2528 Commits
Author SHA1 Message Date
5331a7c831 Filter harvest reminders in SQL instead of Ruby memory (#4831)
* Filter harvest reminders in SQL instead of Ruby memory

- Add `harvest_in_next_week` scope to `PredictHarvest` concern
- Use SQL scope in `NotifierMailer#harvest_reminder` and `exists?` in `growstuff:send_harvest_reminders` task
- Add unit specs for `.harvest_in_next_week` scope and rake task

Co-authored-by: CloCkWeRX <365751+CloCkWeRX@users.noreply.github.com>

* Filter harvest reminders in SQL instead of Ruby memory

- Add `harvest_in_next_week` scope to `PredictHarvest` concern
- Use SQL scope in `NotifierMailer#harvest_reminder` and `exists?` in `growstuff:send_harvest_reminders` task
- Add unit specs for `.harvest_in_next_week` scope and rake task

Co-authored-by: CloCkWeRX <365751+CloCkWeRX@users.noreply.github.com>

* Filter harvest reminders in SQL instead of Ruby memory

- Add `harvest_in_next_week` scope to `PredictHarvest` concern
- Use SQL scope in `NotifierMailer#harvest_reminder` and `exists?` in `growstuff:send_harvest_reminders` task
- Add unit specs for `.harvest_in_next_week` scope and rake task

Co-authored-by: CloCkWeRX <365751+CloCkWeRX@users.noreply.github.com>

---------

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
2026-09-22 19:18:57 +09:30
Brenda Wallace 3d64118cdf Merge pull request #4837 from Growstuff/feat/fluent-crop-icons
Give crops icons from Fluent Emoji, chosen by crop wranglers
2026-09-22 18:45:45 +09:30
Brenda WallaceandClaude Sonnet 5 a7fd4768b4 Update the edit garden spec: Edit on a card opens the dialog
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 17:42:26 +12:00
Brenda WallaceandClaude Sonnet 5 c4c0488a41 Add feature specs for the edit garden dialog
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 17:42:26 +12:00
Brenda WallaceandClaude Sonnet 5 86deb55d53 Let garden cards edit a garden as JSON
GET /gardens/:slug/edit.json gives the form's values and choices, and
PATCH /gardens/:slug.json returns the garden's refreshed card, or the
validation errors as 422, without queueing a flash message.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 17:42:26 +12:00
Brenda WallaceandClaude Sonnet 5 5cba0909e1 Update the garden index spec for chip menus
An owner's planting chip is a menu now, with a View link to the planting.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 17:42:26 +12:00
Brenda WallaceandClaude Sonnet 5 6987d66f95 Add specs for the mark as finished dialog
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 17:42:26 +12:00
Brenda WallaceandClaude Sonnet 5 82da18f68d Add feature specs for the add photo dialog, with Flickr stubbed
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 17:42:26 +12:00
Brenda WallaceandClaude Sonnet 5 9d33a0feaf Add a small page for a Flickr connect pop-up to end on
/authentications/connected says whether Flickr got connected and closes
itself. It has no site header or footer, as it appears in a small window.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 17:42:26 +12:00
Brenda WallaceandClaude Sonnet 5 d9d3ebb9bd Let garden cards choose and add a Flickr photo as JSON
GET /photos/new.json gives a page of the member's Flickr photos (or says
to connect or reconnect Flickr), and POST /photos.json adds one to the
planting or garden and returns the garden's refreshed card, or the
validation errors as 422.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 17:42:26 +12:00
Brenda WallaceandClaude Sonnet 5 f7d10f50de Add feature specs for the save seeds and record harvest dialogs
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 17:42:26 +12:00
Brenda WallaceandClaude Sonnet 5 eb92386f0e Let garden cards save seeds as JSON
GET /plantings/:slug/seeds/new.json gives the form's starting values and
choices; POST /seeds.json returns a link to the new seed, or the
validation errors as 422, without queueing a flash message.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 17:42:26 +12:00
Brenda WallaceandClaude Sonnet 5 2142fe2f65 Show a tick on the progress bar for each harvest
Each harvest is a small tick at its date on the bar's own scale, with the
date as a tooltip and a hidden sentence for screen readers. The card data
carries the positions, worked out with the same formula as the fill.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 17:42:26 +12:00
Brenda WallaceandClaude Sonnet 5 7596b2634c Put Mark as finished last in the planting menu, below a divider
It takes the planting off the list, so it goes after the everyday
actions, separated from them. The card menu and the garden page menu
match.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 17:42:26 +12:00
Brenda WallaceandClaude Sonnet 5 1c5d90230b Fix the JSON request specs, and tolerate an empty harvest param
The update helper took its hash as keywords, and the form GET sent a JSON
content type, which Rails wraps into an empty harvest param that new then
rejected. The specs are fixed, and new ignores a blank harvest param.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 17:42:26 +12:00
Brenda WallaceandClaude Sonnet 5 e19473205c Let garden cards record a harvest as JSON
GET /plantings/:slug/harvests/new.json gives the form's starting values
and choices; POST /harvests.json returns the garden's refreshed card, or
the validation errors as 422.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 17:42:26 +12:00
Brenda WallaceandClaude Sonnet 5 ee79428271 Call them inactive gardens, and explain what that page is
Rename the past gardens page to inactive gardens throughout (label, route,
action, view, styles). Move its link on the gardens page to below the
member's card, with a line saying what it holds, and add an explanation
to the page itself.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 17:42:26 +12:00
Brenda WallaceandClaude Sonnet 5 e68ef96292 Replace the include in-active tickbox with a link to past gardens
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 17:42:26 +12:00
Brenda WallaceandClaude Sonnet 5 dcf59ff4e7 Add a gallery page of a member's past gardens
/members/:slug/gardens/past shows their inactive gardens as photo cards:
when each was in use, how many plantings, and the crops grown in it. The
owner (or a collaborator) can mark a garden active again from its card.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 17:42:26 +12:00
Brenda WallaceandClaude Sonnet 5 1e80abb70c Take Plant something here out of the garden card menu
The Add planting button does that now. The card data carries its link as
plant_url, instead of it being borrowed from a menu item.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 17:42:26 +12:00
Brenda WallaceandClaude Sonnet 5 6685e253c5 Drop the garden choice from the planting edit dialog
Moving a planting between gardens isn't part of quick editing. The
dialog no longer asks, or sends, which garden.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 17:42:26 +12:00
Brenda WallaceandClaude Sonnet 5 fef5463292 Serve a planting's form values as JSON for the edit dialog
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 17:42:26 +12:00
Brenda WallaceandClaude Sonnet 5 29708a281e Let garden cards update a planting as JSON
PATCH /plantings/:slug.json now returns the garden's refreshed card, as
create does, or the validation errors as 422. Annual plantings in the
card say whether the viewer can edit them.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 17:42:26 +12:00
Brenda WallaceandClaude Sonnet 5 27f4282f5b Let plantings use a crop's alternate name
A planting can now choose one of its crop's alternate names (eg. "lauki"
for calabash, "aubergine" for eggplant) and shows it instead of the
crop's primary name.

- plantings.alternate_name_id, validated to belong to the planting's crop
- Planting#display_name, used on cards, the planting page, garden lists,
  the member page, the homepage and the RSS/ICS feeds
- planting form offers the crop's alternate names, preselecting the one
  that was searched for
- crop autosuggest shows the matched alternate name, eg.
  "aubergine (eggplant)", via matched_alternate_name in the search JSON

Part of #554

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 21:28:46 +12:00
Brenda WallaceandClaude Sonnet 5 809b213825 Fix the photo edit page's save button, which wouldn't compile
'.= f.submit' is not valid Haml, so the page raised a syntax error. Add a
request spec that renders it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 17:40:20 +12:00
Brenda WallaceandClaude Sonnet 5 a05df8546f Colour progress bars by how the planting is getting on
Blue while growing, green while harvesting, amber when late, red when very
late, grey when finished. The track is now neutral grey so every colour
reads against it. Planting#progress_state decides, and the garden cards,
planting progress and timeline bars use it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-20 21:08:52 +12:00
Brenda Wallace 2989dafc4c Merge dev into chore/bootstrap-5 2026-09-20 20:20:51 +12:00
Daniel O'Connorandgoogle-labs-jules[bot] d3e9da077b Optimize Wikidata rake task using find_each and eager loading (#4812)
- Replace Crop.all.each with Crop.includes(:scientific_names).find_each
- Avoid per-crop N+1 queries when looking up Wikidata IDs from scientific names
- Add RSpec tests for wikidata:import_alternate_names task

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
2026-09-20 17:27:11 +09:30
Daniel O'Connorandgoogle-labs-jules[bot] 2b366213e8 Cap RSS feeds iterate unbounded associations to 50 records (#4809)
- Cap @member.posts in app/views/members/show.rss.haml with .limit(50)
- Cap @post.comments in app/views/posts/show.rss.haml with .limit(50) and eager load authors
- Remove eager loading of all comments+authors on Post in PostsController#show
- Add view specs to verify RSS feed limits

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
2026-09-20 16:41:28 +09:30
Brenda WallaceandClaude Sonnet 5 487a7c3b83 Stop the crop wrangler spec racing a live GBIF request
Typing a scientific name makes the page ask GBIF for suggestions after
jQuery UI's 300ms delay. Whether that request lands before Save navigates away
is a race the test only usually won, and when it lost, VCR blocked the real
request and failed the spec. Answer the suggestion with a double instead.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-20 19:00:48 +12:00
Brenda WallaceandClaude Sonnet 5 9dae899693 Quick planting addition form (#4808)
* Add rake tasks to pull crops and a member's data from production

crops:pull_from_production copies approved crops, and
members:pull_from_production copies one member's gardens, plantings and
planting photos, from growstuff.org into a local database. Both refuse to
run in a production environment.

They are built to be gentle on a server that has been struggling: one
request at a time with a pause between requests, exponential backoff
(honouring Retry-After) on timeouts, 429s and 5xx, and a hard stop rather
than endless retries. The client also stops if a server keeps sending the
same page, and will not follow a next link to another host.

Gardens and plantings come from the site's own JSON, which carries every
column; the JSON:API only exposes a garden's name, and its related
resource endpoints return the wrong records. Only the crops that a
member's plantings use are fetched. Flickr photos are stored as records
pointing at Flickr's image URLs, as on production, so nothing is
downloaded from Flickr.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Add ACTIVE_ONLY option to the member import

ACTIVE_ONLY=true leaves out the site's all=1 flag, so production lists only
the member's active gardens and current (not finished, not failed)
plantings. The server does the filtering, so it also means fewer requests,
and no crops or photos are fetched for plantings that are skipped.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Bundle JavaScript with esbuild via jsbundling-rails

Adds React and an esbuild build (yarn build, yarn build:watch) that writes
app/assets/builds/react_islands.js, which Sprockets serves. jsbundling-rails
runs the build as part of assets:precompile, which CI and the Dockerfile
already run after yarn install.

react_islands.jsx is the entry point: it mounts a React component into every
element with data-react-component. No components are registered yet.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Add react_component view helper

Renders an empty element carrying the component name and its props as JSON,
for react_islands.jsx to mount a React component into. Props go through
Rails' attribute escaping.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Show a React GardenSummary island on the gardens index

The first React island, to prove the pipeline end to end: server-rendered
props (owner and garden count) drive a small component with client-side
state. Loaded only on this page, deferred, via the scripts content block.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Add GardenCardSerializer for the React garden cards

Turns a page of gardens into plain hashes: the garden, its owner, its
actions menu, and its active plantings split into perennials and annuals,
with progress, badges and quick actions. It mirrors what gardens/_card,
gardens/_actions and the planting partials render today, so the server
still decides labels, links and permissions.

Active plantings for the whole page are loaded in one query (with harvests
and the crop's parents), instead of several queries per garden.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Render the garden cards on the index with React

The gardens index now hands GardenCardSerializer's output to a GardenCards
island instead of rendering gardens/_card per garden. The actions menu moves
to the top right of the card header. Everything else on the card is the same:
image, perennials, and each annual planting with badges, progress bar and its
own quick actions menu. The server still chooses labels, links and
permissions.

The dropdowns keep the existing Bootstrap and jquery_ujs markup
(data-bs-toggle, data-method, data-confirm), so mark inactive, delete and the
confirmations work unchanged. Menu button ids are now unique per card, so the
edit-from-index spec finds the menu by its Actions text instead of the old
repeated id.

gardens/_card stays for the activity page. This replaces the GardenSummary
island, which only proved the pipeline.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Don't raise when validating a planting that has no garden

owner_must_match_garden_owner called garden.owner unconditionally, so a
planting with a missing garden raised NoMethodError (a 500) instead of
failing the garden presence validation.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Create plantings as JSON for the React garden cards

POST /plantings.json returns the garden's updated card (201), or the
validation errors (422), so the cards can show a new planting without a page
reload. It is for our own pages only: session cookie plus CSRF token, and no
CORS headers, unlike the public /api/v1.

Creating now also checks the garden is yours. Before, can :create Planting
had no garden condition and create never authorized the garden, so a crafted
request could plant into someone else's garden. This applies to the HTML form
as well. A missing garden is still left to the presence validation.

CanCan::AccessDenied now answers JSON requests with a 403 and an error,
instead of redirecting them.

The request spec logs in through a real cookie session to exercise CSRF
protection: PlantingsController uses the null_session strategy, so a request
without the token is treated as signed out (401), and the sign_in test helper
would bypass that.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Don't ask which garden when planting from a garden

Arriving at the planting form with a garden_id of one of your own gardens
(from 'Plant something here') now shows 'Planting in <garden>' with a hidden
garden field and a link to choose a different one, instead of the garden
radio buttons. Any other arrival, including a garden_id that isn't yours,
still asks as before.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Open 'Plant something here' as a dialog over the garden cards

Clicking 'Plant something here' on a card opens a dialog to plant into that
garden, without leaving the list or asking which garden. Saving posts to
POST /plantings.json, swaps in the garden's updated card so the new planting
shows straight away (briefly highlighted, with a confirmation), and closes the
dialog. Errors show inside it and it stays open. A ctrl/cmd/shift-click still
opens the normal form.

The dialog is a React-controlled Bootstrap modal: focus moves in and is
trapped, Escape and the backdrop close it, and focus returns afterwards. The
crop picker searches /crops/search.json like the existing autosuggest, and the
planted-from and sun choices come from Planting's constants via props.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Use the Bootstrap 4 class names the app's CSS defines in the card components

The stylesheet is MDB 4 (Bootstrap 4), with Bootstrap 5's JavaScript on top,
so Bootstrap 5-only classes had no styling: the dialog's close button was
btn-close (a small grey box), and visually-hidden, form-select and text-start
did nothing. Use close with a times sign, sr-only, form-control and text-left,
as the app's existing modals do.

dropdown-menu-end stays, because Bootstrap 5's dropdown JavaScript reads it to
place the menu; dropdown-menu-right is added for the CSS.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Make the plant-something form read as a sentence

'I planted [N] [crop](s) on [date] from [seed] in [sun].' Each blank is an
underlined inline field: quantity, the crop search, the planted date, and
planted-from and sun/shade selects. The (s) drops away when the quantity is 1.
The blanks are labelled for screen readers (Quantity, Crop, Planted date,
Planted from, Sun or shade). Notes and 'Mark as finished' stay below.

The browser spec now fills in quantity, planted from and sun as well, and
checks they are saved.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Hint at typing a crop name, and offer to request one that isn't found

The crop blank says 'type a crop name'. When a search finds nothing it says
so and links to the new-crop form (in a new tab, so the dialog stays open),
as the old planting form did.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Say 'number' in the quantity blank

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Choose a crop with the cursor keys

The crop picker is now an accessible combobox. As you type, matches appear in
a listbox: Up and Down move through them, Enter chooses the highlighted one
(or the top match if none is highlighted), and Escape closes the list before a
second Escape closes the dialog. Focus moves to 'Change' once a crop is chosen.
Matches can still be clicked.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Trim the plant-something form to just the sentence

Remove 'Mark as finished' and the 'Tell us more about it' notes from the
dialog, along with the never-shown rating field it carried in its state.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Make the highlighted crop match obvious

The theme's list-group 'active' shade was barely darker than the other
matches. The one Enter would choose now has a solid blue background, white
bold text and a darker left edge.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Tidy the plant-something sentence layout

Group the sentence into phrases (I planted..., on..., from..., in...) that stay
together when it wraps, so a word is never stranded away from its blank. Hide
the quantity spinner that clipped 'number', widen the date, and give the two
selects a minimum width, with a grey 'optional' when they are still empty.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Don't let a second Enter undo a chosen crop

After choosing a crop the input is replaced by the crop's name and a Change
button. Focus went to that button, so pressing Enter again pressed it and
cleared the choice. Focus now moves on to the date blank, so a second Enter
submits the form as it would from any field.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Plant with one search and a confirmation, instead of the sentence form

The dialog asks one big question, 'What did you plant?', with the same
crop search as before (arrow keys, Enter, and a way to request a crop that
isn't found). Choosing a crop shows it back ('You're planting lettuce in
Orchard, today.') with Change and Plant it buttons, and focus lands on Plant
it so Enter confirms. The planting gets today's date and no other details;
those can be added later, which keeps adding one quick.

Drops the sentence blanks, their styles, and the planted-from/sun option lists
the page passed down.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Fix unreadable text on a just-planted crop

The highlight for a newly added planting set an inline pale green background
on the crop chip. Chips are brown with white text, so the name became white on
light green. The highlight is now a CSS class that leaves the chip's colours
alone: a soft green row with a left bar for annual plantings, and a green ring
around the chip for perennials. The browser spec checks the chip carries no
inline style.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Tidy the plant dialog and show what is happening at each stage

Standard patterns so the dialog always says where you are and what it is doing:
- a two-step indicator (Choose a crop, Confirm) with a tick on the finished step
- a spinner in the search box while searching, plus a 'Searching...' status, a
  count of matches, and an info icon on the 'no crops match' note
- the choice shown back as a Crop / Garden / When summary with a Change button
- Plant it as a filled green primary button that stays green while focused (the
  theme greys a focused button) and shows a spinner while planting, with Cancel
  and Change disabled meanwhile
- an error panel with a warning icon that says the choice is kept for retrying
- the matches listed in the dialog rather than floating over its buttons
- a tick on the 'Planted ...' notice on the cards
- the shovel (spade-marker.svg, the map marker) in the dialog title

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Build the React bundle in CI and for fresh checkouts

app/assets/builds/react_islands.js is built by esbuild and not committed, so on
a fresh checkout pages that load it (the gardens index) fail with 'The asset
react_islands.js is not present in the asset pipeline'. The rspec CI job never
built it: only the browser-feature jobs did, through assets:precompile. Add a
yarn build step there, and have the specs build the bundle once if it is
missing so a local checkout doesn't fail either.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Add the missing 'View' translation

The planting quick-actions menu used t('view'), which has no translation. In a
view Rails hides that by showing a humanised 'View'; the card serializer uses
I18n.t, which showed 'Translation missing: en.view' in the menu. Add
buttons.view, use it in both places, and add a serializer spec that fails if
any card label is a missing translation.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Fix two crop_jsonld_data specs that could not pass

The helper builds its hash with symbol keys, but the spec read 'data["@context"]'
with a string key. And 'caps posts and photos at 50' relied on
create(:post, crops: [crop]), which creates no crop_posts rows here, and on
crop.photo_associations.create!, which the Crop model's own association leaves
without a photographable. Link posts with CropPost and photos with
PhotoAssociation explicitly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Run the helper and serializer specs in CI

The rspec job ran lib, services, models, controllers, views, routing and
requests, but not spec/helpers or spec/serializers, so those specs (including
the react_component helper and the card serializer) never ran there, and two
broken crops_helper specs went unnoticed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Tidy the garden cards

The cards were cluttered and misaligned. Now:
- the header has a smaller title with the owner under it, and the menu top right
- a smaller picture, and 'Perennials' and 'Annuals' headings instead of a
  divider and a gap; 'Nothing planted here yet.' for an empty garden
- each annual planting is a row of three aligned columns: the crop with when it
  was planted (which tells duplicates apart), then its badges and progress bar,
  then a labelled three-dots menu in place of a bare chevron
- the note when there is nothing to predict from is one short muted line that
  no longer repeats the crop name
- on a phone the picture and rows stack

Also restores the just-planted highlight styles, which a stylesheet edit had
dropped, and adds a spec that the highlight has a background, since the class
existing was all that was checked.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Make the planting menu button compact

The theme gives every .btn a 5.5rem minimum width, so the three-dots button's
hover background was a wide grey box. It is now a 2.25rem round button.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-20 16:04:15 +09:30
Brenda WallaceandClaude Sonnet 5 a9193dc988 Use Bootstrap 5 close buttons and alert-dismissible
.close with a times character -> .btn-close (which draws its own cross), with an
aria-label on each. The alert helper said alert-dismissable, Bootstrap 3's
spelling; Bootstrap 5 needs alert-dismissible for the close button's spacing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-20 18:03:36 +12:00
Daniel O'Connorandgoogle-labs-jules[bot] b6270c8d0c Add pagination to crops#data_improvement (#4807)
In `CropsController#data_improvement`, paginate `@crops` with
`per_page: 50` so tabs like photos/descriptions do not render all
crops at once. In `data_improvement.html.haml`, render `= will_paginate @crops`.

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
2026-09-20 15:04:14 +09:30
Daniel O'Connorandgoogle-labs-jules[bot] a3811cf9bd Optimize SearchCrops#search_data planters_ids and has_photos (#4806)
Use `.distinct` when fetching `planters_ids` to avoid duplicate member
IDs and replace `photos.size` with the `photo_associations_count`
counter cache column for `has_photos`.

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
2026-09-20 14:41:33 +09:30
Daniel O'Connorandgoogle-labs-jules[bot] c7eef27f9a Fix new harvest from planting not prefilling crop (#4786)
When initializing a new harvest from a planting (via `planting_slug`
or `harvest[planting_id]`), `HarvestsController#new` now properly looks
up the planting, assigns `@harvest.planting` and `@harvest.crop` from
the planting's crop, and sets `@planting` and `@crop` instance variables
for view rendering.

Regression tests added to `HarvestsController` spec and request spec.

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
2026-09-20 14:10:14 +09:30
Brenda WallaceandClaude Sonnet 5 23e83b1a5f Add rake tasks to pull crops and a member's data from production (#4803)
* Add rake tasks to pull crops and a member's data from production

crops:pull_from_production copies approved crops, and
members:pull_from_production copies one member's gardens, plantings and
planting photos, from growstuff.org into a local database. Both refuse to
run in a production environment.

They are built to be gentle on a server that has been struggling: one
request at a time with a pause between requests, exponential backoff
(honouring Retry-After) on timeouts, 429s and 5xx, and a hard stop rather
than endless retries. The client also stops if a server keeps sending the
same page, and will not follow a next link to another host.

Gardens and plantings come from the site's own JSON, which carries every
column; the JSON:API only exposes a garden's name, and its related
resource endpoints return the wrong records. Only the crops that a
member's plantings use are fetched. Flickr photos are stored as records
pointing at Flickr's image URLs, as on production, so nothing is
downloaded from Flickr.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Add ACTIVE_ONLY option to the member import

ACTIVE_ONLY=true leaves out the site's all=1 flag, so production lists only
the member's active gardens and current (not finished, not failed)
plantings. The server does the filtering, so it also means fewer requests,
and no crops or photos are fetched for plantings that are skipped.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-20 14:06:01 +09:30
eb427f00de Optimize PaperTrail whodunnit query in CropsController#show (#4796)
* Optimize PaperTrail whodunnit query in CropsController#show

Replace @crop.versions.map(&:whodunnit) with
@crop.versions.distinct.pluck(:whodunnit) to avoid instantiating all PaperTrail version objects and loading object/object_changes into memory.

Co-authored-by: CloCkWeRX <365751+CloCkWeRX@users.noreply.github.com>

* Optimize PaperTrail whodunnit query in CropsController#show

Replace @crop.versions.map(&:whodunnit) with
@crop.versions.distinct.pluck(:whodunnit) to avoid instantiating all PaperTrail version objects and loading object/object_changes into memory. Added unit test in spec/controllers/crops_controller_spec.rb.

Co-authored-by: CloCkWeRX <365751+CloCkWeRX@users.noreply.github.com>

* Optimize PaperTrail whodunnit query in CropsController#show

Use @crop.versions.reorder(nil).distinct.pluck(:whodunnit) to avoid instantiating all PaperTrail version objects into memory and prevent PostgreSQL PG::InvalidColumnReference errors with SELECT DISTINCT.

Co-authored-by: CloCkWeRX <365751+CloCkWeRX@users.noreply.github.com>

* Fix crop route parameter in controller spec

Update parameter key from id to slug in CropsController show spec to match param: :slug route definition.

Co-authored-by: CloCkWeRX <365751+CloCkWeRX@users.noreply.github.com>

---------

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
2026-09-20 13:53:58 +09:30
Daniel O'Connorandgoogle-labs-jules[bot] a349ce784a Optimize Like lookups in Likeable concern (#4799)
- Update liked_by? to return false if member is nil and use likes.exists?(member_id: member.id) instead of allocating login name arrays.
- Simplify liked_by_members_names to use members.pluck(:login_name).
- Update app/views/photos/_likes.html.haml to use photo.liked_by?(current_member).
- Add unit tests for liked_by? in spec/models/like_spec.rb.

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
2026-09-20 13:46:02 +09:30
Brenda WallaceandClaude Sonnet 5 571405a840 Stop Rack::Attack depending on memcached, and cut the memcached timeout (#4800)
* Keep Rack::Attack counters in process memory

Rack::Attack was storing its request counters in Rails.cache, which is
memcached in production. On 2026-09-19, memcached calls were timing out
in the production logs. When that happens, the counters stop working.

In the same log window, one IP made 753 requests. 717 of them
succeeded. The 15 per minute throttle and the 500 per day ban did not
stop it.

Rack::Attack now uses its own in-memory store, capped at 8 MB. Each
Puma worker keeps its own counters, so the limits apply per worker.

Refs #1640

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Add BLOCKED_IPS list to Rack::Attack

When one crawler overloads the site, we need a fast way to block it.
Rack::Attack now blocks every IP listed in the BLOCKED_IPS environment
variable. The list is comma separated.

Setting the variable does not need a code change. It also keeps IP
addresses out of the repository.

Refs #1640

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Lower the memcached socket timeout to 0.5 seconds

The production cache store waited up to 1.5 seconds for each memcached
call. Dalli's own default is 1 second. Dalli also retries after a
failure, so one bad cache call can block a Puma thread for longer than
the timeout.

On 2026-09-19, the production logs showed memcached timeouts every few
seconds. With only 5 threads per Puma worker, blocked threads make the
site slow. Slow requests hold memory for longer.

The timeout is now 0.5 seconds. A healthy memcached call takes a few
milliseconds. A call that takes 0.5 seconds has failed in practice, so the
request is better off skipping the cache.

Refs #1640

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-20 13:36:25 +09:30
Brenda WallaceandClaude Sonnet 5 82480f9ce8 Fix races in the members list and places search feature specs (#4801)
* Wait for the page reload in the members list spec

The "members list" feature specs click "Show" and then read the list of
members right away. "Show" submits a GET form, so the browser loads a
whole new page. The spec could read the list from the old page. The old
page was then replaced, and Selenium raised StaleElementReferenceError.

This failed once in CI on the pull request for #4800:
https://github.com/Growstuff/growstuff/actions/runs/35477162596

Both examples now wait for the new URL before they read the list. The
URL contains the chosen sort order, so the wait only ends once the new
page has loaded.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Wait for the place page in the places search spec

The "User searches" specs fill in the search form, click "Search", and
then read the page straight away. The search goes to /places/search,
which redirects to /places/<place>. The spec could read the page while
the browser was still moving to the new page.

Selenium then raised "Node with given id does not belong to the
document". It failed in CI on the first example in "with a valid place":
https://github.com/Growstuff/growstuff/actions/runs/35481754102

The search_with helper now waits for the place page URL before it
returns. A blank search stays on the same URL, so it does not wait.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Rename the places CI workflow from "Admin" to "Places"

The workflow in ci-features-places.yml was named "CI Features - Admin".
It looks like it was copied from the admin workflow and never renamed.
It runs spec/features/places/.

Two workflows then had the same name. Every commit showed two
"CI Features - Admin" runs. A failing places spec appeared under the
admin name, which sent people to the wrong specs:
https://github.com/Growstuff/growstuff/actions/runs/35481754102

Branch protection requires a check called "rspec". That is the job
name, not the workflow name, so the required check does not change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-20 13:35:25 +09:30
4d87d9ccb9 Optimize places memory usage by paginating, plucking fields, and limiting nearest_to (#4792)
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
Co-authored-by: Brenda Wallace <brenda@wallace.net.nz>
2026-09-20 13:30:42 +09:30
Daniel O'Connorandgoogle-labs-jules[bot] 4dee371b34 Optimize CanCan abilities using declarative hash conditions (#4797)
Refactor block-based CanCan ability definitions for ScientificName, AlternateName, Planting, Activity, and Harvest into declarative hash conditions to eliminate per-record SQL queries and N+1 evaluation overhead.

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
2026-09-19 18:19:00 +09:30
Daniel O'Connor 07f07e4c3d Merge pull request #4794 from Growstuff/improve-cache-memory-usage-9918781528129420138
Optimize caching memory usage in PhotoCapable and CropsHelper
2026-09-18 13:24:31 +09:30
google-labs-jules[bot]andCloCkWeRX 298a56f1c8 Use database distinct count for GardenType gardens
Replaces in-memory `.uniq.count`, `.uniq.empty?`, and `.uniq.each` calls
with database-level `.distinct` queries on GardenType gardens association.

Co-authored-by: CloCkWeRX <365751+CloCkWeRX@users.noreply.github.com>
2026-09-17 12:41:39 +00:00
Daniel O'Connor fa592e850c Remove rescue from index_everything method
Remove error handling from index_everything method.
2026-09-16 18:50:30 +12:00
google-labs-jules[bot]andCloCkWeRX 9ebd98de2d Harden Rack::Attack and pagination against aggressive web crawlers
- Configure Allow2Ban in Rack::Attack to ban IPs requesting >500 pages per day for 1 week (7 days).
- Add honeypot route /dont-crawl-me disallowed in robots.txt and configure Fail2Ban in Rack::Attack to ban IPs visiting it for 7 days upon 1 hit.
- Update PhotosController#index to raise ActiveRecord::RecordNotFound when page parameter is out of bounds, returning 404 Not Found to crawlers instead of 200 OK.
- Add request and controller specs for Rack::Attack rules and pagination 404 responses.

Co-authored-by: CloCkWeRX <365751+CloCkWeRX@users.noreply.github.com>
2026-09-16 18:50:30 +12:00
2f43175324 Fallback to crop specific photos in PhotoCapable thumbnail_url (#4739)
* Implement crop photo fallback in PhotoCapable thumbnail_url

Currently, plantings, seeds, and harvests display "no photo available" placeholders when they do not have specific photos attached. This change enables the PhotoCapable#thumbnail_url method to automatically fall back to the associated crop's thumbnail_url when no item-specific photo is present.

Additionally, this commit:
- Fixes validation in PhotoAssociation to handle crop photos that do not have a polymorphic photographable owner.
- Disables cache store during testing to prevent stale/cached default photos from leaking across specs.
- Adds comprehensive unit tests for the fallback logic.

Co-authored-by: CloCkWeRX <365751+CloCkWeRX@users.noreply.github.com>

* Keep only core crop photo fallback and unit tests, removing excess changes

Co-authored-by: CloCkWeRX <365751+CloCkWeRX@users.noreply.github.com>

---------

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
2026-08-10 20:09:18 +09:30
4563df45d0 Upgrade to Rails 8.1 (#4685)
* Upgrade application to Rails 8.1

- Update rails gem to ~> 8.1.0
- Set config.load_defaults 8.1 in application.rb
- Add gem 'csv' for Ruby 3.4+ compatibility
- Replace deprecated 'render text:' with 'render plain:' in PagesController
- Add compatibility patch for jsonapi-resources routing in Rails 8.1
- Add compatibility patch for Faraday 2.x error constants
- Add Searchkick test stubs for environments without Elasticsearch
- Include required Active Storage update migrations

Co-authored-by: CloCkWeRX <365751+CloCkWeRX@users.noreply.github.com>

* Remove disabled searchkick

* Apply suggestion from @CloCkWeRX

* Delete config/initializers/faraday_patch.rb

* Apply suggestions from code review

Co-authored-by: Daniel O'Connor <daniel.oconnor@gmail.com>

* Remove modifications for tests without elasticsearch

* Remove psych gem and its dependencies

Removed psych gem version 5.4.0 and its dependencies.

---------

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
2026-08-09 22:53:15 +09:30
c9b8c54b81 Fix plantings ICS feed regression and enable request spec (#4687)
* Fix plantings ICS feed regression and enable request spec

- Update `app/views/plantings/index.ics.erb` to use ActiveRecord method calls instead of hash bracket access.
- Enable `spec/requests/plantings_spec.rb` and update it to account for mirrored harvest events.
- Add `config/initializers/faraday_patch.rb` to fix Searchkick/Faraday 2.x compatibility issues.

Co-authored-by: CloCkWeRX <365751+CloCkWeRX@users.noreply.github.com>

* Delete config/initializers/faraday_patch.rb

* Apply suggestion from @CloCkWeRX

* Apply suggestions from code review

Co-authored-by: Daniel O'Connor <daniel.oconnor@gmail.com>

* Apply suggestion from @CloCkWeRX

---------

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
2026-06-24 23:18:24 +09:30
fa7f460510 Refactor Plantings to remove Elasticsearch dependency (#4686)
* Refactor Plantings to remove Elasticsearch dependency

This commit refactors the Plantings controller, model, and views to use
ActiveRecord instead of Searchkick/Elasticsearch.

Key changes:
- Refactored `PlantingsController#index` to use an ActiveRecord-based
  `plantings` method.
- Moved `homepage_records` logic from `SearchPlantings` concern to the
  `Planting` model using ActiveRecord scopes.
- Removed `SearchPlantings` concern and deleted the file.
- Updated `home/_plantings.html.haml` and `plantings/index.rss.haml` to
  use dot notation for `Planting` object attributes.
- Updated specs to remove Searchkick-related setup and assertions.

Co-authored-by: CloCkWeRX <365751+CloCkWeRX@users.noreply.github.com>

* Fix Planting.reindex

* Fix test

* Mark test pending

---------

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
2026-06-24 22:26:49 +09:30